refactor data fetching
This commit is contained in:
@@ -8,11 +8,13 @@ import type * as bsky from '#/types/bsky'
|
||||
|
||||
export function ActionsWrapper({
|
||||
message,
|
||||
convoId,
|
||||
isFromSelf,
|
||||
senderProfile,
|
||||
children,
|
||||
}: {
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
convoId: string
|
||||
hasReactions?: boolean
|
||||
isFromSelf: boolean
|
||||
senderProfile?: bsky.profile.AnyProfileView
|
||||
@@ -21,7 +23,10 @@ export function ActionsWrapper({
|
||||
const {t: l} = useLingui()
|
||||
|
||||
return (
|
||||
<MessageContextMenu message={message} senderProfile={senderProfile}>
|
||||
<MessageContextMenu
|
||||
message={message}
|
||||
convoId={convoId}
|
||||
senderProfile={senderProfile}>
|
||||
{trigger =>
|
||||
// will always be true, since this file is platform split
|
||||
trigger.IS_NATIVE && (
|
||||
|
||||
@@ -16,12 +16,14 @@ import {hasReachedReactionLimit} from './util'
|
||||
|
||||
export function ActionsWrapper({
|
||||
message,
|
||||
convoId,
|
||||
hasReactions,
|
||||
isFromSelf,
|
||||
senderProfile,
|
||||
children,
|
||||
}: {
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
convoId: string
|
||||
hasReactions?: boolean
|
||||
isFromSelf: boolean
|
||||
senderProfile?: bsky.profile.AnyProfileView
|
||||
@@ -115,7 +117,10 @@ export function ActionsWrapper({
|
||||
)
|
||||
}}
|
||||
</EmojiReactionPicker>
|
||||
<MessageContextMenu message={message} senderProfile={senderProfile}>
|
||||
<MessageContextMenu
|
||||
message={message}
|
||||
convoId={convoId}
|
||||
senderProfile={senderProfile}>
|
||||
{({props, state, IS_NATIVE, control}) => {
|
||||
// always false, file is platform split
|
||||
if (IS_NATIVE) return null
|
||||
|
||||
@@ -31,10 +31,12 @@ import {hasReachedReactionLimit} from './util'
|
||||
|
||||
export let MessageContextMenu = ({
|
||||
message,
|
||||
convoId,
|
||||
senderProfile,
|
||||
children,
|
||||
}: {
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
convoId: string
|
||||
senderProfile?: bsky.profile.AnyProfileView
|
||||
children: TriggerProps['children']
|
||||
}): React.ReactNode => {
|
||||
@@ -181,11 +183,12 @@ export let MessageContextMenu = ({
|
||||
)}
|
||||
</ContextMenu.Outer>
|
||||
</ContextMenu.Root>
|
||||
|
||||
<ReportDialog
|
||||
control={reportControl}
|
||||
subject={{
|
||||
view: 'message',
|
||||
convoId: convo.convo.view.id,
|
||||
convoId,
|
||||
message,
|
||||
}}
|
||||
onAfterSubmit={() => {
|
||||
@@ -199,7 +202,7 @@ export let MessageContextMenu = ({
|
||||
control={blockOrDeleteControl}
|
||||
currentScreen="conversation"
|
||||
params={{
|
||||
convoId: convo.convo.view.id,
|
||||
convoId,
|
||||
message,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -85,12 +85,14 @@ function isWithinClusterBoundary({
|
||||
|
||||
let MessageItem = ({
|
||||
item,
|
||||
convoId,
|
||||
isGroupChat = false,
|
||||
prevMessage,
|
||||
nextMessage,
|
||||
relatedProfiles,
|
||||
}: {
|
||||
item: ConvoItem & {type: 'message' | 'pending-message'}
|
||||
convoId: string
|
||||
isGroupChat?: boolean
|
||||
prevMessage:
|
||||
| ChatBskyConvoDefs.MessageView
|
||||
@@ -431,6 +433,7 @@ let MessageItem = ({
|
||||
hasReactions={hasReactions}
|
||||
isFromSelf={isFromSelf}
|
||||
message={message}
|
||||
convoId={convoId}
|
||||
senderProfile={profile}>
|
||||
{AppBskyEmbedRecord.isView(message.embed) && (
|
||||
<MessageItemEmbed
|
||||
|
||||
@@ -114,6 +114,10 @@ function Inner({convoId}: {convoId: string}) {
|
||||
? parseConvoView(convoData, currentAccount?.did)
|
||||
: null
|
||||
|
||||
const isDisabled = Boolean(
|
||||
convoData?.members.find(m => m.did === currentAccount?.did)?.chatDisabled,
|
||||
)
|
||||
|
||||
// Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user,
|
||||
// we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be
|
||||
// empty. So, we also check for that possible state as well and render once we can.
|
||||
@@ -187,20 +191,18 @@ function Inner({convoId}: {convoId: string}) {
|
||||
hasScrolled={hasScrolled}
|
||||
setHasScrolled={setHasScrolled}
|
||||
isActive={isConvoActive(convoState)}
|
||||
isDisabled={convoState.status === ConvoStatus.Disabled}
|
||||
isDisabled={isDisabled}
|
||||
hasMessages={isConvoActive(convoState) && convoState.items.length > 0}
|
||||
readyToShow={readyToShow}
|
||||
/>
|
||||
{!readyToShow && (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_10,
|
||||
a.w_full,
|
||||
a.h_full,
|
||||
a.inset_0,
|
||||
{zIndex: -10},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
// t.atoms.bg,
|
||||
{backgroundColor: 'rgba(255,0,0,0.2)'},
|
||||
]}>
|
||||
<View style={[{marginBottom: 75}]}>
|
||||
<Loader size="xl" />
|
||||
@@ -219,6 +221,7 @@ function InnerReady({
|
||||
isActive,
|
||||
isDisabled,
|
||||
hasMessages,
|
||||
readyToShow,
|
||||
}: {
|
||||
hasScrolled: boolean
|
||||
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
|
||||
@@ -226,6 +229,7 @@ function InnerReady({
|
||||
isActive: boolean
|
||||
isDisabled: boolean
|
||||
hasMessages: boolean
|
||||
readyToShow: boolean
|
||||
}) {
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {top: topInset} = useSafeAreaInsets()
|
||||
@@ -317,12 +321,14 @@ function InnerReady({
|
||||
) : (
|
||||
header
|
||||
)}
|
||||
{isActive && (
|
||||
{isActive && convo && (
|
||||
<MessagesList
|
||||
convo={convo}
|
||||
hasScrolled={hasScrolled}
|
||||
setHasScrolled={setHasScrolled}
|
||||
hasAcceptOverride={!!params.accept}
|
||||
transparentHeaderHeight={IS_LIQUID_GLASS ? headerHeight : 0}
|
||||
hideMessages={!readyToShow}
|
||||
footer={footer}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
type NavigationProp,
|
||||
} from '#/lib/routes/types'
|
||||
import {logger} from '#/logger'
|
||||
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
|
||||
import {ConvoStatus} from '#/state/messages/convo/types'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {ConvoProvider} from '#/state/messages/convo'
|
||||
import {useConvoQuery} from '#/state/queries/messages/conversation'
|
||||
import {useEditGroupChatName} from '#/state/queries/messages/edit-group-chat-name'
|
||||
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
|
||||
import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members'
|
||||
@@ -34,6 +34,7 @@ import * as Dialog from '#/components/Dialog'
|
||||
import {
|
||||
type ConvoWithDetails,
|
||||
type GroupConvoMember,
|
||||
parseConvoView,
|
||||
} from '#/components/dms/util'
|
||||
import {Error} from '#/components/Error'
|
||||
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft'
|
||||
@@ -94,30 +95,35 @@ export function MessagesConversationSettingsScreen({route}: Props) {
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<ConvoProvider key={convoId} convoId={convoId}>
|
||||
<SettingsInner />
|
||||
<SettingsInner convoId={convoId} />
|
||||
</ConvoProvider>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsInner() {
|
||||
function SettingsInner({convoId}: {convoId: string}) {
|
||||
const {t: l} = useLingui()
|
||||
const convoState = useConvo()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {currentAccount} = useSession()
|
||||
const {data: convoData, error, refetch} = useConvoQuery({convoId})
|
||||
|
||||
if (convoState.status === ConvoStatus.Error) {
|
||||
const convo = convoData
|
||||
? parseConvoView(convoData, currentAccount?.did)
|
||||
: null
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Error
|
||||
title={l`Something went wrong`}
|
||||
message={l`We couldn’t load this conversation’s settings`}
|
||||
onRetry={() => convoState.error.retry()}
|
||||
onRetry={() => refetch()}
|
||||
sideBorders={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (!convoState.convo || !moderationOpts) {
|
||||
if (!convo || !moderationOpts) {
|
||||
return (
|
||||
<View style={[a.flex_1, a.align_center, a.justify_center]}>
|
||||
<Loader size="xl" />
|
||||
@@ -125,7 +131,7 @@ function SettingsInner() {
|
||||
)
|
||||
}
|
||||
|
||||
if (convoState.convo.kind !== 'group') {
|
||||
if (convo.kind !== 'group') {
|
||||
return (
|
||||
<Error
|
||||
title={l`Wrong kind of conversation`}
|
||||
@@ -141,13 +147,7 @@ function SettingsInner() {
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupSettings
|
||||
convo={convoState.convo}
|
||||
moderationOpts={moderationOpts}
|
||||
isReady={isConvoActive(convoState)}
|
||||
/>
|
||||
)
|
||||
return <GroupSettings convo={convo} moderationOpts={moderationOpts} />
|
||||
}
|
||||
|
||||
function keyExtractor(item: Item) {
|
||||
@@ -170,11 +170,9 @@ function isGroupMember(
|
||||
function GroupSettings({
|
||||
convo,
|
||||
moderationOpts,
|
||||
isReady,
|
||||
}: {
|
||||
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||
moderationOpts: ModerationOpts
|
||||
isReady: boolean
|
||||
}) {
|
||||
const [isPTRing, setIsPTRing] = useState(false)
|
||||
|
||||
@@ -254,7 +252,7 @@ function GroupSettings({
|
||||
/>
|
||||
)
|
||||
case 'ADD_MEMBERS_LINK':
|
||||
return <AddMembersLink convo={convo} disabled={!isReady} />
|
||||
return <AddMembersLink convo={convo} />
|
||||
case 'CHAT_MEMBER':
|
||||
return (
|
||||
<Member
|
||||
@@ -295,7 +293,6 @@ function GroupSettings({
|
||||
convo={convo}
|
||||
isOwner={isOwner}
|
||||
moderationOpts={moderationOpts}
|
||||
isReady={isReady}
|
||||
/>
|
||||
}
|
||||
renderItem={renderItem}
|
||||
@@ -311,12 +308,10 @@ function SettingsHeader({
|
||||
convo,
|
||||
isOwner,
|
||||
moderationOpts,
|
||||
isReady,
|
||||
}: {
|
||||
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||
isOwner: boolean
|
||||
moderationOpts: ModerationOpts
|
||||
isReady: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {i18n, t: l} = useLingui()
|
||||
@@ -476,7 +471,7 @@ function SettingsHeader({
|
||||
]}>
|
||||
<SettingsButton
|
||||
color={convo.view.muted ? 'negative_subtle' : 'secondary'}
|
||||
disabled={!isReady || isMuting}
|
||||
disabled={isMuting}
|
||||
icon={convo.view.muted ? BellOffIcon : BellIcon}
|
||||
label={
|
||||
convo.view.muted
|
||||
@@ -488,7 +483,7 @@ function SettingsHeader({
|
||||
/>
|
||||
{isOwner ? (
|
||||
<SettingsButton
|
||||
disabled={!isReady || isEditingName}
|
||||
disabled={isEditingName}
|
||||
icon={EditIcon}
|
||||
label={l`Edit this group chat’s name`}
|
||||
text={l`Edit name`}
|
||||
@@ -497,7 +492,7 @@ function SettingsHeader({
|
||||
) : null}
|
||||
{isJoinLinkEnabled ? (
|
||||
<SettingsButton
|
||||
disabled={!isReady || lockStatus !== 'unlocked'}
|
||||
disabled={lockStatus !== 'unlocked'}
|
||||
icon={ChainLinkIcon}
|
||||
label={
|
||||
isOwner
|
||||
@@ -511,7 +506,7 @@ function SettingsHeader({
|
||||
{canLockGroupChat ? (
|
||||
<SettingsButton
|
||||
color={lockStatus === 'locked' ? 'negative_subtle' : 'secondary'}
|
||||
disabled={!isReady || isLocking}
|
||||
disabled={isLocking}
|
||||
icon={LockIcon}
|
||||
label={
|
||||
lockStatus === 'locked'
|
||||
@@ -526,7 +521,6 @@ function SettingsHeader({
|
||||
) : null}
|
||||
{!isOwner && isReportLinkEnabled && (
|
||||
<SettingsButton
|
||||
disabled={!isReady}
|
||||
icon={FlagIcon}
|
||||
label={l`Report this group chat`}
|
||||
text={l`Report`}
|
||||
@@ -535,7 +529,7 @@ function SettingsHeader({
|
||||
)}
|
||||
{!isOwner && (
|
||||
<SettingsButton
|
||||
disabled={!isReady || isLeaving}
|
||||
disabled={isLeaving}
|
||||
icon={ArrowBoxLeftIcon}
|
||||
label={l`Leave this group chat`}
|
||||
text={l`Leave`}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {ChatBskyConvoDefs, moderateProfile} from '@atproto/api'
|
||||
@@ -7,35 +7,29 @@ import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {type ActiveConvoStates} from '#/state/messages/convo'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
|
||||
import {type ConvoWithDetails} from '#/components/dms/util'
|
||||
import {KnownFollowers} from '#/components/KnownFollowers'
|
||||
import {usePromptControl} from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons'
|
||||
|
||||
export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
|
||||
export function ChatStatusInfo({convo}: {convo: ConvoWithDetails}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const leaveConvoControl = usePromptControl()
|
||||
|
||||
const onAcceptChat = useCallback(() => {
|
||||
convoState.markConvoAccepted()
|
||||
}, [convoState])
|
||||
|
||||
// either the other person, or the chat owner
|
||||
// if we ever allow someone other than the owner to invite people, this will need to change
|
||||
const otherUser = convoState.convo.primaryMember
|
||||
const otherUser = convo.primaryMember
|
||||
|
||||
const lastMessage = ChatBskyConvoDefs.isMessageView(
|
||||
convoState.convo.view.lastMessage,
|
||||
)
|
||||
? convoState.convo.view.lastMessage
|
||||
const lastMessage = ChatBskyConvoDefs.isMessageView(convo.view.lastMessage)
|
||||
? convo.view.lastMessage
|
||||
: null
|
||||
|
||||
if (!moderationOpts) {
|
||||
@@ -65,7 +59,7 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
|
||||
<RejectMenu
|
||||
label={lastMessage ? l`Block or report` : l`Block`}
|
||||
icon={true}
|
||||
convo={convoState.convo.view}
|
||||
convo={convo.view}
|
||||
profile={otherUser}
|
||||
color="negative_subtle"
|
||||
size="large"
|
||||
@@ -80,7 +74,7 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
|
||||
context: 'Button',
|
||||
})}
|
||||
icon={true}
|
||||
convo={convoState.convo.view}
|
||||
convo={convo.view}
|
||||
color="secondary"
|
||||
size="large"
|
||||
currentScreen="conversation"
|
||||
@@ -88,7 +82,7 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
|
||||
onPress={leaveConvoControl.open}
|
||||
/>
|
||||
<LeaveConvoPrompt
|
||||
convoId={convoState.convo.view.id}
|
||||
convoId={convo.view.id}
|
||||
control={leaveConvoControl}
|
||||
currentScreen="conversation"
|
||||
hasMessages={false}
|
||||
@@ -97,8 +91,7 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
|
||||
<View style={[a.w_full, a.flex_row]}>
|
||||
<AcceptChatButton
|
||||
icon={true}
|
||||
onAcceptConvo={onAcceptChat}
|
||||
convo={convoState.convo.view}
|
||||
convo={convo.view}
|
||||
color="primary"
|
||||
size="large"
|
||||
currentScreen="conversation"
|
||||
|
||||
@@ -57,6 +57,7 @@ import {MessageItem} from '#/components/dms/MessageItem'
|
||||
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
||||
import {SystemMessageGroup} from '#/components/dms/SystemMessageGroup'
|
||||
import {SystemMessageItem} from '#/components/dms/SystemMessageItem'
|
||||
import {type ConvoWithDetails} from '#/components/dms/util'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
@@ -112,20 +113,25 @@ function onScrollToIndexFailed() {
|
||||
}
|
||||
|
||||
export function MessagesList({
|
||||
convo,
|
||||
hasScrolled,
|
||||
setHasScrolled,
|
||||
footer,
|
||||
hasAcceptOverride,
|
||||
transparentHeaderHeight,
|
||||
hideMessages,
|
||||
}: {
|
||||
convo: ConvoWithDetails
|
||||
hasScrolled: boolean
|
||||
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
|
||||
footer?: React.ReactNode
|
||||
hasAcceptOverride?: boolean
|
||||
transparentHeaderHeight?: number
|
||||
hideMessages?: boolean
|
||||
}) {
|
||||
const ax = useAnalytics()
|
||||
const convoState = useConvoActive()
|
||||
const isGroupChat = convo.kind === 'group'
|
||||
const agent = useAgent()
|
||||
const getPost = useGetPost()
|
||||
const {embedUri, setEmbed} = useMessageEmbed()
|
||||
@@ -419,7 +425,8 @@ export function MessagesList({
|
||||
return (
|
||||
<MessageItem
|
||||
item={item}
|
||||
isGroupChat={convoState.convo.kind === 'group'}
|
||||
convoId={convo.view.id}
|
||||
isGroupChat={isGroupChat}
|
||||
prevMessage={getNeighborMessage(renderItems, index - 1)}
|
||||
nextMessage={getNeighborMessage(renderItems, index + 1)}
|
||||
relatedProfiles={convoState.relatedProfiles}
|
||||
@@ -480,7 +487,7 @@ export function MessagesList({
|
||||
)
|
||||
|
||||
return (
|
||||
<InviteLinkDialogProvider convo={convoState.convo}>
|
||||
<InviteLinkDialogProvider convo={convo}>
|
||||
<KeyboardGestureArea
|
||||
interpolator="ios"
|
||||
// HACKFIX: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1419
|
||||
@@ -513,22 +520,24 @@ export function MessagesList({
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
<MaybeLoader isLoading={convoState.isFetchingHistory} />
|
||||
{convoState.convo?.kind === 'group' &&
|
||||
convoState.hasAllHistory ? (
|
||||
<MessagesListInfoPanel convo={convoState.convo} />
|
||||
{convo.kind === 'group' && convoState.hasAllHistory ? (
|
||||
<MessagesListInfoPanel convo={convo} />
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
// native only (prop is not supported on web)
|
||||
renderScrollComponent={renderScrollComponent}
|
||||
contentContainerStyle={{
|
||||
paddingBottom: platform({
|
||||
// ios is slightly larger as the input has no top padding
|
||||
ios: tokens.space.lg,
|
||||
android: tokens.space.md,
|
||||
web: 0, // web uses ListFooterComponent instead for scroll reasons
|
||||
}),
|
||||
}}
|
||||
contentContainerStyle={[
|
||||
hideMessages && {opacity: 0},
|
||||
{
|
||||
paddingBottom: platform({
|
||||
// ios is slightly larger as the input has no top padding
|
||||
ios: tokens.space.lg,
|
||||
android: tokens.space.md,
|
||||
web: 0, // web uses ListFooterComponent instead for scroll reasons
|
||||
}),
|
||||
},
|
||||
]}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={web({height: tokens.space.md + inputHeightJS})}
|
||||
@@ -558,6 +567,7 @@ export function MessagesList({
|
||||
{footer ?? (
|
||||
<ConversationFooter
|
||||
convoState={convoState}
|
||||
convo={convo}
|
||||
hasAcceptOverride={hasAcceptOverride}>
|
||||
{ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? (
|
||||
<MessageComposer
|
||||
@@ -633,6 +643,7 @@ type FooterState = 'loading' | 'new-chat' | 'request' | 'standard'
|
||||
|
||||
function getFooterState(
|
||||
convoState: ActiveConvoStates,
|
||||
convo: ConvoWithDetails,
|
||||
hasAcceptOverride?: boolean,
|
||||
): FooterState {
|
||||
if (convoState.items.length === 0) {
|
||||
@@ -643,7 +654,7 @@ function getFooterState(
|
||||
}
|
||||
}
|
||||
|
||||
if (convoState.convo.view.status === 'request' && !hasAcceptOverride) {
|
||||
if (convo.view.status === 'request' && !hasAcceptOverride) {
|
||||
return 'request'
|
||||
}
|
||||
|
||||
@@ -652,10 +663,12 @@ function getFooterState(
|
||||
|
||||
function ConversationFooter({
|
||||
convoState,
|
||||
convo,
|
||||
hasAcceptOverride,
|
||||
children,
|
||||
}: {
|
||||
convoState: ConvoState
|
||||
convo: ConvoWithDetails
|
||||
hasAcceptOverride?: boolean
|
||||
children?: React.ReactNode // message input
|
||||
}) {
|
||||
@@ -663,11 +676,11 @@ function ConversationFooter({
|
||||
return null
|
||||
}
|
||||
|
||||
const footerState = getFooterState(convoState, hasAcceptOverride)
|
||||
const footerState = getFooterState(convoState, convo, hasAcceptOverride)
|
||||
|
||||
switch (footerState) {
|
||||
case 'loading':
|
||||
return null
|
||||
return children
|
||||
case 'new-chat':
|
||||
return (
|
||||
<>
|
||||
@@ -676,7 +689,7 @@ function ConversationFooter({
|
||||
</>
|
||||
)
|
||||
case 'request':
|
||||
return <ChatStatusInfo convoState={convoState} />
|
||||
return <ChatStatusInfo convo={convo} />
|
||||
case 'standard':
|
||||
return children
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
ChatBskyConvoDefs,
|
||||
type ChatBskyConvoGetLog,
|
||||
type ChatBskyConvoSendMessage,
|
||||
type ChatBskyGroupDefs,
|
||||
} from '@atproto/api'
|
||||
import {XRPCError} from '@atproto/api'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
@@ -26,7 +25,6 @@ import {
|
||||
type ConvoDispatch,
|
||||
ConvoDispatchEvent,
|
||||
type ConvoError,
|
||||
ConvoErrorCode,
|
||||
type ConvoEvent,
|
||||
type ConvoItem,
|
||||
ConvoItemError,
|
||||
@@ -37,11 +35,6 @@ import {
|
||||
import {type MessagesEventBus} from '#/state/messages/events/agent'
|
||||
import {type MessagesEventBusError} from '#/state/messages/events/types'
|
||||
import {logger} from '#/state/messages/logger'
|
||||
import {
|
||||
type ConvoWithDetails,
|
||||
type GroupConvoMember,
|
||||
parseConvoView,
|
||||
} from '#/components/dms/util'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
|
||||
export function isConvoItemMessage(
|
||||
@@ -81,6 +74,7 @@ export class Convo {
|
||||
private agent: AtpAgent
|
||||
private events: MessagesEventBus
|
||||
private senderUserDid: string
|
||||
private getRecipientDids: () => string[]
|
||||
|
||||
private status: ConvoStatus = ConvoStatus.Uninitialized
|
||||
private error: ConvoError | undefined
|
||||
@@ -115,9 +109,6 @@ export class Convo {
|
||||
private emitter = new EventEmitter<{event: [ConvoEvent]}>()
|
||||
|
||||
convoId: string
|
||||
convo: ConvoWithDetails | undefined
|
||||
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
|
||||
recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined
|
||||
snapshot: ConvoState | undefined
|
||||
|
||||
constructor(params: ConvoParams) {
|
||||
@@ -126,12 +117,8 @@ export class Convo {
|
||||
this.agent = params.agent
|
||||
this.events = params.events
|
||||
this.senderUserDid = params.agent.assertDid
|
||||
this.getRecipientDids = params.getRecipientDids
|
||||
|
||||
if (params.placeholderData) {
|
||||
this.setupPlaceholderData(params.placeholderData)
|
||||
}
|
||||
|
||||
this.setConvo = this.setConvo.bind(this)
|
||||
this.subscribe = this.subscribe.bind(this)
|
||||
this.getSnapshot = this.getSnapshot.bind(this)
|
||||
this.sendMessage = this.sendMessage.bind(this)
|
||||
@@ -140,13 +127,9 @@ export class Convo {
|
||||
this.ingestFirehose = this.ingestFirehose.bind(this)
|
||||
this.onFirehoseConnect = this.onFirehoseConnect.bind(this)
|
||||
this.onFirehoseError = this.onFirehoseError.bind(this)
|
||||
this.markConvoAccepted = this.markConvoAccepted.bind(this)
|
||||
this.addReaction = this.addReaction.bind(this)
|
||||
this.removeReaction = this.removeReaction.bind(this)
|
||||
this.updateGroupName = this.updateGroupName.bind(this)
|
||||
this.updateGroupMembers = this.updateGroupMembers.bind(this)
|
||||
this.updateJoinLink = this.updateJoinLink.bind(this)
|
||||
this.updateLockStatus = this.updateLockStatus.bind(this)
|
||||
this.updateRelatedProfiles = this.updateRelatedProfiles.bind(this)
|
||||
}
|
||||
|
||||
private commit() {
|
||||
@@ -184,7 +167,6 @@ export class Convo {
|
||||
deleteMessage: this.deleteMessage,
|
||||
sendMessage: this.sendMessage,
|
||||
fetchMessageHistory: this.fetchMessageHistory,
|
||||
markConvoAccepted: this.markConvoAccepted,
|
||||
addReaction: this.addReaction,
|
||||
removeReaction: this.removeReaction,
|
||||
}
|
||||
@@ -193,7 +175,6 @@ export class Convo {
|
||||
deleteMessage: undefined,
|
||||
sendMessage: undefined,
|
||||
fetchMessageHistory: undefined,
|
||||
markConvoAccepted: undefined,
|
||||
addReaction: undefined,
|
||||
removeReaction: undefined,
|
||||
}
|
||||
@@ -203,7 +184,6 @@ export class Convo {
|
||||
return {
|
||||
status: ConvoStatus.Initializing,
|
||||
items: [],
|
||||
convo: this.convo,
|
||||
error: undefined,
|
||||
...shared,
|
||||
...emptyMethods,
|
||||
@@ -213,7 +193,6 @@ export class Convo {
|
||||
return {
|
||||
status: this.status,
|
||||
items: this.getItems(),
|
||||
convo: this.convo!,
|
||||
relatedProfiles: this.relatedProfiles,
|
||||
error: undefined,
|
||||
...shared,
|
||||
@@ -224,7 +203,6 @@ export class Convo {
|
||||
return {
|
||||
status: this.status,
|
||||
items: this.getItems(),
|
||||
convo: this.convo!,
|
||||
relatedProfiles: this.relatedProfiles,
|
||||
error: undefined,
|
||||
...shared,
|
||||
@@ -235,7 +213,6 @@ export class Convo {
|
||||
return {
|
||||
status: this.status,
|
||||
items: this.getItems(),
|
||||
convo: this.convo!,
|
||||
relatedProfiles: this.relatedProfiles,
|
||||
error: undefined,
|
||||
...shared,
|
||||
@@ -246,7 +223,6 @@ export class Convo {
|
||||
return {
|
||||
status: this.status,
|
||||
items: this.getItems(),
|
||||
convo: this.convo!,
|
||||
relatedProfiles: this.relatedProfiles,
|
||||
error: undefined,
|
||||
...shared,
|
||||
@@ -257,7 +233,6 @@ export class Convo {
|
||||
return {
|
||||
status: ConvoStatus.Error,
|
||||
items: [],
|
||||
convo: undefined,
|
||||
error: this.error!,
|
||||
isFetchingHistory: false,
|
||||
hasAllHistory: false,
|
||||
@@ -268,7 +243,6 @@ export class Convo {
|
||||
return {
|
||||
status: ConvoStatus.Uninitialized,
|
||||
items: [],
|
||||
convo: this.convo,
|
||||
error: undefined,
|
||||
isFetchingHistory: false,
|
||||
// Explicit null check since the value is initially undefined.
|
||||
@@ -322,20 +296,12 @@ export class Convo {
|
||||
this.withdrawRequestedPollInterval()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Disable: {
|
||||
this.status = ConvoStatus.Disabled
|
||||
void this.fetchMessageHistory() // finish init
|
||||
this.cleanupFirehoseConnection?.()
|
||||
this.withdrawRequestedPollInterval()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case ConvoStatus.Ready: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Resume: {
|
||||
void this.refreshConvo()
|
||||
this.requestPollInterval(ACTIVE_POLL_INTERVAL)
|
||||
break
|
||||
}
|
||||
@@ -357,12 +323,6 @@ export class Convo {
|
||||
this.withdrawRequestedPollInterval()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Disable: {
|
||||
this.status = ConvoStatus.Disabled
|
||||
this.cleanupFirehoseConnection?.()
|
||||
this.withdrawRequestedPollInterval()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -372,14 +332,8 @@ export class Convo {
|
||||
if (this.wasChatInactive()) {
|
||||
this.reset()
|
||||
} else {
|
||||
if (this.convo) {
|
||||
this.status = ConvoStatus.Ready
|
||||
void this.refreshConvo()
|
||||
this.maybeRecoverFromNetworkError()
|
||||
} else {
|
||||
this.status = ConvoStatus.Initializing
|
||||
void this.setup()
|
||||
}
|
||||
this.status = ConvoStatus.Ready
|
||||
this.maybeRecoverFromNetworkError()
|
||||
this.requestPollInterval(ACTIVE_POLL_INTERVAL)
|
||||
}
|
||||
break
|
||||
@@ -397,12 +351,6 @@ export class Convo {
|
||||
this.withdrawRequestedPollInterval()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Disable: {
|
||||
this.status = ConvoStatus.Disabled
|
||||
this.cleanupFirehoseConnection?.()
|
||||
this.withdrawRequestedPollInterval()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -421,10 +369,6 @@ export class Convo {
|
||||
this.error = action.payload
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Disable: {
|
||||
this.status = ConvoStatus.Disabled
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -447,17 +391,9 @@ export class Convo {
|
||||
this.error = action.payload
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Disable: {
|
||||
this.status = ConvoStatus.Disabled
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case ConvoStatus.Disabled: {
|
||||
// can't do anything
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -475,7 +411,6 @@ export class Convo {
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.convo = undefined
|
||||
this.snapshot = undefined
|
||||
|
||||
this.status = ConvoStatus.Uninitialized
|
||||
@@ -512,97 +447,23 @@ export class Convo {
|
||||
}
|
||||
}
|
||||
|
||||
private setConvo(convo: ChatBskyConvoDefs.ConvoView) {
|
||||
this.convo = parseConvoView(convo, this.senderUserDid) ?? this.convo
|
||||
if (this.convo) {
|
||||
for (const member of this.convo.members) {
|
||||
this.relatedProfiles.set(member.did, member)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateConvo(convo: Partial<ChatBskyConvoDefs.ConvoView>) {
|
||||
if (this.convo) {
|
||||
this.convo =
|
||||
parseConvoView({...this.convo.view, ...convo}, this.senderUserDid) ??
|
||||
this.convo
|
||||
for (const member of this.convo.members) {
|
||||
this.relatedProfiles.set(member.did, member)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the convo with placeholder data, if provided. We still refetch it before rendering the convo,
|
||||
* but this allows us to render the convo header immediately.
|
||||
* Merge a batch of related profiles into `this.relatedProfiles`. Called
|
||||
* both by internal ingestion (message-history + firehose responses) and
|
||||
* externally by `ConvoProvider` when `useListConvoMembersQuery` data
|
||||
* updates.
|
||||
*/
|
||||
private setupPlaceholderData(
|
||||
data: NonNullable<ConvoParams['placeholderData']>,
|
||||
) {
|
||||
this.setConvo(data.convo)
|
||||
updateRelatedProfiles(profiles: ChatBskyActorDefs.ProfileViewBasic[]) {
|
||||
if (profiles.length === 0) return
|
||||
for (const profile of profiles) {
|
||||
this.relatedProfiles.set(profile.did, profile)
|
||||
}
|
||||
this.commit()
|
||||
}
|
||||
|
||||
private async setup() {
|
||||
const setupStart = Date.now()
|
||||
logger.debug('setup start', {id: this.id, convoId: this.convoId})
|
||||
try {
|
||||
const {convo} = await this.fetchConvo()
|
||||
|
||||
this.setConvo(convo)
|
||||
|
||||
/*
|
||||
* Some validation prior to `Ready` status
|
||||
*/
|
||||
if (!this.convo) {
|
||||
throw new Error('could not find convo')
|
||||
}
|
||||
|
||||
const self = this.convo.members.find(m => m.did === this.senderUserDid)
|
||||
|
||||
if (!self) {
|
||||
throw new Error('could not find self in convo')
|
||||
}
|
||||
|
||||
const userIsDisabled = Boolean(self.chatDisabled)
|
||||
|
||||
logger.debug('setup complete', {
|
||||
id: this.id,
|
||||
convoId: this.convoId,
|
||||
setupMs: Date.now() - setupStart,
|
||||
userIsDisabled,
|
||||
})
|
||||
|
||||
if (userIsDisabled) {
|
||||
this.dispatch({event: ConvoDispatchEvent.Disable})
|
||||
} else {
|
||||
this.dispatch({event: ConvoDispatchEvent.Ready})
|
||||
}
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
logger.debug('setup failed', {
|
||||
id: this.id,
|
||||
convoId: this.convoId,
|
||||
setupMs: Date.now() - setupStart,
|
||||
message: e.message,
|
||||
})
|
||||
if (!isNetworkError(e) && !isErrorMaybeAppPasswordPermissions(e)) {
|
||||
logger.error('setup failed', {
|
||||
safeMessage: e.message,
|
||||
})
|
||||
}
|
||||
|
||||
this.dispatch({
|
||||
event: ConvoDispatchEvent.Error,
|
||||
payload: {
|
||||
exception: e,
|
||||
code: ConvoErrorCode.InitFailed,
|
||||
retry: () => {
|
||||
this.reset()
|
||||
},
|
||||
},
|
||||
})
|
||||
this.commit()
|
||||
}
|
||||
private setup() {
|
||||
logger.debug('setup', {id: this.id, convoId: this.convoId})
|
||||
this.dispatch({event: ConvoDispatchEvent.Ready})
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -644,102 +505,6 @@ export class Convo {
|
||||
}
|
||||
}
|
||||
|
||||
private pendingFetchConvo:
|
||||
| Promise<{convo: ChatBskyConvoDefs.ConvoView}>
|
||||
| undefined
|
||||
async fetchConvo() {
|
||||
if (this.pendingFetchConvo) return this.pendingFetchConvo
|
||||
|
||||
// non-blocking
|
||||
void this.fetchMemberList()
|
||||
|
||||
const fetchStart = Date.now()
|
||||
logger.debug('fetchConvo start', {id: this.id, convoId: this.convoId})
|
||||
this.pendingFetchConvo = (async () => {
|
||||
try {
|
||||
const response = await networkRetry(2, () => {
|
||||
return this.agent.chat.bsky.convo.getConvo(
|
||||
{convoId: this.convoId},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
})
|
||||
|
||||
const convo = response.data.convo
|
||||
|
||||
logger.debug('fetchConvo done', {
|
||||
id: this.id,
|
||||
convoId: this.convoId,
|
||||
fetchMs: Date.now() - fetchStart,
|
||||
})
|
||||
|
||||
return {
|
||||
convo,
|
||||
}
|
||||
} finally {
|
||||
this.pendingFetchConvo = undefined
|
||||
}
|
||||
})()
|
||||
|
||||
return this.pendingFetchConvo
|
||||
}
|
||||
|
||||
async refreshConvo() {
|
||||
try {
|
||||
void this.fetchMemberList()
|
||||
const {convo} = await this.fetchConvo()
|
||||
// throw new Error('UNCOMMENT TO TEST REFRESH FAILURE')
|
||||
this.setConvo(convo)
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
if (!isNetworkError(e) && !isErrorMaybeAppPasswordPermissions(e)) {
|
||||
logger.error(`failed to refresh convo`, {
|
||||
safeMessage: e.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// purely for populating `this.relatedProfiles` - we do not pipe it
|
||||
// into the ConvoWithDetails. If you want to drive UI based on the member list,
|
||||
// use `useListConvoMembersQuery`
|
||||
// we shouldn't also block loading off of this - the UI should be resilient
|
||||
async fetchMemberList() {
|
||||
const start = Date.now()
|
||||
let cursor: string | undefined
|
||||
let pages = 0
|
||||
let total = 0
|
||||
logger.debug('fetchMemberList start', {
|
||||
id: this.id,
|
||||
convoId: this.convoId,
|
||||
})
|
||||
do {
|
||||
const result = await networkRetry(2, () => {
|
||||
return this.agent.chat.bsky.convo.getConvoMembers(
|
||||
{
|
||||
convoId: this.convoId,
|
||||
limit: 50,
|
||||
cursor,
|
||||
},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
})
|
||||
cursor = result.data.cursor
|
||||
pages++
|
||||
total += result.data.members.length
|
||||
|
||||
for (const member of result.data.members) {
|
||||
this.relatedProfiles.set(member.did, member)
|
||||
}
|
||||
} while (cursor)
|
||||
logger.debug('fetchMemberList done', {
|
||||
id: this.id,
|
||||
convoId: this.convoId,
|
||||
pages,
|
||||
total,
|
||||
fetchMs: Date.now() - start,
|
||||
})
|
||||
}
|
||||
|
||||
private fetchMessageHistoryError: {retry: () => void} | undefined
|
||||
async fetchMessageHistory() {
|
||||
logger.debug('fetchMessageHistory called', {
|
||||
@@ -1017,11 +782,6 @@ export class Convo {
|
||||
id: tempId,
|
||||
message,
|
||||
})
|
||||
if (this.convo?.view.status === 'request') {
|
||||
this.updateConvo({
|
||||
status: 'accepted',
|
||||
})
|
||||
}
|
||||
this.commit()
|
||||
|
||||
if (!this.isProcessingPendingMessages && !this.pendingMessageFailure) {
|
||||
@@ -1029,83 +789,6 @@ export class Convo {
|
||||
}
|
||||
}
|
||||
|
||||
markConvoAccepted() {
|
||||
this.updateConvo({
|
||||
status: 'accepted',
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
updateMuted(muted: boolean) {
|
||||
this.updateConvo({
|
||||
muted,
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
updateGroupName(name: string) {
|
||||
if (this.convo?.kind !== 'group') {
|
||||
throw new Error('updateGroupName can only be called on group convo')
|
||||
}
|
||||
|
||||
this.updateConvo({
|
||||
kind: {
|
||||
...this.convo.details,
|
||||
name,
|
||||
},
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
updateGroupMembers(members: GroupConvoMember[], memberCount: number) {
|
||||
if (this.convo?.kind !== 'group') {
|
||||
throw new Error('updateGroupMembers can only be called on group convo')
|
||||
}
|
||||
|
||||
this.updateConvo({
|
||||
members,
|
||||
kind: {
|
||||
...this.convo.details,
|
||||
memberCount,
|
||||
},
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
updateJoinLink(joinLink: ChatBskyGroupDefs.JoinLinkView | undefined) {
|
||||
if (this.convo?.kind !== 'group') {
|
||||
throw new Error('updateJoinLink can only be called on group convo')
|
||||
}
|
||||
|
||||
this.updateConvo({
|
||||
kind: {
|
||||
...this.convo.details,
|
||||
joinLink,
|
||||
},
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
updateLockStatus(lockStatus: ChatBskyConvoDefs.ConvoLockStatus) {
|
||||
if (this.convo?.kind !== 'group') {
|
||||
throw new Error('updateLockStatus can only be called on group convo')
|
||||
}
|
||||
|
||||
this.updateConvo({
|
||||
kind: {
|
||||
...this.convo.details,
|
||||
lockStatus,
|
||||
},
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
async processPendingMessages() {
|
||||
logger.debug(
|
||||
`processing messages (${this.pendingMessages.size} remaining)`,
|
||||
@@ -1170,14 +853,11 @@ export class Convo {
|
||||
case 'block between recipient and sender':
|
||||
this.emitter.emit('event', {
|
||||
type: 'invalidate-block-state',
|
||||
accountDids: [
|
||||
this.senderUserDid,
|
||||
...this.recipients!.map(r => r.did),
|
||||
],
|
||||
accountDids: [this.senderUserDid, ...this.getRecipientDids()],
|
||||
})
|
||||
break
|
||||
case 'Account is disabled':
|
||||
this.dispatch({event: ConvoDispatchEvent.Disable})
|
||||
this.emitter.emit('event', {type: 'account-disabled'})
|
||||
break
|
||||
case 'Convo not found':
|
||||
case 'Account does not exist':
|
||||
|
||||
@@ -3,10 +3,11 @@ import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from 'react'
|
||||
import {ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
type ConvoParams,
|
||||
type ConvoState,
|
||||
type ConvoStateBackgrounded,
|
||||
type ConvoStateDisabled,
|
||||
type ConvoStateReady,
|
||||
type ConvoStateSuspended,
|
||||
} from '#/state/messages/convo/types'
|
||||
@@ -27,21 +27,12 @@ import {
|
||||
useMarkAsReadMutation,
|
||||
} from '#/state/queries/messages/conversation'
|
||||
import {RQKEY_ROOT as ListConvosQueryKeyRoot} from '#/state/queries/messages/list-conversations'
|
||||
import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members'
|
||||
import {RQKEY as createProfileQueryKey} from '#/state/queries/profile'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {type GroupConvoMember} from '#/components/dms/util'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
|
||||
export * from '#/state/messages/convo/util'
|
||||
|
||||
function membersChanged(
|
||||
a: ChatBskyConvoDefs.ConvoView['members'],
|
||||
b: ChatBskyConvoDefs.ConvoView['members'],
|
||||
) {
|
||||
if (a.length !== b.length) return true
|
||||
const aDids = new Set(a.map(m => m.did))
|
||||
return b.some(m => !aDids.has(m.did))
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ConvoState | null>(null)
|
||||
ChatContext.displayName = 'ChatContext'
|
||||
|
||||
@@ -63,7 +54,6 @@ export function useConvoActive() {
|
||||
| ConvoStateReady
|
||||
| ConvoStateBackgrounded
|
||||
| ConvoStateSuspended
|
||||
| ConvoStateDisabled
|
||||
if (!ctx) {
|
||||
throw new Error('useConvo must be used within a ConvoProvider')
|
||||
}
|
||||
@@ -82,17 +72,27 @@ export function ConvoProvider({
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
const events = useMessagesEventBus()
|
||||
const [convo] = useState(() => {
|
||||
const placeholder = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const getRecipientDids = useCallback(() => {
|
||||
const convo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
getConvoKey(convoId),
|
||||
)
|
||||
return new Convo({
|
||||
convoId,
|
||||
agent,
|
||||
events,
|
||||
placeholderData: placeholder ? {convo: placeholder} : undefined,
|
||||
})
|
||||
})
|
||||
if (!convo) return []
|
||||
return convo.members
|
||||
.filter(m => m.did !== currentAccount?.did)
|
||||
.map(m => m.did)
|
||||
}, [queryClient, convoId, currentAccount?.did])
|
||||
|
||||
const [convo] = useState(
|
||||
() =>
|
||||
new Convo({
|
||||
convoId,
|
||||
agent,
|
||||
events,
|
||||
getRecipientDids,
|
||||
}),
|
||||
)
|
||||
const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot)
|
||||
const {mutate: markAsRead} = useMarkAsReadMutation()
|
||||
|
||||
@@ -103,15 +103,30 @@ export function ConvoProvider({
|
||||
if (isActive) {
|
||||
convo.resume()
|
||||
markAsRead({convoId})
|
||||
// agent no longer owns the convo — invalidate the RQ cache so the
|
||||
// header, member list, and status stay fresh after returning to
|
||||
// the screen.
|
||||
void queryClient.invalidateQueries({queryKey: getConvoKey(convoId)})
|
||||
|
||||
return () => {
|
||||
convo.background()
|
||||
markAsRead({convoId})
|
||||
}
|
||||
}
|
||||
}, [isActive, convo, convoId, markAsRead]),
|
||||
}, [isActive, convo, convoId, markAsRead, queryClient]),
|
||||
)
|
||||
|
||||
// Push member-list data into the agent's `relatedProfiles` Map so that
|
||||
// messages render sender names even when the sender isn't returned in the
|
||||
// per-message `relatedProfiles` payload. The query hook is already
|
||||
// firehose-aware for add/remove-member events.
|
||||
const {data: memberList} = useListConvoMembersQuery({convoId})
|
||||
useEffect(() => {
|
||||
if (memberList) {
|
||||
convo.updateRelatedProfiles(memberList)
|
||||
}
|
||||
}, [memberList, convo])
|
||||
|
||||
useEffect(() => {
|
||||
return convo.on(event => {
|
||||
switch (event.type) {
|
||||
@@ -124,52 +139,44 @@ export function ConvoProvider({
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ListConvosQueryKeyRoot],
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'account-disabled': {
|
||||
// Re-fetch the convo so the UI can surface the disabled state
|
||||
// via `chatDisabled` on the self member.
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: getConvoKey(convoId),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [convo, queryClient])
|
||||
}, [convo, queryClient, convoId])
|
||||
|
||||
useEffect(() => {
|
||||
const [root, id] = getConvoKey(convoId)
|
||||
return queryClient.getQueryCache().subscribe(event => {
|
||||
const queryKey = event.query.queryKey as string[]
|
||||
if (queryKey[0] === root && queryKey[1] === id) {
|
||||
const data = event.query.state.data as
|
||||
| ChatBskyConvoDefs.ConvoView
|
||||
| undefined
|
||||
if (data && convo.convo && data.muted !== convo.convo.view.muted) {
|
||||
convo.updateMuted(data.muted)
|
||||
}
|
||||
if (
|
||||
data &&
|
||||
ChatBskyConvoDefs.isGroupConvo(data.kind) &&
|
||||
convo.convo?.kind === 'group'
|
||||
) {
|
||||
if (data.kind.name !== convo.convo.details.name) {
|
||||
convo.updateGroupName(data.kind.name)
|
||||
}
|
||||
if (data.kind.joinLink !== convo.convo.details.joinLink) {
|
||||
convo.updateJoinLink(data.kind.joinLink)
|
||||
}
|
||||
if (data.kind.lockStatus !== convo.convo.details.lockStatus) {
|
||||
convo.updateLockStatus(data.kind.lockStatus)
|
||||
}
|
||||
}
|
||||
if (
|
||||
data &&
|
||||
ChatBskyConvoDefs.isGroupConvo(data.kind) &&
|
||||
convo.convo?.kind === 'group' &&
|
||||
(membersChanged(data.members, convo.convo.members) ||
|
||||
data.kind.memberCount !== convo.convo.details.memberCount)
|
||||
) {
|
||||
convo.updateGroupMembers(
|
||||
data.members as GroupConvoMember[],
|
||||
data.kind.memberCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [convo, convoId, queryClient])
|
||||
// Auto-accept: when the user sends in a request-status convo, optimistically
|
||||
// flip the cached status to 'accepted' so UI updates immediately. The server
|
||||
// accepts on first send.
|
||||
const wrappedService = useMemo<ConvoState>(() => {
|
||||
if (!isConvoActive(service)) return service
|
||||
const originalSend = service.sendMessage
|
||||
return {
|
||||
...service,
|
||||
sendMessage: message => {
|
||||
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
getConvoKey(convoId),
|
||||
old => {
|
||||
if (!old || old.status !== 'request') return old
|
||||
return {...old, status: 'accepted'}
|
||||
},
|
||||
)
|
||||
originalSend(message)
|
||||
},
|
||||
}
|
||||
}, [service, queryClient, convoId])
|
||||
|
||||
return <ChatContext.Provider value={service}>{children}</ChatContext.Provider>
|
||||
return (
|
||||
<ChatContext.Provider value={wrappedService}>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,15 +6,18 @@ import {
|
||||
} from '@atproto/api'
|
||||
|
||||
import {type MessagesEventBus} from '#/state/messages/events/agent'
|
||||
import {type ConvoWithDetails} from '#/components/dms/util'
|
||||
|
||||
export type ConvoParams = {
|
||||
convoId: string
|
||||
agent: BskyAgent
|
||||
events: MessagesEventBus
|
||||
placeholderData?: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
}
|
||||
/**
|
||||
* Returns the DIDs of the current user's conversation partners (every
|
||||
* member other than self). Called lazily when the agent needs to invalidate
|
||||
* block state after a send failure. Source of truth is the `useConvoQuery`
|
||||
* cache — see `ConvoProvider`.
|
||||
*/
|
||||
getRecipientDids: () => string[]
|
||||
}
|
||||
|
||||
export enum ConvoStatus {
|
||||
@@ -108,42 +111,36 @@ type SendMessage = (
|
||||
message: ChatBskyConvoSendMessage.InputSchema['message'],
|
||||
) => void
|
||||
type FetchMessageHistory = () => Promise<void>
|
||||
type MarkConvoAccepted = () => void
|
||||
type AddReaction = (messageId: string, reaction: string) => Promise<void>
|
||||
type RemoveReaction = (messageId: string, reaction: string) => Promise<void>
|
||||
|
||||
export type ConvoStateUninitialized = {
|
||||
status: ConvoStatus.Uninitialized
|
||||
items: []
|
||||
convo: ConvoWithDetails | undefined
|
||||
error: undefined
|
||||
isFetchingHistory: false
|
||||
hasAllHistory: boolean
|
||||
deleteMessage: undefined
|
||||
sendMessage: undefined
|
||||
fetchMessageHistory: undefined
|
||||
markConvoAccepted: undefined
|
||||
addReaction: undefined
|
||||
removeReaction: undefined
|
||||
}
|
||||
export type ConvoStateInitializing = {
|
||||
status: ConvoStatus.Initializing
|
||||
items: []
|
||||
convo: ConvoWithDetails | undefined
|
||||
error: undefined
|
||||
isFetchingHistory: boolean
|
||||
hasAllHistory: boolean
|
||||
deleteMessage: undefined
|
||||
sendMessage: undefined
|
||||
fetchMessageHistory: undefined
|
||||
markConvoAccepted: undefined
|
||||
addReaction: undefined
|
||||
removeReaction: undefined
|
||||
}
|
||||
export type ConvoStateReady = {
|
||||
status: ConvoStatus.Ready
|
||||
items: ConvoItem[]
|
||||
convo: ConvoWithDetails
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||
error: undefined
|
||||
isFetchingHistory: boolean
|
||||
@@ -151,14 +148,12 @@ export type ConvoStateReady = {
|
||||
deleteMessage: DeleteMessage
|
||||
sendMessage: SendMessage
|
||||
fetchMessageHistory: FetchMessageHistory
|
||||
markConvoAccepted: MarkConvoAccepted
|
||||
addReaction: AddReaction
|
||||
removeReaction: RemoveReaction
|
||||
}
|
||||
export type ConvoStateBackgrounded = {
|
||||
status: ConvoStatus.Backgrounded
|
||||
items: ConvoItem[]
|
||||
convo: ConvoWithDetails
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||
error: undefined
|
||||
isFetchingHistory: boolean
|
||||
@@ -166,14 +161,12 @@ export type ConvoStateBackgrounded = {
|
||||
deleteMessage: DeleteMessage
|
||||
sendMessage: SendMessage
|
||||
fetchMessageHistory: FetchMessageHistory
|
||||
markConvoAccepted: MarkConvoAccepted
|
||||
addReaction: AddReaction
|
||||
removeReaction: RemoveReaction
|
||||
}
|
||||
export type ConvoStateSuspended = {
|
||||
status: ConvoStatus.Suspended
|
||||
items: ConvoItem[]
|
||||
convo: ConvoWithDetails
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||
error: undefined
|
||||
isFetchingHistory: boolean
|
||||
@@ -181,28 +174,24 @@ export type ConvoStateSuspended = {
|
||||
deleteMessage: DeleteMessage
|
||||
sendMessage: SendMessage
|
||||
fetchMessageHistory: FetchMessageHistory
|
||||
markConvoAccepted: MarkConvoAccepted
|
||||
addReaction: AddReaction
|
||||
removeReaction: RemoveReaction
|
||||
}
|
||||
export type ConvoStateError = {
|
||||
status: ConvoStatus.Error
|
||||
items: []
|
||||
convo: undefined
|
||||
error: ConvoError
|
||||
isFetchingHistory: false
|
||||
hasAllHistory: false
|
||||
deleteMessage: undefined
|
||||
sendMessage: undefined
|
||||
fetchMessageHistory: undefined
|
||||
markConvoAccepted: undefined
|
||||
addReaction: undefined
|
||||
removeReaction: undefined
|
||||
}
|
||||
export type ConvoStateDisabled = {
|
||||
status: ConvoStatus.Disabled
|
||||
items: ConvoItem[]
|
||||
convo: ConvoWithDetails
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||
error: undefined
|
||||
isFetchingHistory: boolean
|
||||
@@ -210,7 +199,6 @@ export type ConvoStateDisabled = {
|
||||
deleteMessage: DeleteMessage
|
||||
sendMessage: SendMessage
|
||||
fetchMessageHistory: FetchMessageHistory
|
||||
markConvoAccepted: MarkConvoAccepted
|
||||
addReaction: AddReaction
|
||||
removeReaction: RemoveReaction
|
||||
}
|
||||
@@ -223,7 +211,11 @@ export type ConvoState =
|
||||
| ConvoStateError
|
||||
| ConvoStateDisabled
|
||||
|
||||
export type ConvoEvent = {
|
||||
type: 'invalidate-block-state'
|
||||
accountDids: string[]
|
||||
}
|
||||
export type ConvoEvent =
|
||||
| {
|
||||
type: 'invalidate-block-state'
|
||||
accountDids: string[]
|
||||
}
|
||||
| {
|
||||
type: 'account-disabled'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
type ConvoState,
|
||||
type ConvoStateBackgrounded,
|
||||
type ConvoStateDisabled,
|
||||
type ConvoStateReady,
|
||||
type ConvoStateSuspended,
|
||||
ConvoStatus,
|
||||
@@ -15,7 +14,6 @@ export type ActiveConvoStates =
|
||||
| ConvoStateReady
|
||||
| ConvoStateBackgrounded
|
||||
| ConvoStateSuspended
|
||||
| ConvoStateDisabled
|
||||
|
||||
/**
|
||||
* Checks if a `Convo` has a `status` that is "active", meaning the chat is
|
||||
@@ -26,7 +24,6 @@ export function isConvoActive(convo: ConvoState): convo is ActiveConvoStates {
|
||||
return (
|
||||
convo.status === ConvoStatus.Ready ||
|
||||
convo.status === ConvoStatus.Backgrounded ||
|
||||
convo.status === ConvoStatus.Suspended ||
|
||||
convo.status === ConvoStatus.Disabled
|
||||
convo.status === ConvoStatus.Suspended
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
type ChatBskyConvoAcceptConvo,
|
||||
type ChatBskyConvoDefs,
|
||||
type ChatBskyConvoListConvos,
|
||||
} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
@@ -7,6 +8,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {RQKEY as CONVO_KEY} from './conversation'
|
||||
import {
|
||||
RQKEY as CONVO_LIST_KEY,
|
||||
RQKEY_ROOT as CONVO_LIST_ROOT_KEY,
|
||||
@@ -39,9 +41,18 @@ export function useAcceptConversation(
|
||||
onMutate: () => {
|
||||
let prevAcceptedPages: ChatBskyConvoListConvos.OutputSchema[] = []
|
||||
let prevInboxPages: ChatBskyConvoListConvos.OutputSchema[] = []
|
||||
let prevConvo: ChatBskyConvoDefs.ConvoView | undefined
|
||||
let convoBeingAccepted:
|
||||
| ChatBskyConvoListConvos.OutputSchema['convos'][number]
|
||||
| undefined
|
||||
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
old => {
|
||||
if (!old) return old
|
||||
prevConvo = old
|
||||
return {...old, status: 'accepted'}
|
||||
},
|
||||
)
|
||||
queryClient.setQueryData(
|
||||
CONVO_LIST_KEY('request'),
|
||||
(old?: {
|
||||
@@ -97,7 +108,7 @@ export function useAcceptConversation(
|
||||
},
|
||||
)
|
||||
onMutate?.()
|
||||
return {prevAcceptedPages, prevInboxPages}
|
||||
return {prevAcceptedPages, prevInboxPages, prevConvo}
|
||||
},
|
||||
onSuccess: data => {
|
||||
queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
||||
@@ -105,6 +116,9 @@ export function useAcceptConversation(
|
||||
},
|
||||
onError: (error, _, context) => {
|
||||
logger.error(error)
|
||||
if (context?.prevConvo) {
|
||||
queryClient.setQueryData(CONVO_KEY(convoId), context.prevConvo)
|
||||
}
|
||||
queryClient.setQueryData(
|
||||
CONVO_LIST_KEY('accepted'),
|
||||
(old?: {
|
||||
|
||||
Reference in New Issue
Block a user