Reveal chat timestamp on tap (#10262)
This commit is contained in:
@@ -235,7 +235,13 @@ export function Root({children}: {children: React.ReactNode}) {
|
|||||||
return <Context.Provider value={context}>{children}</Context.Provider>
|
return <Context.Provider value={context}>{children}</Context.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Trigger({children, label, contentLabel, style}: TriggerProps) {
|
export function Trigger({
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
contentLabel,
|
||||||
|
style,
|
||||||
|
onTap,
|
||||||
|
}: TriggerProps) {
|
||||||
const context = useContextMenuContext()
|
const context = useContextMenuContext()
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
@@ -294,6 +300,17 @@ export function Trigger({children, label, contentLabel, style}: TriggerProps) {
|
|||||||
}
|
}
|
||||||
}, [context, insets])
|
}, [context, insets])
|
||||||
|
|
||||||
|
const tapGesture = useMemo(() => {
|
||||||
|
const gesture = Gesture.Tap()
|
||||||
|
.numberOfTaps(1)
|
||||||
|
.cancelsTouchesInView(false)
|
||||||
|
.runOnJS(true)
|
||||||
|
if (onTap) {
|
||||||
|
gesture.onEnd(() => void onTap())
|
||||||
|
}
|
||||||
|
return gesture
|
||||||
|
}, [onTap])
|
||||||
|
|
||||||
const doubleTapGesture = useMemo(() => {
|
const doubleTapGesture = useMemo(() => {
|
||||||
return Gesture.Tap()
|
return Gesture.Tap()
|
||||||
.numberOfTaps(2)
|
.numberOfTaps(2)
|
||||||
@@ -346,8 +363,10 @@ export function Trigger({children, label, contentLabel, style}: TriggerProps) {
|
|||||||
})
|
})
|
||||||
}, [open, hoverablesSV, onTouchUpMenuItem, hoveredItemSV, translationSV])
|
}, [open, hoverablesSV, onTouchUpMenuItem, hoveredItemSV, translationSV])
|
||||||
|
|
||||||
|
// Order matters here: doubleTapGesture must come before tapGesture.
|
||||||
const composedGestures = Gesture.Exclusive(
|
const composedGestures = Gesture.Exclusive(
|
||||||
doubleTapGesture,
|
doubleTapGesture,
|
||||||
|
tapGesture,
|
||||||
pressAndHoldGesture,
|
pressAndHoldGesture,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,14 @@ export type TriggerProps = {
|
|||||||
hint?: string
|
hint?: string
|
||||||
role?: AccessibilityRole
|
role?: AccessibilityRole
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
|
/**
|
||||||
|
* Callback for single taps. Composed with the double-tap and
|
||||||
|
* press-and-hold gestures via `Gesture.Exclusive`, so a double tap
|
||||||
|
* does not also fire this handler.
|
||||||
|
*
|
||||||
|
* @platform ios, android
|
||||||
|
*/
|
||||||
|
onTap?: () => void
|
||||||
}
|
}
|
||||||
export type TriggerChildProps =
|
export type TriggerChildProps =
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -9,15 +9,18 @@ export function ActionsWrapper({
|
|||||||
message,
|
message,
|
||||||
isFromSelf,
|
isFromSelf,
|
||||||
children,
|
children,
|
||||||
|
onTap,
|
||||||
}: {
|
}: {
|
||||||
message: ChatBskyConvoDefs.MessageView
|
message: ChatBskyConvoDefs.MessageView
|
||||||
|
hasReactions?: boolean
|
||||||
isFromSelf: boolean
|
isFromSelf: boolean
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
onTap?: () => void
|
||||||
}) {
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageContextMenu message={message}>
|
<MessageContextMenu message={message} onTap={onTap}>
|
||||||
{trigger =>
|
{trigger =>
|
||||||
// will always be true, since this file is platform split
|
// will always be true, since this file is platform split
|
||||||
trigger.IS_NATIVE && (
|
trigger.IS_NATIVE && (
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import {useCallback, useRef, useState} from 'react'
|
import {useCallback, useRef, useState} from 'react'
|
||||||
import {Pressable, View} from 'react-native'
|
import {Pressable, View} from 'react-native'
|
||||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {useConvoActive} from '#/state/messages/convo'
|
import {useConvoActive} from '#/state/messages/convo'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
@@ -16,16 +15,20 @@ import {hasReachedReactionLimit} from './util'
|
|||||||
|
|
||||||
export function ActionsWrapper({
|
export function ActionsWrapper({
|
||||||
message,
|
message,
|
||||||
|
hasReactions,
|
||||||
isFromSelf,
|
isFromSelf,
|
||||||
children,
|
children,
|
||||||
|
onTap,
|
||||||
}: {
|
}: {
|
||||||
message: ChatBskyConvoDefs.MessageView
|
message: ChatBskyConvoDefs.MessageView
|
||||||
|
hasReactions?: boolean
|
||||||
isFromSelf: boolean
|
isFromSelf: boolean
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
onTap?: () => void
|
||||||
}) {
|
}) {
|
||||||
const viewRef = useRef(null)
|
const viewRef = useRef(null)
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const convo = useConvoActive()
|
const convo = useConvoActive()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
@@ -57,17 +60,17 @@ export function ActionsWrapper({
|
|||||||
) {
|
) {
|
||||||
convo
|
convo
|
||||||
.removeReaction(message.id, emoji)
|
.removeReaction(message.id, emoji)
|
||||||
.catch(() => Toast.show(_(msg`Failed to remove emoji reaction`)))
|
.catch(() => Toast.show(l`Failed to remove emoji reaction`))
|
||||||
} else {
|
} else {
|
||||||
if (hasReachedReactionLimit(message, currentAccount?.did)) return
|
if (hasReachedReactionLimit(message, currentAccount?.did)) return
|
||||||
convo.addReaction(message.id, emoji).catch(() =>
|
convo.addReaction(message.id, emoji).catch(() =>
|
||||||
Toast.show(_(msg`Failed to add emoji reaction`), {
|
Toast.show(l`Failed to add emoji reaction`, {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[_, convo, message, currentAccount?.did],
|
[l, convo, message, currentAccount?.did],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -87,6 +90,7 @@ export function ActionsWrapper({
|
|||||||
isFromSelf
|
isFromSelf
|
||||||
? [a.mr_xs, {marginLeft: 'auto'}, a.flex_row_reverse]
|
? [a.mr_xs, {marginLeft: 'auto'}, a.flex_row_reverse]
|
||||||
: [a.ml_xs, {marginRight: 'auto'}],
|
: [a.ml_xs, {marginRight: 'auto'}],
|
||||||
|
hasReactions ? [a.mb_2xl] : undefined,
|
||||||
]}>
|
]}>
|
||||||
<EmojiReactionPicker message={message} onEmojiSelect={onEmojiSelect}>
|
<EmojiReactionPicker message={message} onEmojiSelect={onEmojiSelect}>
|
||||||
{({props, state, IS_NATIVE, control}) => {
|
{({props, state, IS_NATIVE, control}) => {
|
||||||
@@ -133,10 +137,13 @@ export function ActionsWrapper({
|
|||||||
}}
|
}}
|
||||||
</MessageContextMenu>
|
</MessageContextMenu>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityHint={l`Click to view the date and time`}
|
||||||
|
onPress={onTap}
|
||||||
style={[{maxWidth: '80%'}, isFromSelf ? a.align_end : a.align_start]}>
|
style={[{maxWidth: '80%'}, isFromSelf ? a.align_end : a.align_start]}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ const longDateFormatterWithYear = new Intl.DateTimeFormat(undefined, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let DateDivider = ({date: dateStr}: {date: string}): React.ReactNode => {
|
let DateDivider = ({date: dateStr}: {date: string}): React.ReactNode => {
|
||||||
const {t: l} = useLingui()
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
let date: string
|
let date: string
|
||||||
const time = timeFormatter.format(new Date(dateStr))
|
const time = timeFormatter.format(new Date(dateStr))
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import {createContext, useCallback, useContext, useState} from 'react'
|
||||||
|
|
||||||
|
type DateDividerToggleContextType = {
|
||||||
|
isDividerToggled: (id: string) => boolean
|
||||||
|
toggleDivider: (id: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DateDividerToggleContext = createContext<DateDividerToggleContextType>({
|
||||||
|
isDividerToggled: () => false,
|
||||||
|
toggleDivider: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function DateDividerToggleProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const [toggledIds, setToggledIds] = useState(new Set<string>())
|
||||||
|
|
||||||
|
const toggleDivider = useCallback((id: string) => {
|
||||||
|
setToggledIds(prev => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (next.has(id)) next.delete(id)
|
||||||
|
else next.add(id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const isDividerToggled = useCallback(
|
||||||
|
(id: string) => toggledIds.has(id),
|
||||||
|
[toggledIds],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DateDividerToggleContext.Provider
|
||||||
|
value={{isDividerToggled, toggleDivider}}>
|
||||||
|
{children}
|
||||||
|
</DateDividerToggleContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDateDividerToggle() {
|
||||||
|
return useContext(DateDividerToggleContext)
|
||||||
|
}
|
||||||
@@ -31,9 +31,11 @@ import {hasReachedReactionLimit} from './util'
|
|||||||
export let MessageContextMenu = ({
|
export let MessageContextMenu = ({
|
||||||
message,
|
message,
|
||||||
children,
|
children,
|
||||||
|
onTap,
|
||||||
}: {
|
}: {
|
||||||
message: ChatBskyConvoDefs.MessageView
|
message: ChatBskyConvoDefs.MessageView
|
||||||
children: TriggerProps['children']
|
children: TriggerProps['children']
|
||||||
|
onTap?: () => void
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
@@ -130,7 +132,8 @@ export let MessageContextMenu = ({
|
|||||||
label={l`Message options`}
|
label={l`Message options`}
|
||||||
contentLabel={l`Message from @${
|
contentLabel={l`Message from @${
|
||||||
sender?.handle ?? 'unknown' // should always be defined
|
sender?.handle ?? 'unknown' // should always be defined
|
||||||
}: ${message.text}`}>
|
}: ${message.text}`}
|
||||||
|
onTap={onTap}>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenu.Trigger>
|
</ContextMenu.Trigger>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {memo, useCallback, useMemo, useState} from 'react'
|
import {memo, useCallback, useEffect, useMemo, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
type GestureResponderEvent,
|
type GestureResponderEvent,
|
||||||
|
LayoutAnimation,
|
||||||
Pressable,
|
Pressable,
|
||||||
type StyleProp,
|
type StyleProp,
|
||||||
type TextStyle,
|
type TextStyle,
|
||||||
@@ -11,7 +12,9 @@ import Animated, {
|
|||||||
FadeOut,
|
FadeOut,
|
||||||
LayoutAnimationConfig,
|
LayoutAnimationConfig,
|
||||||
LinearTransition,
|
LinearTransition,
|
||||||
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
|
withTiming,
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
@@ -43,6 +46,7 @@ import {RichText} from '#/components/RichText'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
import {DateDivider} from './DateDivider'
|
import {DateDivider} from './DateDivider'
|
||||||
|
import {useDateDividerToggle} from './DateDividerToggle'
|
||||||
import {MessageItemEmbed} from './MessageItemEmbed'
|
import {MessageItemEmbed} from './MessageItemEmbed'
|
||||||
|
|
||||||
const AVATAR_SIZE = 28
|
const AVATAR_SIZE = 28
|
||||||
@@ -158,17 +162,25 @@ let MessageItem = ({
|
|||||||
new Date(prevMessage.sentAt).getTime() >
|
new Date(prevMessage.sentAt).getTime() >
|
||||||
MESSAGE_GAP_THRESHOLD_MS
|
MESSAGE_GAP_THRESHOLD_MS
|
||||||
|
|
||||||
|
const {isDividerToggled, toggleDivider} = useDateDividerToggle()
|
||||||
|
const isDateDividerToggled = isDividerToggled(message.id)
|
||||||
|
const isNextDateDividerToggled =
|
||||||
|
nextMessage != null && isDividerToggled(nextMessage.id)
|
||||||
const showDateDivider = hasLargeGapFromPrev
|
const showDateDivider = hasLargeGapFromPrev
|
||||||
|
|
||||||
const isInCluster = !(isFirstInCluster && isLastInCluster)
|
const effectiveFirstInCluster = isFirstInCluster || isDateDividerToggled
|
||||||
|
const effectiveLastInCluster = isLastInCluster || isNextDateDividerToggled
|
||||||
|
const isInCluster = !(effectiveFirstInCluster && effectiveLastInCluster)
|
||||||
const isInMiddleOfCluster =
|
const isInMiddleOfCluster =
|
||||||
isInCluster && !isFirstInCluster && !isLastInCluster
|
isInCluster && !effectiveFirstInCluster && !effectiveLastInCluster
|
||||||
|
|
||||||
const hasReactions = message.reactions && message.reactions.length > 0
|
const hasReactions = message.reactions && message.reactions.length > 0
|
||||||
const squaredBottomCorner =
|
const squaredBottomCorner =
|
||||||
!hasReactions && isInCluster && (isInMiddleOfCluster || isFirstInCluster)
|
!hasReactions &&
|
||||||
|
isInCluster &&
|
||||||
|
(isInMiddleOfCluster || effectiveFirstInCluster)
|
||||||
const squaredTopCorner =
|
const squaredTopCorner =
|
||||||
isInCluster && (isInMiddleOfCluster || isLastInCluster)
|
isInCluster && (isInMiddleOfCluster || effectiveLastInCluster)
|
||||||
|
|
||||||
const pendingColor = t.palette.primary_300
|
const pendingColor = t.palette.primary_300
|
||||||
|
|
||||||
@@ -179,6 +191,45 @@ let MessageItem = ({
|
|||||||
const hasEmbedAndText =
|
const hasEmbedAndText =
|
||||||
AppBskyEmbedRecord.isView(message.embed) && rt.text.length > 0
|
AppBskyEmbedRecord.isView(message.embed) && rt.text.length > 0
|
||||||
|
|
||||||
|
const targetBottomRadius =
|
||||||
|
squaredBottomCorner || hasEmbedAndText
|
||||||
|
? SQUARED_BORDER_RADIUS
|
||||||
|
: BORDER_RADIUS
|
||||||
|
const targetTopRadius = squaredTopCorner
|
||||||
|
? SQUARED_BORDER_RADIUS
|
||||||
|
: BORDER_RADIUS
|
||||||
|
|
||||||
|
const bottomRadiusSV = useSharedValue(targetBottomRadius)
|
||||||
|
const topRadiusSV = useSharedValue(targetTopRadius)
|
||||||
|
|
||||||
|
const showDisplayName =
|
||||||
|
isGroupChat &&
|
||||||
|
!isFromSelf &&
|
||||||
|
effectiveFirstInCluster &&
|
||||||
|
!isDateDividerToggled &&
|
||||||
|
!isOnlyEmoji(message.text)
|
||||||
|
const showAvatar = isGroupChat && !isFromSelf && isLastInCluster
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
bottomRadiusSV.set(withTiming(targetBottomRadius, {duration: 300}))
|
||||||
|
}, [targetBottomRadius, bottomRadiusSV])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
topRadiusSV.set(withTiming(targetTopRadius, {duration: 300}))
|
||||||
|
}, [targetTopRadius, topRadiusSV])
|
||||||
|
|
||||||
|
const borderRadiusStyle = useAnimatedStyle(() =>
|
||||||
|
isFromSelf
|
||||||
|
? {
|
||||||
|
borderBottomRightRadius: bottomRadiusSV.get(),
|
||||||
|
borderTopRightRadius: topRadiusSV.get(),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
borderBottomLeftRadius: bottomRadiusSV.get(),
|
||||||
|
borderTopLeftRadius: topRadiusSV.get(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const avatar = profile ? (
|
const avatar = profile ? (
|
||||||
<ProfileCard.Avatar
|
<ProfileCard.Avatar
|
||||||
profile={profile}
|
profile={profile}
|
||||||
@@ -322,7 +373,7 @@ let MessageItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showDateDivider && (
|
{(showDateDivider || isDateDividerToggled) && (
|
||||||
<Animated.View entering={native(FadeIn)} exiting={native(FadeOut)}>
|
<Animated.View entering={native(FadeIn)} exiting={native(FadeOut)}>
|
||||||
<DateDivider date={message.sentAt} />
|
<DateDivider date={message.sentAt} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -330,10 +381,12 @@ let MessageItem = ({
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
isFromSelf ? a.mr_sm : a.ml_sm,
|
isFromSelf ? a.mr_sm : a.ml_sm,
|
||||||
isFirstInCluster && !showDateDivider && a.mt_sm,
|
effectiveFirstInCluster &&
|
||||||
|
!(showDateDivider || isDateDividerToggled) &&
|
||||||
|
a.mt_sm,
|
||||||
]}>
|
]}>
|
||||||
<View style={[a.relative]}>
|
<View style={[a.relative]}>
|
||||||
{isGroupChat && !isFromSelf && isLastInCluster ? (
|
{showAvatar ? (
|
||||||
<View style={[a.absolute, {bottom: hasReactions ? 10 : 0}]}>
|
<View style={[a.absolute, {bottom: hasReactions ? 10 : 0}]}>
|
||||||
{avatar}
|
{avatar}
|
||||||
</View>
|
</View>
|
||||||
@@ -346,10 +399,7 @@ let MessageItem = ({
|
|||||||
paddingLeft: AVATAR_SIZE,
|
paddingLeft: AVATAR_SIZE,
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
{isGroupChat &&
|
{showDisplayName ? (
|
||||||
!isFromSelf &&
|
|
||||||
isFirstInCluster &&
|
|
||||||
!isOnlyEmoji(message.text) ? (
|
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.text_xs,
|
a.text_xs,
|
||||||
@@ -363,9 +413,20 @@ let MessageItem = ({
|
|||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<ActionsWrapper isFromSelf={isFromSelf} message={message}>
|
<ActionsWrapper
|
||||||
|
hasReactions={hasReactions}
|
||||||
|
isFromSelf={isFromSelf}
|
||||||
|
message={message}
|
||||||
|
onTap={() => {
|
||||||
|
if (!hasLargeGapFromPrev) {
|
||||||
|
LayoutAnimation.configureNext(
|
||||||
|
LayoutAnimation.Presets.easeInEaseOut,
|
||||||
|
)
|
||||||
|
toggleDivider(message.id)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
{rt.text.length > 0 && (
|
{rt.text.length > 0 && (
|
||||||
<View
|
<Animated.View
|
||||||
accessibilityHint={l`Double tap or long press the message to add a reaction`}
|
accessibilityHint={l`Double tap or long press the message to add a reaction`}
|
||||||
style={[
|
style={[
|
||||||
!isFromSelf && a.ml_sm,
|
!isFromSelf && a.ml_sm,
|
||||||
@@ -377,7 +438,7 @@ let MessageItem = ({
|
|||||||
a.py_sm,
|
a.py_sm,
|
||||||
a.px_md,
|
a.px_md,
|
||||||
{
|
{
|
||||||
marginTop: isFirstInCluster
|
marginTop: effectiveFirstInCluster
|
||||||
? 0
|
? 0
|
||||||
: CLUSTERED_MESSAGE_GAP,
|
: CLUSTERED_MESSAGE_GAP,
|
||||||
backgroundColor: isFromSelf
|
backgroundColor: isFromSelf
|
||||||
@@ -387,25 +448,7 @@ let MessageItem = ({
|
|||||||
: t.palette.contrast_50,
|
: t.palette.contrast_50,
|
||||||
},
|
},
|
||||||
isFromSelf ? a.self_end : a.self_start,
|
isFromSelf ? a.self_end : a.self_start,
|
||||||
isFromSelf
|
borderRadiusStyle,
|
||||||
? {
|
|
||||||
borderBottomRightRadius:
|
|
||||||
squaredBottomCorner || hasEmbedAndText
|
|
||||||
? SQUARED_BORDER_RADIUS
|
|
||||||
: BORDER_RADIUS,
|
|
||||||
borderTopRightRadius: squaredTopCorner
|
|
||||||
? SQUARED_BORDER_RADIUS
|
|
||||||
: BORDER_RADIUS,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
borderBottomLeftRadius:
|
|
||||||
squaredBottomCorner || hasEmbedAndText
|
|
||||||
? SQUARED_BORDER_RADIUS
|
|
||||||
: BORDER_RADIUS,
|
|
||||||
borderTopLeftRadius: squaredTopCorner
|
|
||||||
? SQUARED_BORDER_RADIUS
|
|
||||||
: BORDER_RADIUS,
|
|
||||||
},
|
|
||||||
]),
|
]),
|
||||||
]}>
|
]}>
|
||||||
<RichText
|
<RichText
|
||||||
@@ -416,7 +459,7 @@ let MessageItem = ({
|
|||||||
emojiMultiplier={3}
|
emojiMultiplier={3}
|
||||||
shouldProxyLinks={true}
|
shouldProxyLinks={true}
|
||||||
/>
|
/>
|
||||||
</View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
{AppBskyEmbedRecord.isView(message.embed) && (
|
{AppBskyEmbedRecord.isView(message.embed) && (
|
||||||
<MessageItemEmbed
|
<MessageItemEmbed
|
||||||
@@ -430,7 +473,7 @@ let MessageItem = ({
|
|||||||
</ActionsWrapper>
|
</ActionsWrapper>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{isLastInCluster && (
|
{effectiveLastInCluster && (
|
||||||
<MessageItemMetadata
|
<MessageItemMetadata
|
||||||
item={item}
|
item={item}
|
||||||
style={[isFromSelf ? a.text_right : a.text_left]}
|
style={[isFromSelf ? a.text_right : a.text_left]}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import {MessageInput} from '#/screens/Messages/components/MessageInput'
|
|||||||
import {MessageListError} from '#/screens/Messages/components/MessageListError'
|
import {MessageListError} from '#/screens/Messages/components/MessageListError'
|
||||||
import {atoms as a, platform, tokens, useTheme, web} from '#/alf'
|
import {atoms as a, platform, tokens, useTheme, web} from '#/alf'
|
||||||
import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
|
import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
|
||||||
|
import {DateDividerToggleProvider} from '#/components/dms/DateDividerToggle'
|
||||||
import {MessageItem} from '#/components/dms/MessageItem'
|
import {MessageItem} from '#/components/dms/MessageItem'
|
||||||
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
@@ -417,7 +418,7 @@ export function MessagesList({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DateDividerToggleProvider>
|
||||||
<KeyboardGestureArea
|
<KeyboardGestureArea
|
||||||
interpolator="ios"
|
interpolator="ios"
|
||||||
// HACKFIX: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1419
|
// HACKFIX: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1419
|
||||||
@@ -528,7 +529,7 @@ export function MessagesList({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{newMessagesPill.show && <NewMessagesPill onPress={scrollToEndOnPress} />}
|
{newMessagesPill.show && <NewMessagesPill onPress={scrollToEndOnPress} />}
|
||||||
</>
|
</DateDividerToggleProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user