Compare commits

...

2 Commits

Author SHA1 Message Date
Samuel Newman 03c6bd6f16 new chat badge 2025-01-07 19:47:41 +00:00
Samuel Newman fa34d92da0 move mark as read to convo state 2025-01-07 19:22:06 +00:00
7 changed files with 199 additions and 40 deletions
@@ -0,0 +1,75 @@
import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
import {Text} from '#/components/Typography'
export function MessagesListPendingAcceptFooter({
// recipient: initialRecipient,
convoId,
hasMessages,
}: {
recipient: AppBskyActorDefs.ProfileViewBasic
convoId: string
hasMessages: boolean
}) {
const t = useTheme()
const {_} = useLingui()
// const recipient = useProfileShadow(initialRecipient)
const leaveConvoControl = useDialogControl()
const reportControl = useDialogControl()
return (
<View style={[hasMessages && a.pt_md, a.pb_xl, a.gap_lg]}>
<Divider />
<Text style={[a.text_md, a.font_bold, a.text_center]}>
{/* {isBlocking ? (
<Trans>You have blocked this user</Trans>
) : (
<Trans>This user has blocked you</Trans>
)} */}
</Text>
<View style={[a.flex_row, a.justify_between, a.gap_lg, a.px_md]}>
<Button
label={_(msg`Leave chat`)}
color="secondary"
variant="solid"
size="small"
style={[a.flex_1]}
onPress={leaveConvoControl.open}>
<ButtonText style={{color: t.palette.negative_500}}>
<Trans>Leave chat</Trans>
</ButtonText>
</Button>
<Button
label={_(msg`Report`)}
color="secondary"
variant="solid"
size="small"
style={[a.flex_1]}
onPress={reportControl.open}>
<ButtonText style={{color: t.palette.negative_500}}>
<Trans>Report</Trans>
</ButtonText>
</Button>
</View>
<LeaveConvoPrompt
control={leaveConvoControl}
currentScreen="conversation"
convoId={convoId}
/>
<ReportConversationPrompt control={reportControl} />
</View>
)
}
+18 -7
View File
@@ -23,9 +23,11 @@ import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter'
import {MessagesListHeader} from '#/components/dms/MessagesListHeader'
import {MessagesListPendingAcceptFooter} from '#/components/dms/MessagesListPendingAcceptFooter'
import {Error} from '#/components/Error'
import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
import {ChatDisabled} from './components/ChatDisabled'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
@@ -193,14 +195,23 @@ function InnerReady({
<MessagesList
hasScrolled={hasScrolled}
setHasScrolled={setHasScrolled}
blocked={moderation?.blocked}
footer={
<MessagesListBlockedFooter
recipient={recipient}
convoId={convoState.convo.id}
hasMessages={convoState.items.length > 0}
blockInfo={blockInfo}
/>
convoState.status === ConvoStatus.Disabled ? (
<ChatDisabled />
) : moderation?.blocked ? (
<MessagesListBlockedFooter
recipient={recipient}
convoId={convoState.convo.id}
hasMessages={convoState.items.length > 0}
blockInfo={blockInfo}
/>
) : convoState.pendingAcceptance ? (
<MessagesListPendingAcceptFooter
recipient={recipient}
convoId={convoState.convo.id}
hasMessages={convoState.items.length > 0}
/>
) : null
}
/>
)}
@@ -7,7 +7,7 @@ import {
moderateProfile,
ModerationOpts,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {GestureActionView} from '#/lib/custom-animations/GestureActionView'
@@ -27,7 +27,7 @@ import {useMarkAsReadMutation} from '#/state/queries/messages/conversation'
import {useSession} from '#/state/session'
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
import * as tokens from '#/alf/tokens'
import {useDialogControl} from '#/components/Dialog'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
@@ -294,7 +294,14 @@ function ChatListItemReady({
<View
style={[a.flex_1, a.justify_center, web({paddingRight: 45})]}>
<View style={[a.w_full, a.flex_row, a.align_end, a.pb_2xs]}>
<View
style={[
a.w_full,
a.flex_row,
a.align_end,
a.pb_2xs,
convo.opened === false && a.pr_4xl,
]}>
<Text
numberOfLines={1}
style={[{maxWidth: '85%'}, web([a.leading_normal])]}>
@@ -373,22 +380,50 @@ function ChatListItemReady({
/>
</View>
{convo.unreadCount > 0 && (
{convo.opened === false &&
(isNative || !(showActions || menuControl.isOpen)) ? (
<View
style={[
a.absolute,
a.rounded_full,
a.rounded_sm,
a.px_sm,
native(a.py_2xs),
{
backgroundColor: isDimStyle
? t.palette.contrast_200
: t.palette.primary_500,
height: 7,
width: 7,
top: 15,
right: 12,
top: 12,
right: 10,
},
]}
/>
]}>
<Text
style={[
{color: 'white'},
a.text_xs,
a.font_heavy,
web(a.leading_normal),
]}>
<Trans>NEW</Trans>
</Text>
</View>
) : (
convo.unreadCount > 0 && (
<View
style={[
a.absolute,
a.rounded_full,
{
backgroundColor: isDimStyle
? t.palette.contrast_200
: t.palette.primary_500,
height: 7,
width: 7,
top: 15,
right: 12,
},
]}
/>
)
)}
</View>
)}
@@ -31,7 +31,6 @@ import {
EmojiPickerState,
} from '#/view/com/composer/text-input/web/EmojiPicker.web'
import {List, ListMethods} from '#/view/com/util/List'
import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled'
import {MessageInput} from '#/screens/Messages/components/MessageInput'
import {MessageListError} from '#/screens/Messages/components/MessageListError'
import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
@@ -78,13 +77,11 @@ function onScrollToIndexFailed() {
export function MessagesList({
hasScrolled,
setHasScrolled,
blocked,
footer,
}: {
hasScrolled: boolean
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
blocked?: boolean
footer?: React.ReactNode
footer: React.ReactNode | null
}) {
const convoState = useConvoActive()
const agent = useAgent()
@@ -427,11 +424,7 @@ export function MessagesList({
/>
</ScrollProvider>
<Animated.View style={animatedStickyViewStyle}>
{convoState.status === ConvoStatus.Disabled ? (
<ChatDisabled />
) : blocked ? (
footer
) : (
{footer || (
<>
{isConvoActive(convoState) &&
!convoState.isFetchingHistory &&
+39 -7
View File
@@ -78,10 +78,13 @@ export class Convo {
private emitter = new EventEmitter<{event: [ConvoEvent]}>()
private onMarkAsRead: () => void
convoId: string
convo: ChatBskyConvoDefs.ConvoView | undefined
sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined = undefined
pendingAcceptance: boolean | undefined
snapshot: ConvoState | undefined
constructor(params: ConvoParams) {
@@ -89,7 +92,8 @@ export class Convo {
this.convoId = params.convoId
this.agent = params.agent
this.events = params.events
this.senderUserDid = params.agent.session?.did!
this.senderUserDid = params.agent.session!.did
this.onMarkAsRead = params.onMarkAsRead
this.subscribe = this.subscribe.bind(this)
this.getSnapshot = this.getSnapshot.bind(this)
@@ -139,6 +143,7 @@ export class Convo {
deleteMessage: undefined,
sendMessage: undefined,
fetchMessageHistory: undefined,
pendingAcceptance: undefined,
}
}
case ConvoStatus.Disabled:
@@ -156,6 +161,7 @@ export class Convo {
deleteMessage: this.deleteMessage,
sendMessage: this.sendMessage,
fetchMessageHistory: this.fetchMessageHistory,
pendingAcceptance: this.pendingAcceptance,
}
}
case ConvoStatus.Error: {
@@ -170,6 +176,7 @@ export class Convo {
deleteMessage: undefined,
sendMessage: undefined,
fetchMessageHistory: undefined,
pendingAcceptance: undefined,
}
}
default: {
@@ -184,6 +191,7 @@ export class Convo {
deleteMessage: undefined,
sendMessage: undefined,
fetchMessageHistory: undefined,
pendingAcceptance: undefined,
}
}
}
@@ -246,11 +254,13 @@ export class Convo {
case ConvoDispatchEvent.Resume: {
this.refreshConvo()
this.requestPollInterval(ACTIVE_POLL_INTERVAL)
this.markAsRead()
break
}
case ConvoDispatchEvent.Background: {
this.status = ConvoStatus.Backgrounded
this.requestPollInterval(BACKGROUND_POLL_INTERVAL)
this.markAsRead()
break
}
case ConvoDispatchEvent.Suspend: {
@@ -513,6 +523,7 @@ export class Convo {
convo: ChatBskyConvoDefs.ConvoView
sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[]
pendingAcceptance: boolean
}>
| undefined
async fetchConvo() {
@@ -522,10 +533,11 @@ export class Convo {
convo: ChatBskyConvoDefs.ConvoView
sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[]
pendingAcceptance: boolean
}>(async (resolve, reject) => {
try {
const response = await networkRetry(2, () => {
return this.agent.api.chat.bsky.convo.getConvo(
return this.agent.chat.bsky.convo.getConvo(
{
convoId: this.convoId,
},
@@ -539,6 +551,7 @@ export class Convo {
convo,
sender: convo.members.find(m => m.did === this.senderUserDid),
recipients: convo.members.filter(m => m.did !== this.senderUserDid),
pendingAcceptance: !convo.opened,
})
} catch (e) {
reject(e)
@@ -552,11 +565,13 @@ export class Convo {
async refreshConvo() {
try {
const {convo, sender, recipients} = await this.fetchConvo()
const {convo, sender, recipients, pendingAcceptance} =
await this.fetchConvo()
// throw new Error('UNCOMMENT TO TEST REFRESH FAILURE')
this.convo = convo || this.convo
this.sender = sender || this.sender
this.recipients = recipients || this.recipients
this.pendingAcceptance = pendingAcceptance ?? this.pendingAcceptance
} catch (e: any) {
logger.error(e, {context: `Convo: failed to refresh convo`})
}
@@ -592,7 +607,7 @@ export class Convo {
const nextCursor = this.oldestRev // for TS
const response = await networkRetry(2, () => {
return this.agent.api.chat.bsky.convo.getMessages(
return this.agent.chat.bsky.convo.getMessages(
{
cursor: nextCursor,
convoId: this.convoId,
@@ -793,7 +808,7 @@ export class Convo {
const {id, message} = pendingMessage
const response = await this.agent.api.chat.bsky.convo.sendMessage(
const response = await this.agent.chat.bsky.convo.sendMessage(
{
convoId: this.convoId,
message,
@@ -888,7 +903,7 @@ export class Convo {
)
try {
const {data} = await this.agent.api.chat.bsky.convo.sendMessageBatch(
const {data} = await this.agent.chat.bsky.convo.sendMessageBatch(
{
items: messageArray.map(({message}) => ({
convoId: this.convoId,
@@ -935,7 +950,7 @@ export class Convo {
try {
await networkRetry(2, () => {
return this.agent.api.chat.bsky.convo.deleteMessageForSelf(
return this.agent.chat.bsky.convo.deleteMessageForSelf(
{
convoId: this.convoId,
messageId,
@@ -1104,4 +1119,21 @@ export class Convo {
return item
})
}
async acceptChat() {
await this.agent.chat.bsky.convo.updateRead({
convoId: this.convoId,
})
this.pendingAcceptance = false
this.onMarkAsRead()
}
markAsRead() {
if (!this.pendingAcceptance) {
this.agent.chat.bsky.convo.updateRead({
convoId: this.convoId,
})
this.onMarkAsRead()
}
}
}
+11 -6
View File
@@ -3,6 +3,7 @@ import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useAppState} from '#/lib/hooks/useAppState'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {Convo} from '#/state/messages/convo/agent'
import {
ConvoParams,
@@ -14,8 +15,10 @@ import {
} from '#/state/messages/convo/types'
import {isConvoActive} from '#/state/messages/convo/util'
import {useMessagesEventBus} from '#/state/messages/events'
import {useMarkAsReadMutation} from '#/state/queries/messages/conversation'
import {RQKEY as ListConvosQueryKey} from '#/state/queries/messages/list-conversations'
import {
RQKEY as ListConvosQueryKey,
useOnMarkAsRead,
} from '#/state/queries/messages/list-conversations'
import {RQKEY as createProfileQueryKey} from '#/state/queries/profile'
import {useAgent} from '#/state/session'
@@ -60,16 +63,20 @@ export function ConvoProvider({
const queryClient = useQueryClient()
const agent = useAgent()
const events = useMessagesEventBus()
const onMarkAsRead = useOnMarkAsRead()
const stableMAR = useNonReactiveCallback(() => {
onMarkAsRead(convoId)
})
const [convo] = useState(
() =>
new Convo({
convoId,
agent,
events,
onMarkAsRead: stableMAR,
}),
)
const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot)
const {mutate: markAsRead} = useMarkAsReadMutation()
const appState = useAppState()
const isActive = appState === 'active'
@@ -77,14 +84,12 @@ export function ConvoProvider({
React.useCallback(() => {
if (isActive) {
convo.resume()
markAsRead({convoId})
return () => {
convo.background()
markAsRead({convoId})
}
}
}, [isActive, convo, convoId, markAsRead]),
}, [isActive, convo]),
)
React.useEffect(() => {
+8
View File
@@ -11,6 +11,7 @@ export type ConvoParams = {
convoId: string
agent: BskyAgent
events: MessagesEventBus
onMarkAsRead: () => void
}
export enum ConvoStatus {
@@ -150,6 +151,7 @@ export type ConvoStateUninitialized = {
deleteMessage: undefined
sendMessage: undefined
fetchMessageHistory: undefined
pendingAcceptance: undefined
}
export type ConvoStateInitializing = {
status: ConvoStatus.Initializing
@@ -162,6 +164,7 @@ export type ConvoStateInitializing = {
deleteMessage: undefined
sendMessage: undefined
fetchMessageHistory: undefined
pendingAcceptance: undefined
}
export type ConvoStateReady = {
status: ConvoStatus.Ready
@@ -174,6 +177,7 @@ export type ConvoStateReady = {
deleteMessage: DeleteMessage
sendMessage: SendMessage
fetchMessageHistory: FetchMessageHistory
pendingAcceptance: boolean | undefined
}
export type ConvoStateBackgrounded = {
status: ConvoStatus.Backgrounded
@@ -186,6 +190,7 @@ export type ConvoStateBackgrounded = {
deleteMessage: DeleteMessage
sendMessage: SendMessage
fetchMessageHistory: FetchMessageHistory
pendingAcceptance: boolean | undefined
}
export type ConvoStateSuspended = {
status: ConvoStatus.Suspended
@@ -198,6 +203,7 @@ export type ConvoStateSuspended = {
deleteMessage: DeleteMessage
sendMessage: SendMessage
fetchMessageHistory: FetchMessageHistory
pendingAcceptance: boolean | undefined
}
export type ConvoStateError = {
status: ConvoStatus.Error
@@ -210,6 +216,7 @@ export type ConvoStateError = {
deleteMessage: undefined
sendMessage: undefined
fetchMessageHistory: undefined
pendingAcceptance: undefined
}
export type ConvoStateDisabled = {
status: ConvoStatus.Disabled
@@ -222,6 +229,7 @@ export type ConvoStateDisabled = {
deleteMessage: DeleteMessage
sendMessage: SendMessage
fetchMessageHistory: FetchMessageHistory
pendingAcceptance: boolean | undefined
}
export type ConvoState =
| ConvoStateUninitialized