From 2aa365b5b6fed920d17d307252a7af7c52b95855 Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 3 Oct 2024 14:57:48 +0900 Subject: [PATCH 1/4] Rename some files and variables (#5587) * Move composer reducers together * videoUploadState -> videoState * Inline videoDispatch --- src/lib/api/index.ts | 2 +- src/lib/media/video/compress.ts | 2 +- .../media/video/upload.shared.ts} | 0 .../media/video/upload.ts} | 4 +- .../media/video/upload.web.ts} | 4 +- .../queries => lib/media}/video/util.ts | 0 src/view/com/composer/Composer.tsx | 120 ++++++++---------- src/view/com/composer/photos/Gallery.tsx | 2 +- .../composer/{state.ts => state/composer.ts} | 7 +- .../com/composer/state}/video.ts | 4 +- 10 files changed, 64 insertions(+), 81 deletions(-) rename src/{state/queries/video/video-upload.shared.ts => lib/media/video/upload.shared.ts} (100%) rename src/{state/queries/video/video-upload.ts => lib/media/video/upload.ts} (92%) rename src/{state/queries/video/video-upload.web.ts => lib/media/video/upload.web.ts} (93%) rename src/{state/queries => lib/media}/video/util.ts (100%) rename src/view/com/composer/{state.ts => state/composer.ts} (97%) rename src/{state/queries/video => view/com/composer/state}/video.ts (98%) diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 51bf51ffff..8b79250042 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -24,7 +24,7 @@ import { threadgateAllowUISettingToAllowRecordValue, writeThreadgateRecord, } from '#/state/queries/threadgate' -import {ComposerState} from '#/view/com/composer/state' +import {ComposerState} from '#/view/com/composer/state/composer' import {LinkMeta} from '../link-meta/link-meta' import {uploadBlob} from './upload-blob' diff --git a/src/lib/media/video/compress.ts b/src/lib/media/video/compress.ts index dec9032a34..c2d1470c63 100644 --- a/src/lib/media/video/compress.ts +++ b/src/lib/media/video/compress.ts @@ -2,8 +2,8 @@ import {getVideoMetaData, Video} from 'react-native-compressor' import {ImagePickerAsset} from 'expo-image-picker' import {SUPPORTED_MIME_TYPES, SupportedMimeTypes} from '#/lib/constants' -import {extToMime} from '#/state/queries/video/util' import {CompressedVideo} from './types' +import {extToMime} from './util' const MIN_SIZE_FOR_COMPRESSION = 25 // 25mb diff --git a/src/state/queries/video/video-upload.shared.ts b/src/lib/media/video/upload.shared.ts similarity index 100% rename from src/state/queries/video/video-upload.shared.ts rename to src/lib/media/video/upload.shared.ts diff --git a/src/state/queries/video/video-upload.ts b/src/lib/media/video/upload.ts similarity index 92% rename from src/state/queries/video/video-upload.ts rename to src/lib/media/video/upload.ts index 46f24a58b1..3330370b3e 100644 --- a/src/state/queries/video/video-upload.ts +++ b/src/lib/media/video/upload.ts @@ -7,8 +7,8 @@ import {nanoid} from 'nanoid/non-secure' import {AbortError} from '#/lib/async/cancelable' import {ServerError} from '#/lib/media/video/errors' import {CompressedVideo} from '#/lib/media/video/types' -import {createVideoEndpointUrl, mimeToExt} from '#/state/queries/video/util' -import {getServiceAuthToken, getVideoUploadLimits} from './video-upload.shared' +import {createVideoEndpointUrl, mimeToExt} from './util' +import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' export async function uploadVideo({ video, diff --git a/src/state/queries/video/video-upload.web.ts b/src/lib/media/video/upload.web.ts similarity index 93% rename from src/state/queries/video/video-upload.web.ts rename to src/lib/media/video/upload.web.ts index bbae641999..ec65f96c97 100644 --- a/src/state/queries/video/video-upload.web.ts +++ b/src/lib/media/video/upload.web.ts @@ -7,8 +7,8 @@ import {nanoid} from 'nanoid/non-secure' import {AbortError} from '#/lib/async/cancelable' import {ServerError} from '#/lib/media/video/errors' import {CompressedVideo} from '#/lib/media/video/types' -import {createVideoEndpointUrl, mimeToExt} from '#/state/queries/video/util' -import {getServiceAuthToken, getVideoUploadLimits} from './video-upload.shared' +import {createVideoEndpointUrl, mimeToExt} from './util' +import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' export async function uploadVideo({ video, diff --git a/src/state/queries/video/util.ts b/src/lib/media/video/util.ts similarity index 100% rename from src/state/queries/video/util.ts rename to src/lib/media/video/util.ts diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 59aae29516..f4e290ca8d 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -82,13 +82,6 @@ import {useProfileQuery} from '#/state/queries/profile' import {Gif} from '#/state/queries/tenor' import {ThreadgateAllowUISetting} from '#/state/queries/threadgate' import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util' -import {NO_VIDEO, NoVideoState} from '#/state/queries/video/video' -import { - processVideo, - VideoAction, - VideoState, - VideoState as VideoUploadState, -} from '#/state/queries/video/video' import {useAgent, useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {ComposerOpts} from '#/state/shell/composer' @@ -123,7 +116,8 @@ import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import * as Prompt from '#/components/Prompt' import {Text as NewText} from '#/components/Typography' -import {composerReducer, createComposerState} from './state' +import {composerReducer, createComposerState} from './state/composer' +import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video' const MAX_IMAGES = 4 @@ -200,16 +194,10 @@ export const ComposePost = ({ createComposerState, ) - let videoUploadState: VideoState | NoVideoState = NO_VIDEO + let videoState: VideoState | NoVideoState = NO_VIDEO if (composerState.embed.media?.type === 'video') { - videoUploadState = composerState.embed.media.video + videoState = composerState.embed.media.video } - const videoDispatch = useCallback( - (videoAction: VideoAction) => { - dispatch({type: 'embed_update_video', videoAction}) - }, - [dispatch], - ) const selectVideo = React.useCallback( (asset: ImagePickerAsset) => { @@ -217,14 +205,14 @@ export const ComposePost = ({ dispatch({type: 'embed_add_video', asset, abortController}) processVideo( asset, - videoDispatch, + videoAction => dispatch({type: 'embed_update_video', videoAction}), agent, currentDid, abortController.signal, _, ) }, - [_, videoDispatch, agent, currentDid], + [_, agent, currentDid], ) // Whenever we receive an initial video uri, we should immediately run compression if necessary @@ -235,23 +223,26 @@ export const ComposePost = ({ }, [initVideoUri, selectVideo]) const clearVideo = React.useCallback(() => { - videoUploadState.abortController.abort() + videoState.abortController.abort() dispatch({type: 'embed_remove_video'}) - }, [videoUploadState.abortController, dispatch]) + }, [videoState.abortController, dispatch]) const updateVideoDimensions = useCallback( (width: number, height: number) => { - videoDispatch({ - type: 'update_dimensions', - width, - height, - signal: videoUploadState.abortController.signal, + dispatch({ + type: 'embed_update_video', + videoAction: { + type: 'update_dimensions', + width, + height, + signal: videoState.abortController.signal, + }, }) }, - [videoUploadState.abortController, videoDispatch], + [videoState.abortController], ) - const hasVideo = Boolean(videoUploadState.asset || videoUploadState.video) + const hasVideo = Boolean(videoState.asset || videoState.video) const [publishOnUpload, setPublishOnUpload] = useState(false) @@ -288,7 +279,7 @@ export const ComposePost = ({ graphemeLength > 0 || images.length !== 0 || extGif || - videoUploadState.status !== 'idle' + videoState.status !== 'idle' ) { closeAllDialogs() Keyboard.dismiss() @@ -303,7 +294,7 @@ export const ComposePost = ({ closeAllDialogs, discardPromptControl, onClose, - videoUploadState.status, + videoState.status, ]) useImperativeHandle(cancelRef, () => ({onPressCancel})) @@ -400,8 +391,8 @@ export const ComposePost = ({ if ( !finishedUploading && - videoUploadState.asset && - videoUploadState.status !== 'done' + videoState.asset && + videoState.status !== 'done' ) { setPublishOnUpload(true) return @@ -414,7 +405,7 @@ export const ComposePost = ({ images.length === 0 && !extLink && !quote && - videoUploadState.status === 'idle' + videoState.status === 'idle' ) { setError(_(msg`Did you want to say anything?`)) return @@ -442,14 +433,14 @@ export const ComposePost = ({ onStateChange: setProcessingState, langs: toPostLanguages(langPrefs.postLanguage), video: - videoUploadState.status === 'done' + videoState.status === 'done' ? { - blobRef: videoUploadState.pendingPublish.blobRef, + blobRef: videoState.pendingPublish.blobRef, altText: videoAltText, captions: captions, aspectRatio: { - width: videoUploadState.asset.width, - height: videoUploadState.asset.height, + width: videoState.asset.width, + height: videoState.asset.height, }, } : undefined, @@ -550,20 +541,20 @@ export const ComposePost = ({ setLangPrefs, threadgateAllowUISettings, videoAltText, - videoUploadState.asset, - videoUploadState.pendingPublish, - videoUploadState.status, + videoState.asset, + videoState.pendingPublish, + videoState.status, ], ) React.useEffect(() => { - if (videoUploadState.pendingPublish && publishOnUpload) { - if (!videoUploadState.pendingPublish.mutableProcessed) { - videoUploadState.pendingPublish.mutableProcessed = true + if (videoState.pendingPublish && publishOnUpload) { + if (!videoState.pendingPublish.mutableProcessed) { + videoState.pendingPublish.mutableProcessed = true onPressPublish(true) } } - }, [onPressPublish, publishOnUpload, videoUploadState.pendingPublish]) + }, [onPressPublish, publishOnUpload, videoState.pendingPublish]) const canPost = useMemo( () => graphemeLength <= MAX_GRAPHEME_LENGTH && !isAltTextRequiredAndMissing, @@ -576,10 +567,10 @@ export const ComposePost = ({ const canSelectImages = images.length < MAX_IMAGES && !extLink && - videoUploadState.status === 'idle' && - !videoUploadState.video + videoState.status === 'idle' && + !videoState.video const hasMedia = - images.length > 0 || Boolean(extLink) || Boolean(videoUploadState.video) + images.length > 0 || Boolean(extLink) || Boolean(videoState.video) const onEmojiButtonPress = useCallback(() => { openEmojiPicker?.(textInput.current?.getCursorPosition()) @@ -694,9 +685,7 @@ export const ComposePost = ({ size="small" style={[a.rounded_full, a.py_sm]} onPress={() => onPressPublish()} - disabled={ - videoUploadState.status !== 'idle' && publishOnUpload - }> + disabled={videoState.status !== 'idle' && publishOnUpload}> {replyTo ? ( Reply @@ -732,7 +721,7 @@ export const ComposePost = ({ )} setError('')} clearVideo={clearVideo} /> @@ -798,17 +787,17 @@ export const ComposePost = ({ style={[a.w_full, a.mt_lg]} entering={native(ZoomIn)} exiting={native(ZoomOut)}> - {videoUploadState.asset && - (videoUploadState.status === 'compressing' ? ( + {videoState.asset && + (videoState.status === 'compressing' ? ( - ) : videoUploadState.video ? ( + ) : videoState.video ? ( @@ -854,9 +843,8 @@ export const ComposePost = ({ t.atoms.border_contrast_medium, styles.bottomBar, ]}> - {videoUploadState.status !== 'idle' && - videoUploadState.status !== 'done' ? ( - + {videoState.status !== 'idle' && videoState.status !== 'done' ? ( + ) : ( void clearVideo: () => void }) { @@ -1134,7 +1122,7 @@ function ErrorBanner({ const {_} = useLingui() const videoError = - videoUploadState.status === 'error' ? videoUploadState.error : undefined + videoState.status === 'error' ? videoState.error : undefined const error = standardError || videoError const onClearError = () => { @@ -1176,7 +1164,7 @@ function ErrorBanner({ - {videoError && videoUploadState.jobId && ( + {videoError && videoState.jobId && ( - Job ID: {videoUploadState.jobId} + Job ID: {videoState.jobId} )} @@ -1211,7 +1199,7 @@ function ToolbarWrapper({ ) } -function VideoUploadToolbar({state}: {state: VideoUploadState}) { +function VideoUploadToolbar({state}: {state: VideoState}) { const t = useTheme() const {_} = useLingui() const progress = state.progress diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 5692f3d2c9..5ff7042bc1 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -21,7 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery' import {Text} from '#/view/com/util/text/Text' import {useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' -import {ComposerAction} from '../state' +import {ComposerAction} from '../state/composer' import {EditImageDialog} from './EditImageDialog' import {ImageAltTextDialog} from './ImageAltTextDialog' diff --git a/src/view/com/composer/state.ts b/src/view/com/composer/state/composer.ts similarity index 97% rename from src/view/com/composer/state.ts rename to src/view/com/composer/state/composer.ts index 8e974ad7a5..a23a5d8c86 100644 --- a/src/view/com/composer/state.ts +++ b/src/view/com/composer/state/composer.ts @@ -1,13 +1,8 @@ import {ImagePickerAsset} from 'expo-image-picker' import {ComposerImage, createInitialImages} from '#/state/gallery' -import { - createVideoState, - VideoAction, - videoReducer, - VideoState, -} from '#/state/queries/video/video' import {ComposerOpts} from '#/state/shell/composer' +import {createVideoState, VideoAction, videoReducer, VideoState} from './video' type PostRecord = { uri: string diff --git a/src/state/queries/video/video.ts b/src/view/com/composer/state/video.ts similarity index 98% rename from src/state/queries/video/video.ts rename to src/view/com/composer/state/video.ts index dbbb6c2026..2695056579 100644 --- a/src/state/queries/video/video.ts +++ b/src/view/com/composer/state/video.ts @@ -4,6 +4,8 @@ import {JobStatus} from '@atproto/api/dist/client/types/app/bsky/video/defs' import {I18n} from '@lingui/core' import {msg} from '@lingui/macro' +import {createVideoAgent} from '#/lib/media/video/util' +import {uploadVideo} from '#/lib/media/video/upload' import {AbortError} from '#/lib/async/cancelable' import {compressVideo} from '#/lib/media/video/compress' import { @@ -13,8 +15,6 @@ import { } from '#/lib/media/video/errors' import {CompressedVideo} from '#/lib/media/video/types' import {logger} from '#/logger' -import {createVideoAgent} from '#/state/queries/video/util' -import {uploadVideo} from '#/state/queries/video/video-upload' export type VideoAction = | { From 523a9a48c21ac1aade3d703703f0e59ffeac482e Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 3 Oct 2024 17:28:13 +0300 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=AA=B5=F0=9F=93=8C=20(#5594)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/statsig/events.ts | 2 ++ src/view/com/util/forms/PostDropdownBtn.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index c9bc8fefb2..9a306ee4f4 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -145,6 +145,8 @@ export type LogEvents = { } 'post:mute': {} 'post:unmute': {} + 'post:pin': {} + 'post:unpin': {} 'profile:follow:sampled': { didBecomeMutual: boolean | undefined followeeClout: number | undefined diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index fe6efc02fa..33287564a7 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -22,6 +22,7 @@ import {getCurrentRoute} from '#/lib/routes/helpers' import {makeProfileLink} from '#/lib/routes/links' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {shareUrl} from '#/lib/sharing' +import {logEvent} from '#/lib/statsig/statsig' import {richTextToString} from '#/lib/strings/rich-text-helpers' import {toShareUrl} from '#/lib/strings/url-helpers' import {useTheme} from '#/lib/ThemeContext' @@ -350,6 +351,7 @@ let PostDropdownBtn = ({ ]) const onPressPin = useCallback(() => { + logEvent(isPinned ? 'post:unpin' : 'post:pin', {}) pinPostMutate({ postUri, postCid, From 7e79c7f768e40ef192decfeac0dfac63c3d37468 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 3 Oct 2024 18:19:38 +0300 Subject: [PATCH 3/4] =?UTF-8?q?[=F0=9F=90=B4]=20Reduce=20amount=20that=20m?= =?UTF-8?q?essage=20sent=20date=20is=20shown=20(#4228)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/dms/DateDivider.tsx | 80 +++++++++++++ src/components/dms/MessageItem.tsx | 178 ++++++++++++++-------------- src/components/dms/ReportDialog.tsx | 1 + src/components/dms/util.ts | 9 ++ src/state/messages/convo/agent.ts | 23 +++- src/state/messages/convo/types.ts | 12 ++ 6 files changed, 209 insertions(+), 94 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..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' From eb3b01d0adc5d5c053e1d350c087941b56106497 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 3 Oct 2024 10:45:01 -0500 Subject: [PATCH 4/4] Fix profile header buttons (#5558) * Fix profile header buttons * Adjust labeler buttons too * Fix load state jumps * Small tweak for web * Remove log --- src/components/Button.tsx | 10 ++--- src/components/dms/MessageProfileButton.tsx | 26 +++++-------- .../Profile/Header/ProfileHeaderLabeler.tsx | 14 +++---- .../Profile/Header/ProfileHeaderStandard.tsx | 9 +++-- src/screens/Profile/Header/index.tsx | 7 ++-- src/view/com/profile/ProfileMenu.tsx | 39 +++++++------------ 6 files changed, 42 insertions(+), 63 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 17179994a9..1c14b48c75 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -14,7 +14,7 @@ import { } from 'react-native' import {LinearGradient} from 'expo-linear-gradient' -import {atoms as a, flatten, select, tokens, useTheme, web} from '#/alf' +import {atoms as a, flatten, select, tokens, useTheme} from '#/alf' import {Props as SVGIconProps} from '#/components/icons/common' import {Text} from '#/components/Typography' @@ -352,7 +352,7 @@ export const Button = React.forwardRef( }) } else if (size === 'small') { baseStyles.push({ - paddingVertical: 8, + paddingVertical: 9, paddingHorizontal: 12, borderRadius: 6, gap: 6, @@ -374,7 +374,7 @@ export const Button = React.forwardRef( } } else if (size === 'small') { if (shape === 'round') { - baseStyles.push({height: 36, width: 36}) + baseStyles.push({height: 34, width: 34}) } else { baseStyles.push({height: 34, width: 34}) } @@ -627,9 +627,9 @@ export function useSharedButtonTextStyles() { } if (size === 'large') { - baseStyles.push(a.text_md, a.leading_tight, web({top: -0.4})) + baseStyles.push(a.text_md, a.leading_tight) } else if (size === 'small') { - baseStyles.push(a.text_sm, a.leading_tight, web({top: -0.4})) + baseStyles.push(a.text_sm, a.leading_tight) } else if (size === 'tiny') { baseStyles.push(a.text_xs, a.leading_tight) } diff --git a/src/components/dms/MessageProfileButton.tsx b/src/components/dms/MessageProfileButton.tsx index 7f440d621f..932982d05e 100644 --- a/src/components/dms/MessageProfileButton.tsx +++ b/src/components/dms/MessageProfileButton.tsx @@ -4,12 +4,13 @@ import {AppBskyActorDefs} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {logEvent} from '#/lib/statsig/statsig' import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members' -import {logEvent} from 'lib/statsig/statsig' import {atoms as a, useTheme} from '#/alf' -import {Message_Stroke2_Corner0_Rounded as Message} from '../icons/Message' -import {Link} from '../Link' -import {canBeMessaged} from './util' +import {ButtonIcon} from '#/components/Button' +import {canBeMessaged} from '#/components/dms/util' +import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message' +import {Link} from '#/components/Link' export function MessageProfileButton({ profile, @@ -40,15 +41,9 @@ export function MessageProfileButton({ a.align_center, t.atoms.bg_contrast_25, a.rounded_full, - {width: 36, height: 36}, + {width: 34, height: 34}, ]}> - + ) } else { @@ -66,12 +61,9 @@ export function MessageProfileButton({ shape="round" label={_(msg`Message ${profile.handle}`)} to={`/messages/${convo.id}`} - style={[a.justify_center, {width: 36, height: 36}]} + style={[a.justify_center]} onPress={onPress}> - + ) } else { diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index 7b44e58693..8c95413a8c 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -25,7 +25,7 @@ import {usePreferencesQuery} from '#/state/queries/preferences' import {useRequireAuth, useSession} from '#/state/session' import {ProfileMenu} from '#/view/com/profile/ProfileMenu' import * as Toast from '#/view/com/util/Toast' -import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf' +import {atoms as a, tokens, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import {DialogOuterProps} from '#/components/Dialog' import { @@ -61,7 +61,6 @@ let ProfileHeaderLabeler = ({ const profile: Shadow = useProfileShadow(profileUnshadowed) const t = useTheme() - const {gtMobile} = useBreakpoints() const {_} = useLingui() const {currentAccount, hasSession} = useSession() const {openModal} = useModalControls() @@ -167,7 +166,7 @@ let ProfileHeaderLabeler = ({ style={[a.px_lg, a.pt_md, a.pb_sm]} pointerEvents={isIOS ? 'auto' : 'box-none'}> {isMe ? ( ) }}