From 944172209af2f7af46785878890c600b1c60ffe6 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 27 May 2024 09:58:17 +0300 Subject: [PATCH 1/8] don't show meta if next message is from other other party --- src/components/dms/MessageItem.tsx | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index c5c472cf08..fbfaebca26 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -38,9 +38,12 @@ let MessageItem = ({ 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 isLastInGroup = useMemo(() => { // if this message is pending, it means the next message is pending too @@ -48,24 +51,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,7 +78,11 @@ let MessageItem = ({ }, [message.text, message.facets]) return ( - + {AppBskyEmbedRecord.isView(message.embed) && ( From da1db5573f3acd5c7502107943a59e44f09bca0d Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 29 May 2024 13:28:48 +0300 Subject: [PATCH 2/8] add date divider between days --- src/components/dms/DateDivider.tsx | 50 ++++++++++ src/components/dms/MessageItem.tsx | 153 +++++++++++++---------------- src/components/dms/util.ts | 9 ++ src/state/messages/convo/agent.ts | 23 ++++- src/state/messages/convo/types.ts | 12 +++ 5 files changed, 160 insertions(+), 87 deletions(-) create mode 100644 src/components/dms/DateDivider.tsx diff --git a/src/components/dms/DateDivider.tsx b/src/components/dms/DateDivider.tsx new file mode 100644 index 0000000000..b1f38b1271 --- /dev/null +++ b/src/components/dms/DateDivider.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {atoms as a, useTheme} from '#/alf' +import {Text} from '../Typography' +import {localDateString} from './util' + +let DateDivider = ({date: dateStr}: {date: string}): React.ReactNode => { + const {_} = useLingui() + const t = useTheme() + + let display: string + + const date = new Date(dateStr) + + const today = new Date() + const yesterday = new Date() + yesterday.setDate(today.getDate() - 1) + + if (localDateString(today) === localDateString(date)) { + display = _(msg`Today`) + } else if (localDateString(yesterday) === localDateString(date)) { + display = _(msg`Yesterday`) + } else { + display = new Intl.DateTimeFormat(undefined, { + day: 'numeric', + month: 'numeric', + year: 'numeric', + }).format(date) + } + + return ( + + + {display} + + ) +} +DateDivider = React.memo(DateDivider) +export {DateDivider} diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index fbfaebca26..59951ad310 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,7 +35,7 @@ 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 @@ -45,6 +47,17 @@ let MessageItem = ({ const isNextFromSameSender = isNextFromSelf === isFromSelf + const isNewDay = useMemo(() => { + // TODO: figure out how we can show this for when we're at the start + // of the conversation + if (!prevMessage) return false + + const thisDate = new Date(message.sentAt) + const prevDate = new Date(prevMessage.sentAt) + + return localDateString(thisDate) !== localDateString(prevDate) + }, [message, prevMessage]) + const isLastInGroup = useMemo(() => { // if this message is pending, it means the next message is pending too if (isPending && nextMessage) { @@ -78,56 +91,59 @@ let MessageItem = ({ }, [message.text, message.facets]) return ( - - - {AppBskyEmbedRecord.isView(message.embed) && ( - - )} - {rt.text.length > 0 && ( - - - - )} - + <> + {isNewDay && } + + + {AppBskyEmbedRecord.isView(message.embed) && ( + + )} + {rt.text.length > 0 && ( + + + + )} + - {isLastInGroup && ( - - )} - + {isLastInGroup && ( + + )} + + ) } MessageItem = React.memo(MessageItem) @@ -167,31 +183,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 }, [_], ) @@ -244,15 +241,5 @@ let MessageItemMetadata = ({ ) } - 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/util.ts b/src/components/dms/util.ts index 5952b9acf4..003532d0c6 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 de2605b5ad..53d77046a2 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 53e205e211..21772262ea 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' From e8be471c5bf30ba7f8f932cd9a96ebcc4157104d Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 29 May 2024 13:30:50 +0300 Subject: [PATCH 3/8] change text color --- src/components/dms/DateDivider.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/dms/DateDivider.tsx b/src/components/dms/DateDivider.tsx index b1f38b1271..88b405ecdf 100644 --- a/src/components/dms/DateDivider.tsx +++ b/src/components/dms/DateDivider.tsx @@ -42,7 +42,15 @@ let DateDivider = ({date: dateStr}: {date: string}): React.ReactNode => { {top: '50%', left: 0, right: 0}, ]} /> - {display} + + {display} + ) } From 989fedef23c1c4af1063cebb74e1153a633cb7b7 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 29 May 2024 13:36:00 +0300 Subject: [PATCH 4/8] fix type error --- src/components/dms/ReportDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/dms/ReportDialog.tsx b/src/components/dms/ReportDialog.tsx index 5493a1c87f..2dcd778545 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]} /> From 4455fd08c2337ab0b1d160a707977e8a6c9ea312 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 29 May 2024 13:44:47 +0300 Subject: [PATCH 5/8] fix logic for showing tail --- src/components/dms/MessageItem.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index 59951ad310..8986eb6ac3 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -47,6 +47,8 @@ let MessageItem = ({ const isNextFromSameSender = isNextFromSelf === isFromSelf + const needsTail = !nextIsMessage || !isNextFromSameSender + const isNewDay = useMemo(() => { // TODO: figure out how we can show this for when we're at the start // of the conversation @@ -121,8 +123,8 @@ let MessageItem = ({ }, isFromSelf ? a.self_end : a.self_start, isFromSelf - ? {borderBottomRightRadius: isLastInGroup ? 2 : 17} - : {borderBottomLeftRadius: isLastInGroup ? 2 : 17}, + ? {borderBottomRightRadius: needsTail ? 2 : 17} + : {borderBottomLeftRadius: needsTail ? 2 : 17}, ] }> Date: Thu, 30 May 2024 13:17:26 +0300 Subject: [PATCH 6/8] minimal date separator with time --- src/components/dms/DateDivider.tsx | 69 ++++++++++++++++++++---------- src/components/dms/MessageItem.tsx | 13 +++++- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/components/dms/DateDivider.tsx b/src/components/dms/DateDivider.tsx index 88b405ecdf..375b83106b 100644 --- a/src/components/dms/DateDivider.tsx +++ b/src/components/dms/DateDivider.tsx @@ -1,55 +1,78 @@ import React from 'react' import {View} from 'react-native' -import {msg} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' 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 display: string + let date: string + const time = timeFormatter.format(new Date(dateStr)) - const date = new Date(dateStr) + const timestamp = new Date(dateStr) const today = new Date() const yesterday = new Date() yesterday.setDate(today.getDate() - 1) + const oneWeekAgo = new Date() + oneWeekAgo.setDate(today.getDate() - 7) - if (localDateString(today) === localDateString(date)) { - display = _(msg`Today`) - } else if (localDateString(yesterday) === localDateString(date)) { - display = _(msg`Yesterday`) + if (localDateString(today) === localDateString(timestamp)) { + date = _(msg`Today`) + } else if (localDateString(yesterday) === localDateString(timestamp)) { + date = _(msg`Yesterday`) } else { - display = new Intl.DateTimeFormat(undefined, { - day: 'numeric', - month: 'numeric', - year: 'numeric', - }).format(date) + if (timestamp > oneWeekAgo) { + if (timestamp.getFullYear() === today.getFullYear()) { + date = longDateFormatter.format(timestamp) + } else { + date = longDateFormatterWithYear.format(timestamp) + } + } else { + date = weekdayFormatter.format(timestamp) + } } return ( - - + - {display} + + + {date} + {' '} + at {time} + ) diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index 8986eb6ac3..ac2035f73b 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -47,8 +47,6 @@ let MessageItem = ({ const isNextFromSameSender = isNextFromSelf === isFromSelf - const needsTail = !nextIsMessage || !isNextFromSameSender - const isNewDay = useMemo(() => { // TODO: figure out how we can show this for when we're at the start // of the conversation @@ -60,6 +58,17 @@ let MessageItem = ({ 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 if (isPending && nextMessage) { From 418953371f6de2e9a136d0718f533c68abdb0be4 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 23 Sep 2024 21:01:22 +0100 Subject: [PATCH 7/8] show if no prev message --- src/components/dms/MessageItem.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index ac2035f73b..52220e2cac 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -48,9 +48,7 @@ let MessageItem = ({ const isNextFromSameSender = isNextFromSelf === isFromSelf const isNewDay = useMemo(() => { - // TODO: figure out how we can show this for when we're at the start - // of the conversation - if (!prevMessage) return false + if (!prevMessage) return true const thisDate = new Date(message.sentAt) const prevDate = new Date(prevMessage.sentAt) From 8f5b20c210528bc2ac5efb7c37360c9a02333e0a Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 3 Oct 2024 09:34:52 +0300 Subject: [PATCH 8/8] fix comparison, use subdays --- src/components/dms/DateDivider.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/dms/DateDivider.tsx b/src/components/dms/DateDivider.tsx index 375b83106b..a9c82e8ea2 100644 --- a/src/components/dms/DateDivider.tsx +++ b/src/components/dms/DateDivider.tsx @@ -2,6 +2,7 @@ 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' @@ -36,17 +37,15 @@ let DateDivider = ({date: dateStr}: {date: string}): React.ReactNode => { const timestamp = new Date(dateStr) const today = new Date() - const yesterday = new Date() - yesterday.setDate(today.getDate() - 1) - const oneWeekAgo = new Date() - oneWeekAgo.setDate(today.getDate() - 7) + 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 < oneWeekAgo) { if (timestamp.getFullYear() === today.getFullYear()) { date = longDateFormatter.format(timestamp) } else {