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',
+ },
+)