Group runs of system messages under a single toggle (#10388)
This commit is contained in:
@@ -45,6 +45,7 @@ import {Text} from '#/components/Typography'
|
||||
import {DateDivider} from './DateDivider'
|
||||
import {MessageItemEmbed} from './MessageItemEmbed'
|
||||
import {ReactionsDialog} from './ReactionsDialog'
|
||||
import {CLUSTERED_MESSAGE_THRESHOLD_MS, MESSAGE_GAP_THRESHOLD_MS} from './util'
|
||||
|
||||
const AVATAR_SIZE = 28
|
||||
const CLUSTERED_MESSAGE_GAP = 2
|
||||
@@ -52,9 +53,6 @@ const BORDER_RADIUS = 18
|
||||
const SQUARED_BORDER_RADIUS = 4
|
||||
const DISPLAY_NAME_INSET = 22
|
||||
|
||||
const CLUSTERED_MESSAGE_THRESHOLD_MS = 5 * 60 * 1000
|
||||
const MESSAGE_GAP_THRESHOLD_MS = 60 * 60 * 1000
|
||||
|
||||
function isWithinClusterBoundary({
|
||||
isPending,
|
||||
adjacentMessage,
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import {Pressable, View} from 'react-native'
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
LinearTransition,
|
||||
useAnimatedStyle,
|
||||
useDerivedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {type ChatBskyActorDefs} from '@atproto/api'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {type SystemMessageGroupItem} from '#/screens/Messages/components/groupSystemMessages'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {SystemMessageItem} from '#/components/dms/SystemMessageItem'
|
||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const ANIMATION_DURATION_MS = 200
|
||||
|
||||
export function SystemMessageGroup({
|
||||
item,
|
||||
expanded,
|
||||
onToggle,
|
||||
relatedProfiles,
|
||||
}: {
|
||||
item: SystemMessageGroupItem
|
||||
expanded: boolean
|
||||
onToggle: (key: string) => void
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const count = item.items.length
|
||||
|
||||
const label = plural(count, {
|
||||
one: '# chat update',
|
||||
other: '# chat updates',
|
||||
})
|
||||
|
||||
const rotation = useDerivedValue(() =>
|
||||
withTiming(expanded ? -180 : 0, {duration: ANIMATION_DURATION_MS}),
|
||||
)
|
||||
const chevronStyle = useAnimatedStyle(() => ({
|
||||
transform: [{rotate: `${rotation.get()}deg`}],
|
||||
}))
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Pressable
|
||||
testID="systemMessageGroupToggle"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint={
|
||||
expanded ? l`Hide group chat updates` : l`Show group chat updates`
|
||||
}
|
||||
accessibilityState={{expanded}}
|
||||
hitSlop={HITSLOP_10}
|
||||
onPress={() => onToggle(item.key)}
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.px_md,
|
||||
a.mt_md,
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.text_center,
|
||||
t.atoms.text_contrast_medium,
|
||||
{includeFontPadding: false, textAlignVertical: 'center'},
|
||||
]}>
|
||||
{label}
|
||||
</Text>
|
||||
<Animated.View style={[a.ml_2xs, chevronStyle]}>
|
||||
<ChevronDown size="xs" style={t.atoms.text_contrast_medium} />
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
<Animated.View layout={LinearTransition.duration(ANIMATION_DURATION_MS)}>
|
||||
{expanded
|
||||
? item.items.map(child => (
|
||||
<Animated.View
|
||||
key={child.key}
|
||||
entering={FadeIn.duration(ANIMATION_DURATION_MS)}
|
||||
exiting={FadeOut.duration(ANIMATION_DURATION_MS)}>
|
||||
<SystemMessageItem
|
||||
item={child}
|
||||
relatedProfiles={relatedProfiles}
|
||||
/>
|
||||
</Animated.View>
|
||||
))
|
||||
: null}
|
||||
</Animated.View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -2,9 +2,13 @@ import {View} from 'react-native'
|
||||
import {type ChatBskyActorDefs} from '@atproto/api'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {type ConvoItem} from '#/state/messages/convo/types'
|
||||
import {useInviteLinkDialog} from '#/screens/Messages/components/InviteLinkDialogProvider'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {getSystemMessageInfo} from '#/components/dms/getSystemMessageInfo'
|
||||
import {Link} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function SystemMessageItem({
|
||||
@@ -15,14 +19,16 @@ export function SystemMessageItem({
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {i18n} = useLingui()
|
||||
const {i18n, t: l} = useLingui()
|
||||
const inviteLinkControl = useInviteLinkDialog()
|
||||
|
||||
const info = getSystemMessageInfo(item.message.data, relatedProfiles)
|
||||
if (!info) return null
|
||||
|
||||
const {Icon, message} = info
|
||||
const {Icon, action} = info
|
||||
const text = i18n._(info.message)
|
||||
|
||||
return (
|
||||
const row = (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
@@ -40,8 +46,30 @@ export function SystemMessageItem({
|
||||
t.atoms.text_contrast_medium,
|
||||
{includeFontPadding: false, textAlignVertical: 'center'},
|
||||
]}>
|
||||
{i18n._(message)}
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
||||
switch (action?.kind) {
|
||||
case 'profile':
|
||||
return (
|
||||
<Link
|
||||
to={makeProfileLink(action.profile)}
|
||||
label={text}
|
||||
accessibilityHint={l`Opens profile`}
|
||||
style={a.w_full}>
|
||||
{row}
|
||||
</Link>
|
||||
)
|
||||
case 'inviteLink':
|
||||
if (!inviteLinkControl) return row
|
||||
return (
|
||||
<Button label={text} onPress={inviteLinkControl.open} style={a.w_full}>
|
||||
{row}
|
||||
</Button>
|
||||
)
|
||||
default:
|
||||
return row
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,17 +16,31 @@ import {
|
||||
} from '#/components/icons/Lock'
|
||||
import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil'
|
||||
|
||||
export type SystemMessageAction =
|
||||
| {
|
||||
kind: 'profile'
|
||||
profile: ChatBskyActorDefs.ProfileViewBasic
|
||||
displayName: string
|
||||
}
|
||||
| {kind: 'inviteLink'}
|
||||
|
||||
export type SystemMessageInfo = {
|
||||
message: MessageDescriptor
|
||||
Icon: React.ComponentType<SVGIconProps>
|
||||
action?: SystemMessageAction
|
||||
}
|
||||
|
||||
function getReferredDisplayName(
|
||||
function getProfileAction(
|
||||
user: ChatBskyConvoDefs.SystemMessageReferredUser,
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>,
|
||||
): string | null {
|
||||
): Extract<SystemMessageAction, {kind: 'profile'}> | null {
|
||||
const profile = relatedProfiles.get(user.did)
|
||||
return profile ? createSanitizedDisplayName(profile) : null
|
||||
if (!profile) return null
|
||||
return {
|
||||
kind: 'profile',
|
||||
profile,
|
||||
displayName: createSanitizedDisplayName(profile),
|
||||
}
|
||||
}
|
||||
|
||||
export function getSystemMessageInfo(
|
||||
@@ -34,34 +48,40 @@ export function getSystemMessageInfo(
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>,
|
||||
): SystemMessageInfo | null {
|
||||
if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) {
|
||||
const name = getReferredDisplayName(data.member, relatedProfiles)
|
||||
const action = getProfileAction(data.member, relatedProfiles)
|
||||
return {
|
||||
Icon: JoinIcon,
|
||||
message: name
|
||||
? msg`${name} was added to the group`
|
||||
message: action
|
||||
? msg`${action.displayName} was added to the group`
|
||||
: msg`Someone was added to the group`,
|
||||
action: action ?? undefined,
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataRemoveMember(data)) {
|
||||
const name = getReferredDisplayName(data.member, relatedProfiles)
|
||||
const action = getProfileAction(data.member, relatedProfiles)
|
||||
return {
|
||||
Icon: LeaveIcon,
|
||||
message: name
|
||||
? msg`${name} was removed from the group`
|
||||
message: action
|
||||
? msg`${action.displayName} was removed from the group`
|
||||
: msg`Someone was removed from the group`,
|
||||
action: action ?? undefined,
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataMemberJoin(data)) {
|
||||
const name = getReferredDisplayName(data.member, relatedProfiles)
|
||||
const action = getProfileAction(data.member, relatedProfiles)
|
||||
return {
|
||||
Icon: JoinIcon,
|
||||
message: name
|
||||
? msg`${name} joined the group`
|
||||
message: action
|
||||
? msg`${action.displayName} joined the group`
|
||||
: msg`Someone joined the group`,
|
||||
action: action ?? undefined,
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataMemberLeave(data)) {
|
||||
const name = getReferredDisplayName(data.member, relatedProfiles)
|
||||
const action = getProfileAction(data.member, relatedProfiles)
|
||||
return {
|
||||
Icon: LeaveIcon,
|
||||
message: name ? msg`${name} left the group` : msg`Someone left the group`,
|
||||
message: action
|
||||
? msg`${action.displayName} left the group`
|
||||
: msg`Someone left the group`,
|
||||
action: action ?? undefined,
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataLockConvo(data)) {
|
||||
return {Icon: LockIcon, message: msg`Chat locked`}
|
||||
@@ -77,13 +97,29 @@ export function getSystemMessageInfo(
|
||||
: msg`Chat title changed`,
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataCreateJoinLink(data)) {
|
||||
return {Icon: ChainLinkIcon, message: msg`Invite link created`}
|
||||
return {
|
||||
Icon: ChainLinkIcon,
|
||||
message: msg`Invite link created`,
|
||||
action: {kind: 'inviteLink'},
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataEditJoinLink(data)) {
|
||||
return {Icon: ChainLinkIcon, message: msg`Invite link edited`}
|
||||
return {
|
||||
Icon: ChainLinkIcon,
|
||||
message: msg`Invite link edited`,
|
||||
action: {kind: 'inviteLink'},
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataEnableJoinLink(data)) {
|
||||
return {Icon: ChainLinkIcon, message: msg`Invite link enabled`}
|
||||
return {
|
||||
Icon: ChainLinkIcon,
|
||||
message: msg`Invite link enabled`,
|
||||
action: {kind: 'inviteLink'},
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataDisableJoinLink(data)) {
|
||||
return {Icon: ChainLinkBrokenIcon, message: msg`Invite link disabled`}
|
||||
return {
|
||||
Icon: ChainLinkBrokenIcon,
|
||||
message: msg`Invite link disabled`,
|
||||
action: {kind: 'inviteLink'},
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import {EMOJI_REACTION_LIMIT} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
export const MESSAGE_GAP_THRESHOLD_MS = 60 * 60 * 1000
|
||||
export const CLUSTERED_MESSAGE_THRESHOLD_MS = 5 * 60 * 1000
|
||||
|
||||
export function canBeMessaged(profile: bsky.profile.AnyProfileView) {
|
||||
switch (profile.associated?.chat?.allowIncoming) {
|
||||
case 'none':
|
||||
|
||||
Reference in New Issue
Block a user