my fork of the bluesky client

[🐴] Reduce amount that message sent date is shown (#4228)

authored by samuel.fm and committed by

GitHub 7e79c7f7 523a9a48

+209 -94
+80
src/components/dms/DateDivider.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {msg, Trans} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + import {subDays} from 'date-fns' 6 + 7 + import {atoms as a, useTheme} from '#/alf' 8 + import {Text} from '../Typography' 9 + import {localDateString} from './util' 10 + 11 + const timeFormatter = new Intl.DateTimeFormat(undefined, { 12 + hour: 'numeric', 13 + minute: 'numeric', 14 + }) 15 + const weekdayFormatter = new Intl.DateTimeFormat(undefined, { 16 + weekday: 'long', 17 + }) 18 + const longDateFormatter = new Intl.DateTimeFormat(undefined, { 19 + weekday: 'short', 20 + month: 'long', 21 + day: 'numeric', 22 + }) 23 + const longDateFormatterWithYear = new Intl.DateTimeFormat(undefined, { 24 + weekday: 'short', 25 + month: 'long', 26 + day: 'numeric', 27 + year: 'numeric', 28 + }) 29 + 30 + let DateDivider = ({date: dateStr}: {date: string}): React.ReactNode => { 31 + const {_} = useLingui() 32 + const t = useTheme() 33 + 34 + let date: string 35 + const time = timeFormatter.format(new Date(dateStr)) 36 + 37 + const timestamp = new Date(dateStr) 38 + 39 + const today = new Date() 40 + const yesterday = subDays(today, 1) 41 + const oneWeekAgo = subDays(today, 7) 42 + 43 + if (localDateString(today) === localDateString(timestamp)) { 44 + date = _(msg`Today`) 45 + } else if (localDateString(yesterday) === localDateString(timestamp)) { 46 + date = _(msg`Yesterday`) 47 + } else { 48 + if (timestamp < oneWeekAgo) { 49 + if (timestamp.getFullYear() === today.getFullYear()) { 50 + date = longDateFormatter.format(timestamp) 51 + } else { 52 + date = longDateFormatterWithYear.format(timestamp) 53 + } 54 + } else { 55 + date = weekdayFormatter.format(timestamp) 56 + } 57 + } 58 + 59 + return ( 60 + <View style={[a.w_full, a.my_lg]}> 61 + <Text 62 + style={[ 63 + a.text_xs, 64 + a.text_center, 65 + t.atoms.bg, 66 + t.atoms.text_contrast_medium, 67 + a.px_md, 68 + ]}> 69 + <Trans> 70 + <Text style={[a.text_xs, t.atoms.text_contrast_medium, a.font_bold]}> 71 + {date} 72 + </Text>{' '} 73 + at {time} 74 + </Trans> 75 + </Text> 76 + </View> 77 + ) 78 + } 79 + DateDivider = React.memo(DateDivider) 80 + export {DateDivider}
+88 -90
src/components/dms/MessageItem.tsx
··· 17 17 18 18 import {ConvoItem} from '#/state/messages/convo/types' 19 19 import {useSession} from '#/state/session' 20 - import {TimeElapsed} from 'view/com/util/TimeElapsed' 20 + import {TimeElapsed} from '#/view/com/util/TimeElapsed' 21 21 import {atoms as a, useTheme} from '#/alf' 22 22 import {ActionsWrapper} from '#/components/dms/ActionsWrapper' 23 23 import {InlineLinkText} from '#/components/Link' 24 24 import {Text} from '#/components/Typography' 25 25 import {isOnlyEmoji, RichText} from '../RichText' 26 + import {DateDivider} from './DateDivider' 26 27 import {MessageItemEmbed} from './MessageItemEmbed' 28 + import {localDateString} from './util' 27 29 28 30 let MessageItem = ({ 29 31 item, ··· 33 35 const t = useTheme() 34 36 const {currentAccount} = useSession() 35 37 36 - const {message, nextMessage} = item 38 + const {message, nextMessage, prevMessage} = item 37 39 const isPending = item.type === 'pending-message' 38 40 39 41 const isFromSelf = message.sender?.did === currentAccount?.did 40 42 43 + const nextIsMessage = ChatBskyConvoDefs.isMessageView(nextMessage) 44 + 41 45 const isNextFromSelf = 42 - ChatBskyConvoDefs.isMessageView(nextMessage) && 43 - nextMessage.sender?.did === currentAccount?.did 46 + nextIsMessage && nextMessage.sender?.did === currentAccount?.did 47 + 48 + const isNextFromSameSender = isNextFromSelf === isFromSelf 49 + 50 + const isNewDay = useMemo(() => { 51 + if (!prevMessage) return true 52 + 53 + const thisDate = new Date(message.sentAt) 54 + const prevDate = new Date(prevMessage.sentAt) 55 + 56 + return localDateString(thisDate) !== localDateString(prevDate) 57 + }, [message, prevMessage]) 58 + 59 + const isLastMessageOfDay = useMemo(() => { 60 + if (!nextMessage || !nextIsMessage) return true 61 + 62 + const thisDate = new Date(message.sentAt) 63 + const prevDate = new Date(nextMessage.sentAt) 64 + 65 + return localDateString(thisDate) !== localDateString(prevDate) 66 + }, [message.sentAt, nextIsMessage, nextMessage]) 67 + 68 + const needsTail = isLastMessageOfDay || !isNextFromSameSender 44 69 45 70 const isLastInGroup = useMemo(() => { 46 71 // if this message is pending, it means the next message is pending too ··· 48 73 return false 49 74 } 50 75 51 - // if the next message is from a different sender, then it's the last in the group 52 - if (isFromSelf ? !isNextFromSelf : isNextFromSelf) { 53 - return true 54 - } 55 - 56 - // or, if there's a 3 minute gap between this message and the next 76 + // or, if there's a 5 minute gap between this message and the next 57 77 if (ChatBskyConvoDefs.isMessageView(nextMessage)) { 58 78 const thisDate = new Date(message.sentAt) 59 79 const nextDate = new Date(nextMessage.sentAt) 60 80 61 81 const diff = nextDate.getTime() - thisDate.getTime() 62 82 63 - // 3 minutes 64 - return diff > 3 * 60 * 1000 83 + // 5 minutes 84 + return diff > 5 * 60 * 1000 65 85 } 66 86 67 87 return true 68 - }, [message, nextMessage, isFromSelf, isNextFromSelf, isPending]) 88 + }, [message, nextMessage, isPending]) 69 89 70 90 const lastInGroupRef = useRef(isLastInGroup) 71 91 if (lastInGroupRef.current !== isLastInGroup) { ··· 80 100 }, [message.text, message.facets]) 81 101 82 102 return ( 83 - <View style={[isFromSelf ? a.mr_md : a.ml_md]}> 84 - <ActionsWrapper isFromSelf={isFromSelf} message={message}> 85 - {AppBskyEmbedRecord.isView(message.embed) && ( 86 - <MessageItemEmbed embed={message.embed} /> 87 - )} 88 - {rt.text.length > 0 && ( 89 - <View 90 - style={ 91 - !isOnlyEmoji(message.text) && [ 92 - a.py_sm, 93 - a.my_2xs, 94 - a.rounded_md, 95 - { 96 - paddingLeft: 14, 97 - paddingRight: 14, 98 - backgroundColor: isFromSelf 99 - ? isPending 100 - ? pendingColor 101 - : t.palette.primary_500 102 - : t.palette.contrast_50, 103 - borderRadius: 17, 104 - }, 105 - isFromSelf ? a.self_end : a.self_start, 106 - isFromSelf 107 - ? {borderBottomRightRadius: isLastInGroup ? 2 : 17} 108 - : {borderBottomLeftRadius: isLastInGroup ? 2 : 17}, 109 - ] 110 - }> 111 - <RichText 112 - value={rt} 113 - style={[a.text_md, isFromSelf && {color: t.palette.white}]} 114 - interactiveStyle={a.underline} 115 - enableTags 116 - emojiMultiplier={3} 117 - /> 118 - </View> 119 - )} 120 - </ActionsWrapper> 103 + <> 104 + {isNewDay && <DateDivider date={message.sentAt} />} 105 + <View 106 + style={[ 107 + isFromSelf ? a.mr_md : a.ml_md, 108 + nextIsMessage && !isNextFromSameSender && a.mb_md, 109 + ]}> 110 + <ActionsWrapper isFromSelf={isFromSelf} message={message}> 111 + {AppBskyEmbedRecord.isView(message.embed) && ( 112 + <MessageItemEmbed embed={message.embed} /> 113 + )} 114 + {rt.text.length > 0 && ( 115 + <View 116 + style={ 117 + !isOnlyEmoji(message.text) && [ 118 + a.py_sm, 119 + a.my_2xs, 120 + a.rounded_md, 121 + { 122 + paddingLeft: 14, 123 + paddingRight: 14, 124 + backgroundColor: isFromSelf 125 + ? isPending 126 + ? pendingColor 127 + : t.palette.primary_500 128 + : t.palette.contrast_50, 129 + borderRadius: 17, 130 + }, 131 + isFromSelf ? a.self_end : a.self_start, 132 + isFromSelf 133 + ? {borderBottomRightRadius: needsTail ? 2 : 17} 134 + : {borderBottomLeftRadius: needsTail ? 2 : 17}, 135 + ] 136 + }> 137 + <RichText 138 + value={rt} 139 + style={[a.text_md, isFromSelf && {color: t.palette.white}]} 140 + interactiveStyle={a.underline} 141 + enableTags 142 + emojiMultiplier={3} 143 + /> 144 + </View> 145 + )} 146 + </ActionsWrapper> 121 147 122 - {isLastInGroup && ( 123 - <MessageItemMetadata 124 - item={item} 125 - style={isFromSelf ? a.text_right : a.text_left} 126 - /> 127 - )} 128 - </View> 148 + {isLastInGroup && ( 149 + <MessageItemMetadata 150 + item={item} 151 + style={isFromSelf ? a.text_right : a.text_left} 152 + /> 153 + )} 154 + </View> 155 + </> 129 156 ) 130 157 } 131 158 MessageItem = React.memo(MessageItem) ··· 165 192 166 193 const diff = now.getTime() - date.getTime() 167 194 168 - // if under 1 minute 169 - if (diff < 1000 * 60) { 195 + // if under 30 seconds 196 + if (diff < 1000 * 30) { 170 197 return _(msg`Now`) 171 198 } 172 199 173 - // if in the last day 174 - if (localDateString(now) === localDateString(date)) { 175 - return time 176 - } 177 - 178 - // if yesterday 179 - const yesterday = new Date(now) 180 - yesterday.setDate(yesterday.getDate() - 1) 181 - 182 - if (localDateString(yesterday) === localDateString(date)) { 183 - return _(msg`Yesterday, ${time}`) 184 - } 185 - 186 - return i18n.date(date, { 187 - hour: 'numeric', 188 - minute: 'numeric', 189 - day: 'numeric', 190 - month: 'numeric', 191 - year: 'numeric', 192 - }) 200 + return time 193 201 }, 194 202 [_], 195 203 ) ··· 242 250 </Text> 243 251 ) 244 252 } 245 - 246 253 MessageItemMetadata = React.memo(MessageItemMetadata) 247 254 export {MessageItemMetadata} 248 - 249 - function localDateString(date: Date) { 250 - // can't use toISOString because it should be in local time 251 - const mm = date.getMonth() 252 - const dd = date.getDate() 253 - const yyyy = date.getFullYear() 254 - // not padding with 0s because it's not necessary, it's just used for comparison 255 - return `${yyyy}-${mm}-${dd}` 256 - }
+1
src/components/dms/ReportDialog.tsx
··· 277 277 message, 278 278 key: '', 279 279 nextMessage: null, 280 + prevMessage: null, 280 281 }} 281 282 style={[a.text_left, a.mb_0]} 282 283 />
+9
src/components/dms/util.ts
··· 16 16 return false 17 17 } 18 18 } 19 + 20 + export function localDateString(date: Date) { 21 + // can't use toISOString because it should be in local time 22 + const mm = date.getMonth() 23 + const dd = date.getDate() 24 + const yyyy = date.getFullYear() 25 + // not padding with 0s because it's not necessary, it's just used for comparison 26 + return `${yyyy}-${mm}-${dd}` 27 + }
+19 -4
src/state/messages/convo/agent.ts
··· 972 972 key: m.id, 973 973 message: m, 974 974 nextMessage: null, 975 + prevMessage: null, 975 976 }) 976 977 } else if (ChatBskyConvoDefs.isDeletedMessageView(m)) { 977 978 items.unshift({ ··· 979 980 key: m.id, 980 981 message: m, 981 982 nextMessage: null, 983 + prevMessage: null, 982 984 }) 983 985 } 984 986 }) ··· 1001 1003 key: m.id, 1002 1004 message: m, 1003 1005 nextMessage: null, 1006 + prevMessage: null, 1004 1007 }) 1005 1008 } else if (ChatBskyConvoDefs.isDeletedMessageView(m)) { 1006 1009 items.push({ ··· 1008 1011 key: m.id, 1009 1012 message: m, 1010 1013 nextMessage: null, 1014 + prevMessage: null, 1011 1015 }) 1012 1016 } 1013 1017 }) ··· 1030 1034 sender: this.sender!, 1031 1035 }, 1032 1036 nextMessage: null, 1037 + prevMessage: null, 1033 1038 failed: this.pendingMessageFailure !== null, 1034 1039 retry: 1035 1040 this.pendingMessageFailure === 'recoverable' ··· 1060 1065 }) 1061 1066 .map((item, i, arr) => { 1062 1067 let nextMessage = null 1068 + let prevMessage = null 1063 1069 const isMessage = isConvoItemMessage(item) 1064 1070 1065 1071 if (isMessage) { 1066 1072 if ( 1067 - isMessage && 1068 - (ChatBskyConvoDefs.isMessageView(item.message) || 1069 - ChatBskyConvoDefs.isDeletedMessageView(item.message)) 1073 + ChatBskyConvoDefs.isMessageView(item.message) || 1074 + ChatBskyConvoDefs.isDeletedMessageView(item.message) 1070 1075 ) { 1071 1076 const next = arr[i + 1] 1072 1077 1073 1078 if ( 1074 1079 isConvoItemMessage(next) && 1075 - next && 1076 1080 (ChatBskyConvoDefs.isMessageView(next.message) || 1077 1081 ChatBskyConvoDefs.isDeletedMessageView(next.message)) 1078 1082 ) { 1079 1083 nextMessage = next.message 1080 1084 } 1085 + 1086 + const prev = arr[i - 1] 1087 + 1088 + if ( 1089 + isConvoItemMessage(prev) && 1090 + (ChatBskyConvoDefs.isMessageView(prev.message) || 1091 + ChatBskyConvoDefs.isDeletedMessageView(prev.message)) 1092 + ) { 1093 + prevMessage = prev.message 1094 + } 1081 1095 } 1082 1096 1083 1097 return { 1084 1098 ...item, 1085 1099 nextMessage, 1100 + prevMessage, 1086 1101 } 1087 1102 } 1088 1103
+12
src/state/messages/convo/types.ts
··· 87 87 | ChatBskyConvoDefs.MessageView 88 88 | ChatBskyConvoDefs.DeletedMessageView 89 89 | null 90 + prevMessage: 91 + | ChatBskyConvoDefs.MessageView 92 + | ChatBskyConvoDefs.DeletedMessageView 93 + | null 90 94 } 91 95 | { 92 96 type: 'pending-message' ··· 96 100 | ChatBskyConvoDefs.MessageView 97 101 | ChatBskyConvoDefs.DeletedMessageView 98 102 | null 103 + prevMessage: 104 + | ChatBskyConvoDefs.MessageView 105 + | ChatBskyConvoDefs.DeletedMessageView 106 + | null 99 107 failed: boolean 100 108 /** 101 109 * Retry sending the message. If present, the message is in a failed state. ··· 107 115 key: string 108 116 message: ChatBskyConvoDefs.DeletedMessageView 109 117 nextMessage: 118 + | ChatBskyConvoDefs.MessageView 119 + | ChatBskyConvoDefs.DeletedMessageView 120 + | null 121 + prevMessage: 110 122 | ChatBskyConvoDefs.MessageView 111 123 | ChatBskyConvoDefs.DeletedMessageView 112 124 | null