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