diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/components/dms/DateDivider.tsx | 80 | ||||
-rw-r--r-- | src/components/dms/MessageItem.tsx | 178 | ||||
-rw-r--r-- | src/components/dms/ReportDialog.tsx | 1 | ||||
-rw-r--r-- | src/components/dms/util.ts | 9 | ||||
-rw-r--r-- | src/state/messages/convo/agent.ts | 23 | ||||
-rw-r--r-- | src/state/messages/convo/types.ts | 12 |
6 files changed, 209 insertions, 94 deletions
diff --git a/src/components/dms/DateDivider.tsx b/src/components/dms/DateDivider.tsx new file mode 100644 index 000000000..a9c82e8ea --- /dev/null +++ b/src/components/dms/DateDivider.tsx @@ -0,0 +1,80 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {subDays} from 'date-fns' + +import {atoms as a, useTheme} from '#/alf' +import {Text} from '../Typography' +import {localDateString} from './util' + +const timeFormatter = new Intl.DateTimeFormat(undefined, { + hour: 'numeric', + minute: 'numeric', +}) +const weekdayFormatter = new Intl.DateTimeFormat(undefined, { + weekday: 'long', +}) +const longDateFormatter = new Intl.DateTimeFormat(undefined, { + weekday: 'short', + month: 'long', + day: 'numeric', +}) +const longDateFormatterWithYear = new Intl.DateTimeFormat(undefined, { + weekday: 'short', + month: 'long', + day: 'numeric', + year: 'numeric', +}) + +let DateDivider = ({date: dateStr}: {date: string}): React.ReactNode => { + const {_} = useLingui() + const t = useTheme() + + let date: string + const time = timeFormatter.format(new Date(dateStr)) + + const timestamp = new Date(dateStr) + + const today = new Date() + const yesterday = subDays(today, 1) + const oneWeekAgo = subDays(today, 7) + + if (localDateString(today) === localDateString(timestamp)) { + date = _(msg`Today`) + } else if (localDateString(yesterday) === localDateString(timestamp)) { + date = _(msg`Yesterday`) + } else { + if (timestamp < oneWeekAgo) { + if (timestamp.getFullYear() === today.getFullYear()) { + date = longDateFormatter.format(timestamp) + } else { + date = longDateFormatterWithYear.format(timestamp) + } + } else { + date = weekdayFormatter.format(timestamp) + } + } + + return ( + <View style={[a.w_full, a.my_lg]}> + <Text + style={[ + a.text_xs, + a.text_center, + t.atoms.bg, + t.atoms.text_contrast_medium, + a.px_md, + ]}> + <Trans> + <Text style={[a.text_xs, t.atoms.text_contrast_medium, a.font_bold]}> + {date} + </Text>{' '} + at {time} + </Trans> + </Text> + </View> + ) +} +DateDivider = React.memo(DateDivider) +export {DateDivider} diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index c5c472cf0..52220e2ca 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -17,13 +17,15 @@ import {useLingui} from '@lingui/react' import {ConvoItem} from '#/state/messages/convo/types' 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 {ActionsWrapper} from '#/components/dms/ActionsWrapper' import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' import {isOnlyEmoji, RichText} from '../RichText' +import {DateDivider} from './DateDivider' import {MessageItemEmbed} from './MessageItemEmbed' +import {localDateString} from './util' let MessageItem = ({ item, @@ -33,14 +35,37 @@ let MessageItem = ({ const t = useTheme() const {currentAccount} = useSession() - const {message, nextMessage} = item + const {message, nextMessage, prevMessage} = item const isPending = item.type === 'pending-message' const isFromSelf = message.sender?.did === currentAccount?.did + const nextIsMessage = ChatBskyConvoDefs.isMessageView(nextMessage) + const isNextFromSelf = - ChatBskyConvoDefs.isMessageView(nextMessage) && - nextMessage.sender?.did === currentAccount?.did + nextIsMessage && nextMessage.sender?.did === currentAccount?.did + + const isNextFromSameSender = isNextFromSelf === isFromSelf + + const isNewDay = useMemo(() => { + if (!prevMessage) return true + + const thisDate = new Date(message.sentAt) + const prevDate = new Date(prevMessage.sentAt) + + return localDateString(thisDate) !== localDateString(prevDate) + }, [message, prevMessage]) + + const isLastMessageOfDay = useMemo(() => { + if (!nextMessage || !nextIsMessage) return true + + const thisDate = new Date(message.sentAt) + const prevDate = new Date(nextMessage.sentAt) + + return localDateString(thisDate) !== localDateString(prevDate) + }, [message.sentAt, nextIsMessage, nextMessage]) + + const needsTail = isLastMessageOfDay || !isNextFromSameSender const isLastInGroup = useMemo(() => { // if this message is pending, it means the next message is pending too @@ -48,24 +73,19 @@ let MessageItem = ({ return false } - // if the next message is from a different sender, then it's the last in the group - if (isFromSelf ? !isNextFromSelf : isNextFromSelf) { - return true - } - - // or, if there's a 3 minute gap between this message and the next + // or, if there's a 5 minute gap between this message and the next if (ChatBskyConvoDefs.isMessageView(nextMessage)) { const thisDate = new Date(message.sentAt) const nextDate = new Date(nextMessage.sentAt) const diff = nextDate.getTime() - thisDate.getTime() - // 3 minutes - return diff > 3 * 60 * 1000 + // 5 minutes + return diff > 5 * 60 * 1000 } return true - }, [message, nextMessage, isFromSelf, isNextFromSelf, isPending]) + }, [message, nextMessage, isPending]) const lastInGroupRef = useRef(isLastInGroup) if (lastInGroupRef.current !== isLastInGroup) { @@ -80,52 +100,59 @@ let MessageItem = ({ }, [message.text, message.facets]) return ( - <View style={[isFromSelf ? a.mr_md : a.ml_md]}> - <ActionsWrapper isFromSelf={isFromSelf} message={message}> - {AppBskyEmbedRecord.isView(message.embed) && ( - <MessageItemEmbed embed={message.embed} /> - )} - {rt.text.length > 0 && ( - <View - style={ - !isOnlyEmoji(message.text) && [ - a.py_sm, - a.my_2xs, - a.rounded_md, - { - paddingLeft: 14, - paddingRight: 14, - backgroundColor: isFromSelf - ? isPending - ? pendingColor - : t.palette.primary_500 - : t.palette.contrast_50, - borderRadius: 17, - }, - isFromSelf ? a.self_end : a.self_start, - isFromSelf - ? {borderBottomRightRadius: isLastInGroup ? 2 : 17} - : {borderBottomLeftRadius: isLastInGroup ? 2 : 17}, - ] - }> - <RichText - value={rt} - style={[a.text_md, isFromSelf && {color: t.palette.white}]} - interactiveStyle={a.underline} - enableTags - emojiMultiplier={3} - /> - </View> - )} - </ActionsWrapper> + <> + {isNewDay && <DateDivider date={message.sentAt} />} + <View + style={[ + isFromSelf ? a.mr_md : a.ml_md, + nextIsMessage && !isNextFromSameSender && a.mb_md, + ]}> + <ActionsWrapper isFromSelf={isFromSelf} message={message}> + {AppBskyEmbedRecord.isView(message.embed) && ( + <MessageItemEmbed embed={message.embed} /> + )} + {rt.text.length > 0 && ( + <View + style={ + !isOnlyEmoji(message.text) && [ + a.py_sm, + a.my_2xs, + a.rounded_md, + { + paddingLeft: 14, + paddingRight: 14, + backgroundColor: isFromSelf + ? isPending + ? pendingColor + : t.palette.primary_500 + : t.palette.contrast_50, + borderRadius: 17, + }, + isFromSelf ? a.self_end : a.self_start, + isFromSelf + ? {borderBottomRightRadius: needsTail ? 2 : 17} + : {borderBottomLeftRadius: needsTail ? 2 : 17}, + ] + }> + <RichText + value={rt} + style={[a.text_md, isFromSelf && {color: t.palette.white}]} + interactiveStyle={a.underline} + enableTags + emojiMultiplier={3} + /> + </View> + )} + </ActionsWrapper> - {isLastInGroup && ( - <MessageItemMetadata - item={item} - style={isFromSelf ? a.text_right : a.text_left} - /> - )} - </View> + {isLastInGroup && ( + <MessageItemMetadata + item={item} + style={isFromSelf ? a.text_right : a.text_left} + /> + )} + </View> + </> ) } MessageItem = React.memo(MessageItem) @@ -165,31 +192,12 @@ let MessageItemMetadata = ({ const diff = now.getTime() - date.getTime() - // if under 1 minute - if (diff < 1000 * 60) { + // if under 30 seconds + if (diff < 1000 * 30) { return _(msg`Now`) } - // if in the last day - if (localDateString(now) === localDateString(date)) { - return time - } - - // if yesterday - const yesterday = new Date(now) - yesterday.setDate(yesterday.getDate() - 1) - - if (localDateString(yesterday) === localDateString(date)) { - return _(msg`Yesterday, ${time}`) - } - - return i18n.date(date, { - hour: 'numeric', - minute: 'numeric', - day: 'numeric', - month: 'numeric', - year: 'numeric', - }) + return time }, [_], ) @@ -242,15 +250,5 @@ let MessageItemMetadata = ({ </Text> ) } - MessageItemMetadata = React.memo(MessageItemMetadata) export {MessageItemMetadata} - -function localDateString(date: Date) { - // can't use toISOString because it should be in local time - const mm = date.getMonth() - const dd = date.getDate() - const yyyy = date.getFullYear() - // not padding with 0s because it's not necessary, it's just used for comparison - return `${yyyy}-${mm}-${dd}` -} diff --git a/src/components/dms/ReportDialog.tsx b/src/components/dms/ReportDialog.tsx index 5493a1c87..2dcd77854 100644 --- a/src/components/dms/ReportDialog.tsx +++ b/src/components/dms/ReportDialog.tsx @@ -277,6 +277,7 @@ function PreviewMessage({message}: {message: ChatBskyConvoDefs.MessageView}) { message, key: '', nextMessage: null, + prevMessage: null, }} style={[a.text_left, a.mb_0]} /> diff --git a/src/components/dms/util.ts b/src/components/dms/util.ts index 5952b9acf..003532d0c 100644 --- a/src/components/dms/util.ts +++ b/src/components/dms/util.ts @@ -16,3 +16,12 @@ export function canBeMessaged(profile: AppBskyActorDefs.ProfileView) { return false } } + +export function localDateString(date: Date) { + // can't use toISOString because it should be in local time + const mm = date.getMonth() + const dd = date.getDate() + const yyyy = date.getFullYear() + // not padding with 0s because it's not necessary, it's just used for comparison + return `${yyyy}-${mm}-${dd}` +} diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts index de2605b5a..53d77046a 100644 --- a/src/state/messages/convo/agent.ts +++ b/src/state/messages/convo/agent.ts @@ -972,6 +972,7 @@ export class Convo { key: m.id, message: m, nextMessage: null, + prevMessage: null, }) } else if (ChatBskyConvoDefs.isDeletedMessageView(m)) { items.unshift({ @@ -979,6 +980,7 @@ export class Convo { key: m.id, message: m, nextMessage: null, + prevMessage: null, }) } }) @@ -1001,6 +1003,7 @@ export class Convo { key: m.id, message: m, nextMessage: null, + prevMessage: null, }) } else if (ChatBskyConvoDefs.isDeletedMessageView(m)) { items.push({ @@ -1008,6 +1011,7 @@ export class Convo { key: m.id, message: m, nextMessage: null, + prevMessage: null, }) } }) @@ -1030,6 +1034,7 @@ export class Convo { sender: this.sender!, }, nextMessage: null, + prevMessage: null, failed: this.pendingMessageFailure !== null, retry: this.pendingMessageFailure === 'recoverable' @@ -1060,29 +1065,39 @@ export class Convo { }) .map((item, i, arr) => { let nextMessage = null + let prevMessage = null const isMessage = isConvoItemMessage(item) if (isMessage) { if ( - isMessage && - (ChatBskyConvoDefs.isMessageView(item.message) || - ChatBskyConvoDefs.isDeletedMessageView(item.message)) + ChatBskyConvoDefs.isMessageView(item.message) || + ChatBskyConvoDefs.isDeletedMessageView(item.message) ) { const next = arr[i + 1] if ( isConvoItemMessage(next) && - next && (ChatBskyConvoDefs.isMessageView(next.message) || ChatBskyConvoDefs.isDeletedMessageView(next.message)) ) { nextMessage = next.message } + + const prev = arr[i - 1] + + if ( + isConvoItemMessage(prev) && + (ChatBskyConvoDefs.isMessageView(prev.message) || + ChatBskyConvoDefs.isDeletedMessageView(prev.message)) + ) { + prevMessage = prev.message + } } return { ...item, nextMessage, + prevMessage, } } diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts index 53e205e21..21772262e 100644 --- a/src/state/messages/convo/types.ts +++ b/src/state/messages/convo/types.ts @@ -87,6 +87,10 @@ export type ConvoItem = | ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.DeletedMessageView | null + prevMessage: + | ChatBskyConvoDefs.MessageView + | ChatBskyConvoDefs.DeletedMessageView + | null } | { type: 'pending-message' @@ -96,6 +100,10 @@ export type ConvoItem = | ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.DeletedMessageView | null + prevMessage: + | ChatBskyConvoDefs.MessageView + | ChatBskyConvoDefs.DeletedMessageView + | null failed: boolean /** * Retry sending the message. If present, the message is in a failed state. @@ -110,6 +118,10 @@ export type ConvoItem = | ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.DeletedMessageView | null + prevMessage: + | ChatBskyConvoDefs.MessageView + | ChatBskyConvoDefs.DeletedMessageView + | null } | { type: 'error' |