diff --git a/src/components/dms/DateDivider.tsx b/src/components/dms/DateDivider.tsx
new file mode 100644
index 0000000000..a9c82e8ea2
--- /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 (
+
+
+
+
+ {date}
+ {' '}
+ at {time}
+
+
+
+ )
+}
+DateDivider = React.memo(DateDivider)
+export {DateDivider}
diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx
index c5c472cf08..52220e2cac 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 (
-
-
- {AppBskyEmbedRecord.isView(message.embed) && (
-
- )}
- {rt.text.length > 0 && (
-
-
-
- )}
-
+ <>
+ {isNewDay && }
+
+
+ {AppBskyEmbedRecord.isView(message.embed) && (
+
+ )}
+ {rt.text.length > 0 && (
+
+
+
+ )}
+
- {isLastInGroup && (
-
- )}
-
+ {isLastInGroup && (
+
+ )}
+
+ >
)
}
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 = ({
)
}
-
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 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]}
/>
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'