[🐴] Better retry styling (#4032)
* Pass whole object to MessageItem for clarity * Add retry to pending-message * Style send failure, retry * Group pending messages * Remove todos * Fix types with fake messagezio/stable
parent
ed8922281a
commit
04aea93192
|
@ -1,40 +1,44 @@
|
||||||
import React, {useCallback, useMemo, useRef} from 'react'
|
import React, {useCallback, useMemo, useRef} from 'react'
|
||||||
import {LayoutAnimation, StyleProp, TextStyle, View} from 'react-native'
|
import {
|
||||||
|
GestureResponderEvent,
|
||||||
|
LayoutAnimation,
|
||||||
|
StyleProp,
|
||||||
|
TextStyle,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
import {ChatBskyConvoDefs, RichText as RichTextAPI} from '@atproto/api'
|
import {ChatBskyConvoDefs, RichText as RichTextAPI} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {ConvoItem} from '#/state/messages/convo/types'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {TimeElapsed} from 'view/com/util/TimeElapsed'
|
import {TimeElapsed} from 'view/com/util/TimeElapsed'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
|
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
|
||||||
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {RichText} from '../RichText'
|
import {RichText} from '../RichText'
|
||||||
|
|
||||||
let MessageItem = ({
|
let MessageItem = ({
|
||||||
item,
|
item,
|
||||||
next,
|
|
||||||
pending,
|
|
||||||
}: {
|
}: {
|
||||||
item: ChatBskyConvoDefs.MessageView
|
item: ConvoItem & {type: 'message' | 'pending-message'}
|
||||||
next:
|
|
||||||
| ChatBskyConvoDefs.MessageView
|
|
||||||
| ChatBskyConvoDefs.DeletedMessageView
|
|
||||||
| null
|
|
||||||
pending?: boolean
|
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
const isFromSelf = item.sender?.did === currentAccount?.did
|
const {message, nextMessage} = item
|
||||||
|
const isPending = item.type === 'pending-message'
|
||||||
|
|
||||||
|
const isFromSelf = message.sender?.did === currentAccount?.did
|
||||||
|
|
||||||
const isNextFromSelf =
|
const isNextFromSelf =
|
||||||
ChatBskyConvoDefs.isMessageView(next) &&
|
ChatBskyConvoDefs.isMessageView(nextMessage) &&
|
||||||
next.sender?.did === currentAccount?.did
|
nextMessage.sender?.did === currentAccount?.did
|
||||||
|
|
||||||
const isLastInGroup = useMemo(() => {
|
const isLastInGroup = useMemo(() => {
|
||||||
// TODO this means it's a placeholder. Let's figure out the right way to do this though!
|
// if this message is pending, it means the next message is pending too
|
||||||
if (item.id.length > 13) {
|
if (isPending && nextMessage) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,9 +48,9 @@ let MessageItem = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// or, if there's a 3 minute gap between this message and the next
|
// or, if there's a 3 minute gap between this message and the next
|
||||||
if (ChatBskyConvoDefs.isMessageView(next)) {
|
if (ChatBskyConvoDefs.isMessageView(nextMessage)) {
|
||||||
const thisDate = new Date(item.sentAt)
|
const thisDate = new Date(message.sentAt)
|
||||||
const nextDate = new Date(next.sentAt)
|
const nextDate = new Date(nextMessage.sentAt)
|
||||||
|
|
||||||
const diff = nextDate.getTime() - thisDate.getTime()
|
const diff = nextDate.getTime() - thisDate.getTime()
|
||||||
|
|
||||||
|
@ -55,7 +59,7 @@ let MessageItem = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}, [item, next, isFromSelf, isNextFromSelf])
|
}, [message, nextMessage, isFromSelf, isNextFromSelf, isPending])
|
||||||
|
|
||||||
const lastInGroupRef = useRef(isLastInGroup)
|
const lastInGroupRef = useRef(isLastInGroup)
|
||||||
if (lastInGroupRef.current !== isLastInGroup) {
|
if (lastInGroupRef.current !== isLastInGroup) {
|
||||||
|
@ -67,12 +71,12 @@ let MessageItem = ({
|
||||||
t.name === 'light' ? t.palette.primary_200 : t.palette.primary_800
|
t.name === 'light' ? t.palette.primary_200 : t.palette.primary_800
|
||||||
|
|
||||||
const rt = useMemo(() => {
|
const rt = useMemo(() => {
|
||||||
return new RichTextAPI({text: item.text, facets: item.facets})
|
return new RichTextAPI({text: message.text, facets: message.facets})
|
||||||
}, [item.text, item.facets])
|
}, [message.text, message.facets])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ActionsWrapper isFromSelf={isFromSelf} message={item}>
|
<ActionsWrapper isFromSelf={isFromSelf} message={message}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.py_sm,
|
a.py_sm,
|
||||||
|
@ -82,7 +86,7 @@ let MessageItem = ({
|
||||||
paddingLeft: 14,
|
paddingLeft: 14,
|
||||||
paddingRight: 14,
|
paddingRight: 14,
|
||||||
backgroundColor: isFromSelf
|
backgroundColor: isFromSelf
|
||||||
? pending
|
? isPending
|
||||||
? pendingColor
|
? pendingColor
|
||||||
: t.palette.primary_500
|
: t.palette.primary_500
|
||||||
: t.palette.contrast_50,
|
: t.palette.contrast_50,
|
||||||
|
@ -98,18 +102,20 @@ let MessageItem = ({
|
||||||
a.text_md,
|
a.text_md,
|
||||||
a.leading_snug,
|
a.leading_snug,
|
||||||
isFromSelf && {color: t.palette.white},
|
isFromSelf && {color: t.palette.white},
|
||||||
pending && t.name !== 'light' && {color: t.palette.primary_300},
|
isPending && t.name !== 'light' && {color: t.palette.primary_300},
|
||||||
]}
|
]}
|
||||||
interactiveStyle={a.underline}
|
interactiveStyle={a.underline}
|
||||||
enableTags
|
enableTags
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ActionsWrapper>
|
</ActionsWrapper>
|
||||||
|
|
||||||
|
{isLastInGroup && (
|
||||||
<MessageItemMetadata
|
<MessageItemMetadata
|
||||||
message={item}
|
item={item}
|
||||||
isLastInGroup={isLastInGroup}
|
|
||||||
style={isFromSelf ? a.text_right : a.text_left}
|
style={isFromSelf ? a.text_right : a.text_left}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -117,16 +123,26 @@ MessageItem = React.memo(MessageItem)
|
||||||
export {MessageItem}
|
export {MessageItem}
|
||||||
|
|
||||||
let MessageItemMetadata = ({
|
let MessageItemMetadata = ({
|
||||||
message,
|
item,
|
||||||
isLastInGroup,
|
|
||||||
style,
|
style,
|
||||||
}: {
|
}: {
|
||||||
message: ChatBskyConvoDefs.MessageView
|
item: ConvoItem & {type: 'message' | 'pending-message'}
|
||||||
isLastInGroup: boolean
|
|
||||||
style: StyleProp<TextStyle>
|
style: StyleProp<TextStyle>
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const {message} = item
|
||||||
|
|
||||||
|
const handleRetry = useCallback(
|
||||||
|
(e: GestureResponderEvent) => {
|
||||||
|
if (item.type === 'pending-message' && item.retry) {
|
||||||
|
e.preventDefault()
|
||||||
|
item.retry()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[item],
|
||||||
|
)
|
||||||
|
|
||||||
const relativeTimestamp = useCallback(
|
const relativeTimestamp = useCallback(
|
||||||
(timestamp: string) => {
|
(timestamp: string) => {
|
||||||
|
@ -169,25 +185,47 @@ let MessageItemMetadata = ({
|
||||||
[_],
|
[_],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isLastInGroup) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimeElapsed timestamp={message.sentAt} timeToString={relativeTimestamp}>
|
|
||||||
{({timeElapsed}) => (
|
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
a.text_xs,
|
a.text_xs,
|
||||||
a.mt_2xs,
|
a.mt_2xs,
|
||||||
a.mb_lg,
|
a.mb_lg,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
style,
|
style,
|
||||||
]}>
|
]}>
|
||||||
|
<TimeElapsed timestamp={message.sentAt} timeToString={relativeTimestamp}>
|
||||||
|
{({timeElapsed}) => (
|
||||||
|
<Text style={[a.text_xs, t.atoms.text_contrast_medium]}>
|
||||||
{timeElapsed}
|
{timeElapsed}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</TimeElapsed>
|
</TimeElapsed>
|
||||||
|
|
||||||
|
{item.type === 'pending-message' && item.retry && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
·{' '}
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_xs,
|
||||||
|
{
|
||||||
|
color: t.palette.negative_400,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{_(msg`Failed to send`)}
|
||||||
|
</Text>{' '}
|
||||||
|
·{' '}
|
||||||
|
<InlineLinkText
|
||||||
|
label={_(msg`Click to retry failed message`)}
|
||||||
|
to="#"
|
||||||
|
onPress={handleRetry}
|
||||||
|
style={[a.text_xs]}>
|
||||||
|
{_(msg`Retry`)}
|
||||||
|
</InlineLinkText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -245,8 +245,12 @@ function PreviewMessage({message}: {message: ChatBskyConvoDefs.MessageView}) {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<MessageItemMetadata
|
<MessageItemMetadata
|
||||||
message={message}
|
item={{
|
||||||
isLastInGroup
|
type: 'message',
|
||||||
|
message,
|
||||||
|
key: '',
|
||||||
|
nextMessage: null,
|
||||||
|
}}
|
||||||
style={[a.text_left, a.mb_0]}
|
style={[a.text_left, a.mb_0]}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -26,7 +26,6 @@ export function MessageListError({
|
||||||
msg`This chat was disconnected due to a network error.`,
|
msg`This chat was disconnected due to a network error.`,
|
||||||
),
|
),
|
||||||
[ConvoItemError.HistoryFailed]: _(msg`Failed to load past messages.`),
|
[ConvoItemError.HistoryFailed]: _(msg`Failed to load past messages.`),
|
||||||
[ConvoItemError.PendingFailed]: _(msg`Failed to send message(s).`),
|
|
||||||
}[item.code]
|
}[item.code]
|
||||||
}, [_, item.code])
|
}, [_, item.code])
|
||||||
|
|
||||||
|
|
|
@ -35,13 +35,7 @@ function MaybeLoader({isLoading}: {isLoading: boolean}) {
|
||||||
|
|
||||||
function renderItem({item}: {item: ConvoItem}) {
|
function renderItem({item}: {item: ConvoItem}) {
|
||||||
if (item.type === 'message' || item.type === 'pending-message') {
|
if (item.type === 'message' || item.type === 'pending-message') {
|
||||||
return (
|
return <MessageItem item={item} />
|
||||||
<MessageItem
|
|
||||||
item={item.message}
|
|
||||||
next={item.nextMessage}
|
|
||||||
pending={item.type === 'pending-message'}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else if (item.type === 'deleted-message') {
|
} else if (item.type === 'deleted-message') {
|
||||||
return <Text>Deleted message</Text>
|
return <Text>Deleted message</Text>
|
||||||
} else if (item.type === 'error-recoverable') {
|
} else if (item.type === 'error-recoverable') {
|
||||||
|
|
|
@ -735,6 +735,8 @@ export class Convo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private pendingFailed = false
|
||||||
|
|
||||||
async sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) {
|
async sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) {
|
||||||
// Ignore empty messages for now since they have no other purpose atm
|
// Ignore empty messages for now since they have no other purpose atm
|
||||||
if (!message.text.trim()) return
|
if (!message.text.trim()) return
|
||||||
|
@ -747,11 +749,9 @@ export class Convo {
|
||||||
id: tempId,
|
id: tempId,
|
||||||
message,
|
message,
|
||||||
})
|
})
|
||||||
// remove on each send, it might go through now without user having to click
|
|
||||||
this.footerItems.delete(ConvoItemError.PendingFailed)
|
|
||||||
this.commit()
|
this.commit()
|
||||||
|
|
||||||
if (!this.isProcessingPendingMessages) {
|
if (!this.isProcessingPendingMessages && !this.pendingFailed) {
|
||||||
this.processPendingMessages()
|
this.processPendingMessages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -805,16 +805,7 @@ export class Convo {
|
||||||
this.commit()
|
this.commit()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e, {context: `Convo: failed to send message`})
|
logger.error(e, {context: `Convo: failed to send message`})
|
||||||
this.footerItems.set(ConvoItemError.PendingFailed, {
|
this.pendingFailed = true
|
||||||
type: 'error-recoverable',
|
|
||||||
key: ConvoItemError.PendingFailed,
|
|
||||||
code: ConvoItemError.PendingFailed,
|
|
||||||
retry: () => {
|
|
||||||
this.footerItems.delete(ConvoItemError.PendingFailed)
|
|
||||||
this.commit()
|
|
||||||
this.batchRetryPendingMessages()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
this.commit()
|
this.commit()
|
||||||
} finally {
|
} finally {
|
||||||
this.isProcessingPendingMessages = false
|
this.isProcessingPendingMessages = false
|
||||||
|
@ -868,16 +859,7 @@ export class Convo {
|
||||||
)
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e, {context: `Convo: failed to batch retry messages`})
|
logger.error(e, {context: `Convo: failed to batch retry messages`})
|
||||||
this.footerItems.set(ConvoItemError.PendingFailed, {
|
this.pendingFailed = true
|
||||||
type: 'error-recoverable',
|
|
||||||
key: ConvoItemError.PendingFailed,
|
|
||||||
code: ConvoItemError.PendingFailed,
|
|
||||||
retry: () => {
|
|
||||||
this.footerItems.delete(ConvoItemError.PendingFailed)
|
|
||||||
this.commit()
|
|
||||||
this.batchRetryPendingMessages()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
this.commit()
|
this.commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -958,6 +940,7 @@ export class Convo {
|
||||||
key: m.id,
|
key: m.id,
|
||||||
message: {
|
message: {
|
||||||
...m.message,
|
...m.message,
|
||||||
|
$type: 'chat.bsky.convo.defs#messageView',
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
rev: '__fake__',
|
rev: '__fake__',
|
||||||
sentAt: new Date().toISOString(),
|
sentAt: new Date().toISOString(),
|
||||||
|
@ -968,6 +951,13 @@ export class Convo {
|
||||||
sender: this.sender!,
|
sender: this.sender!,
|
||||||
},
|
},
|
||||||
nextMessage: null,
|
nextMessage: null,
|
||||||
|
retry: this.pendingFailed
|
||||||
|
? () => {
|
||||||
|
this.pendingFailed = false
|
||||||
|
this.commit()
|
||||||
|
this.batchRetryPendingMessages()
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -35,10 +35,6 @@ export enum ConvoItemError {
|
||||||
* Error fetching past messages
|
* Error fetching past messages
|
||||||
*/
|
*/
|
||||||
HistoryFailed = 'historyFailed',
|
HistoryFailed = 'historyFailed',
|
||||||
/**
|
|
||||||
* Error sending new message
|
|
||||||
*/
|
|
||||||
PendingFailed = 'pendingFailed',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConvoErrorCode {
|
export enum ConvoErrorCode {
|
||||||
|
@ -83,7 +79,7 @@ export type ConvoDispatch =
|
||||||
|
|
||||||
export type ConvoItem =
|
export type ConvoItem =
|
||||||
| {
|
| {
|
||||||
type: 'message' | 'pending-message'
|
type: 'message'
|
||||||
key: string
|
key: string
|
||||||
message: ChatBskyConvoDefs.MessageView
|
message: ChatBskyConvoDefs.MessageView
|
||||||
nextMessage:
|
nextMessage:
|
||||||
|
@ -91,6 +87,19 @@ export type ConvoItem =
|
||||||
| ChatBskyConvoDefs.DeletedMessageView
|
| ChatBskyConvoDefs.DeletedMessageView
|
||||||
| null
|
| null
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'pending-message'
|
||||||
|
key: string
|
||||||
|
message: ChatBskyConvoDefs.MessageView
|
||||||
|
nextMessage:
|
||||||
|
| ChatBskyConvoDefs.MessageView
|
||||||
|
| ChatBskyConvoDefs.DeletedMessageView
|
||||||
|
| null
|
||||||
|
/**
|
||||||
|
* Retry sending the message. If present, the message is in a failed state.
|
||||||
|
*/
|
||||||
|
retry?: () => void
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'deleted-message'
|
type: 'deleted-message'
|
||||||
key: string
|
key: string
|
||||||
|
|
Loading…
Reference in New Issue