diff --git a/assets/icons/arrowCornerDownRight_stroke2_rounded_2_rounded.svg b/assets/icons/arrowCornerDownRight_stroke2_corner2_rounded.svg similarity index 100% rename from assets/icons/arrowCornerDownRight_stroke2_rounded_2_rounded.svg rename to assets/icons/arrowCornerDownRight_stroke2_corner2_rounded.svg diff --git a/assets/icons/arrowCornerDownRight_stroke2_corner3_rounded.svg b/assets/icons/arrowCornerDownRight_stroke2_corner3_rounded.svg new file mode 100644 index 0000000000..ff7edc539e --- /dev/null +++ b/assets/icons/arrowCornerDownRight_stroke2_corner3_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index 215fb2fffd..a71b28af53 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -46,7 +46,7 @@ import {isOnlyEmoji} from '#/alf/typography' import {Button} from '#/components/Button' import {ActionsWrapper} from '#/components/dms/ActionsWrapper' import {useMessageDialogs} from '#/components/dms/MessageOverlays' -import {ArrowCornerDownRight_Stroke2_Corner2_Rounded as ArrowCornerDownRightIcon} from '#/components/icons/ArrowCornerDownRight' +import {ArrowCornerDownRight_Stroke2_Corner3_Rounded as ArrowCornerDownRightIcon} from '#/components/icons/ArrowCornerDownRight' import {InlineLinkText} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' import * as Prompt from '#/components/Prompt' @@ -64,28 +64,48 @@ import { const AVATAR_SIZE = 28 const CLUSTERED_MESSAGE_GAP = 2 -const BORDER_RADIUS = 18 +const BORDER_RADIUS = 20 const SQUARED_BORDER_RADIUS = 4 const DISPLAY_NAME_INSET = 20 +function messageIsReply( + message: + | ChatBskyConvoDefs.MessageView + | ChatBskyConvoDefs.DeletedMessageView + | null, +): boolean { + return ( + ChatBskyConvoDefs.isMessageView(message) && + (ChatBskyConvoDefs.isMessageView(message.replyTo) || + ChatBskyConvoDefs.isDeletedMessageView(message.replyTo)) + ) +} + function isWithinClusterBoundary({ isPending, + message, adjacentMessage, isFromSameSender, - currentSentAt, direction, }: { isPending: boolean + message: ChatBskyConvoDefs.MessageView adjacentMessage: | ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.DeletedMessageView | null isFromSameSender: boolean - currentSentAt: string direction: 'prev' | 'next' }): boolean { + // A reply always starts its own cluster, breaking grouping with the message + // above it. Looking back, that's a boundary if this message is a reply; + // looking forward, it's a boundary if the next message is a reply. + if (messageIsReply(direction === 'prev' ? message : adjacentMessage)) { + return true + } if (!isFromSameSender) return true if (ChatBskyConvoDefs.isMessageView(adjacentMessage)) { + const currentSentAt = message.sentAt const thisDate = new Date(currentSentAt) const adjDate = new Date(adjacentMessage.sentAt) const diff = @@ -160,17 +180,17 @@ let MessageItem = ({ const isFirstInCluster = isWithinClusterBoundary({ isPending, + message, adjacentMessage: prevMessage, isFromSameSender: isPrevFromSameSender, - currentSentAt: message.sentAt, direction: 'prev', }) const isLastInCluster = isWithinClusterBoundary({ isPending, + message, adjacentMessage: nextMessage, isFromSameSender: isNextFromSameSender, - currentSentAt: message.sentAt, direction: 'next', }) @@ -401,7 +421,15 @@ let MessageItem = ({ a.flex_grow, !isFromSelf && isGroupChat && {paddingLeft: AVATAR_SIZE}, ]}> - {displayName && showDisplayName ? ( + {replyTo ? ( + scrollToMessage(replyTo.id)} + /> + ) : displayName && showDisplayName ? ( ) : null} - {replyTo ? ( - - ) : null} {profile && isBlockedOrBlocking(profile) && isGroupChat ? ( ) : ( @@ -676,21 +696,28 @@ function BlockedPlaceholder({ } /** - * The "↪ X replied to Y" caption rendered above a reply message. `X` is the - * person sending the reply (self -> "you"), `Y` is the original sender. + * The "↪ X replied to Y" caption rendered above a reply message, in place of + * the display name. `X` is the person sending the reply (self -> "you"), `Y` is + * the original sender. Tapping it scrolls to the original (if loaded). + * + * Aligns with the sender's display name for others (left), or with the message + * bubble for self (right). */ function ReplyCaption({ replyTo, isFromSelf, replierDisplayName, relatedProfiles, + onPress, }: { replyTo: ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.DeletedMessageView isFromSelf: boolean replierDisplayName: string | null relatedProfiles: Map + onPress: () => void }) { const t = useTheme() + const {t: l} = useLingui() const {currentAccount} = useSession() const originalSenderIsSelf = replyTo.sender.did === currentAccount?.did @@ -702,15 +729,28 @@ function ReplyCaption({ : null return ( - + ) } @@ -793,7 +833,7 @@ function ReplyQuote({ onPress={onPress} style={[ a.mb_xs, - a.w_full, + a.flex_1, a.gap_2xs, a.rounded_md, a.px_sm, diff --git a/src/components/icons/ArrowCornerDownRight.tsx b/src/components/icons/ArrowCornerDownRight.tsx index 86dde70154..85346f270d 100644 --- a/src/components/icons/ArrowCornerDownRight.tsx +++ b/src/components/icons/ArrowCornerDownRight.tsx @@ -5,3 +5,9 @@ export const ArrowCornerDownRight_Stroke2_Corner2_Rounded = createSinglePathSVG( path: 'M15.793 10.293a1 1 0 0 1 1.338-.068l.076.068 3.293 3.293a2 2 0 0 1 .138 2.677l-.138.151-3.293 3.293a1 1 0 1 1-1.414-1.414L18.086 16H8a5 5 0 0 1-5-5V5a1 1 0 0 1 2 0v6a3 3 0 0 0 3 3h10.086l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z', }, ) + +export const ArrowCornerDownRight_Stroke2_Corner3_Rounded = createSinglePathSVG( + { + path: 'M5 5a1 1 0 0 0-2 0v4a7 7 0 0 0 7 7h8.086l-2.293 2.293a1 1 0 0 0 1.414 1.414l2.94-2.94a2.5 2.5 0 0 0 0-3.535l-2.94-2.94a1 1 0 1 0-1.414 1.415L18.086 14H10a5 5 0 0 1-5-5V5Z', + }, +)