Update chat presentation (#10444)

This commit is contained in:
DS Boyce
2026-05-15 13:25:47 -07:00
committed by GitHub
parent 0eb382da03
commit 2bf6bdb6bc
13 changed files with 183 additions and 70 deletions
-5
View File
@@ -24,11 +24,6 @@
"count": 5
}
},
"src/components/Button.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 2
}
},
"src/components/Composer/index.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 2
+2 -2
View File
@@ -86,8 +86,8 @@ export type ButtonState = {
export type ButtonContext = VariantProps & ButtonState
type NonTextElements =
| React.ReactElement<any>
| Iterable<React.ReactElement<any> | null | undefined | boolean>
| React.ReactElement
| Iterable<React.ReactElement | null | undefined | boolean>
export type ButtonProps = Pick<
PressableProps,
+7 -8
View File
@@ -1,7 +1,6 @@
import {createContext, useCallback, useContext, useId, useMemo} from 'react'
import {type GestureResponderEvent, View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
import {atoms as a, type TextStyleProp, useTheme, web} from '#/alf'
import {
@@ -139,7 +138,7 @@ export function Cancel({
*/
cta?: string
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const {close} = Dialog.useDialogContext()
const onPress = useCallback(() => {
close()
@@ -150,9 +149,9 @@ export function Cancel({
variant="solid"
color="secondary"
size="large"
label={cta || _(msg`Cancel`)}
label={cta || l`Cancel`}
onPress={onPress}>
<ButtonText>{cta || _(msg`Cancel`)}</ButtonText>
<ButtonText>{cta || l`Cancel`}</ButtonText>
</Button>
)
}
@@ -191,7 +190,7 @@ export function Action({
shouldCloseOnPress?: boolean
testID?: string
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const {close} = Dialog.useDialogContext()
const handleOnPress = useCallback(
(e: GestureResponderEvent) => {
@@ -209,10 +208,10 @@ export function Action({
color={color}
disabled={disabled}
size="large"
label={cta || _(msg`Confirm`)}
label={cta || l`Confirm`}
onPress={handleOnPress}
testID={testID}>
<ButtonText>{cta || _(msg`Confirm`)}</ButtonText>
<ButtonText>{cta || l`Confirm`}</ButtonText>
{icon && <ButtonIcon icon={icon} />}
</Button>
)
@@ -1,6 +1,8 @@
import {View} from 'react-native'
import {plural} from '@lingui/core/macro'
import {Trans, useLingui} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {logger} from '#/logger'
import {useAddGroupMembers} from '#/state/queries/messages/add-group-members'
import {atoms as a, useTheme} from '#/alf'
@@ -30,8 +32,28 @@ export function AddMembersLink({
const {mutate: addGroupMembers, isPending: isAddPending} = useAddGroupMembers(
convoId,
{
onSuccess: () => {
addMembersControl.close()
onSuccess: data => {
addMembersControl.close(() => {
const members = data.addedMembers ?? []
let names = null
if (members.length === 1) {
names = l`${createSanitizedDisplayName(members[0])} added to chat`
} else if (members.length === 2) {
names = l`${createSanitizedDisplayName(members[0])} and ${createSanitizedDisplayName(members[1])} added to chat`
} else if (members.length > 2) {
const memberCount = convo.details.memberCount - 2
names = l`${createSanitizedDisplayName(members[0])}, ${createSanitizedDisplayName(members[1])}and ${plural(
memberCount,
{
one: '# other',
other: '# others',
},
)} added to chat`
}
if (names) Toast.show(names)
})
},
onError: e => {
logger.error('Failed to add group chat members', {message: e})
@@ -51,9 +51,7 @@ export function Member({
requireAuth(async () => {
try {
await queueFollow()
Toast.show(l`Following ${displayName}`, {
type: 'info',
})
Toast.show(l`Following ${displayName}`)
} catch (err) {
const e = err as Error
if (e?.name !== 'AbortError') {
@@ -181,14 +181,6 @@ export function MemberMenu({
</Menu.Trigger>
<Menu.Outer>
<Menu.Group>
<Menu.Item
label={l`Message ${displayName}`}
onPress={handleMessageMember}>
<Menu.ItemIcon icon={MessageIcon} />
<Menu.ItemText>
<Trans context="action">Message</Trans>
</Menu.ItemText>
</Menu.Item>
<Menu.Item
label={l`View ${displayName}s profile`}
onPress={() => {
@@ -199,6 +191,14 @@ export function MemberMenu({
<Trans>Go to profile</Trans>
</Menu.ItemText>
</Menu.Item>
<Menu.Item
label={l`Message ${displayName}`}
onPress={handleMessageMember}>
<Menu.ItemIcon icon={MessageIcon} />
<Menu.ItemText>
<Trans context="action">Message</Trans>
</Menu.ItemText>
</Menu.Item>
</Menu.Group>
<Menu.Divider />
<Menu.Group>
@@ -176,6 +176,8 @@ function GroupSettings({
moderationOpts: ModerationOpts
isReady: boolean
}) {
const [isPTRing, setIsPTRing] = useState(false)
const initialNumToRender = useInitialNumToRender({minItemHeight: 68})
const bottomBarOffset = useBottomBarOffset()
@@ -184,7 +186,7 @@ function GroupSettings({
const primaryMember = convo.primaryMember
const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did
const {data: memberListData = []} = useListConvoMembersQuery({
const {data: memberListData = [], refetch} = useListConvoMembersQuery({
convoId: convo.view.id,
placeholderData: convo.members,
})
@@ -269,6 +271,16 @@ function GroupSettings({
}
}
const onRefresh = async () => {
setIsPTRing(true)
try {
await refetch()
} catch (err) {
logger.error('Failed to refresh group chat members', {message: err})
}
setIsPTRing(false)
}
return (
<List
data={items}
@@ -289,6 +301,8 @@ function GroupSettings({
renderItem={renderItem}
sideBorders={false}
windowSize={11}
refreshing={isPTRing}
onRefresh={() => void onRefresh()}
/>
)
}
@@ -21,31 +21,33 @@ export function EditNamePrompt({
return (
<Prompt.Outer control={control}>
<Prompt.Content>
<Prompt.TitleText>
<Trans>Edit group name</Trans>
</Prompt.TitleText>
<View style={[a.my_sm]}>
<TextField.Root isInvalid={false}>
<TextField.Input
label={l`Edit group name`}
placeholder={l`Group name`}
value={value}
onChangeText={onChangeText}
returnKeyType="done"
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
autoFocus
onSubmitEditing={onConfirm}
/>
</TextField.Root>
</View>
</Prompt.Content>
<Prompt.Actions>
<Prompt.Action cta={l`Save`} onPress={onConfirm} />
<Prompt.Cancel />
</Prompt.Actions>
<>
<Prompt.Content>
<Prompt.TitleText>
<Trans>Edit group name</Trans>
</Prompt.TitleText>
<View style={[a.my_sm]}>
<TextField.Root isInvalid={false}>
<TextField.Input
label={l`Edit group name`}
placeholder={l`Group name`}
value={value}
onChangeText={onChangeText}
returnKeyType="done"
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
autoFocus
onSubmitEditing={onConfirm}
/>
</TextField.Root>
</View>
</Prompt.Content>
<Prompt.Actions>
<Prompt.Action cta={l`Save`} onPress={onConfirm} />
<Prompt.Cancel />
</Prompt.Actions>
</>
</Prompt.Outer>
)
}
@@ -74,9 +74,7 @@ export function ChatLocked({
a.text_sm,
a.font_semi_bold,
a.leading_snug,
{
color: t.palette.negative_500,
},
t.atoms.text_contrast_high,
]}>
<Trans>Unlock chat</Trans>
</Text>
@@ -38,6 +38,7 @@ enum Step {
INFO,
GENERATE,
MANAGE,
CONFIRM_DISABLE,
}
export function InviteLinkDialog({
@@ -341,13 +342,10 @@ export function InviteLinkDialog({
{isOwner ? (
<StackedButton
label={l`Disable`}
icon={isDisabling ? Loader : ChainLinkBrokenIcon}
icon={ChainLinkBrokenIcon}
color="negative_subtle"
style={[a.flex_1, a.rounded_full]}
disabled={isDisabling}
onPress={() => {
disableJoinLink()
}}>
onPress={() => setStep(Step.CONFIRM_DISABLE)}>
<Trans>Disable</Trans>
</StackedButton>
) : null}
@@ -382,10 +380,10 @@ export function InviteLinkDialog({
) : (
<View style={[a.gap_md, a.mt_lg]}>
<Button
disabled={isEnabling || isDisabling}
label={l`Re-enable invite link`}
color="primary"
size="large"
disabled={isEnabling}
onPress={() => {
enableJoinLink()
}}>
@@ -395,6 +393,7 @@ export function InviteLinkDialog({
{isEnabling && <ButtonIcon icon={Loader} />}
</Button>
<Button
disabled={isEnabling || isDisabling}
label={l`Generate new invite link`}
color="secondary"
size="large"
@@ -409,6 +408,63 @@ export function InviteLinkDialog({
)
break
}
case Step.CONFIRM_DISABLE:
content = (
<>
<View style={[a.align_center, a.justify_center, a.mb_lg]}>
<ChainLinkBrokenIcon fill={t.palette.negative_500} size="3xl" />
</View>
<Text
style={[
a.flex_1,
a.pb_sm,
a.text_center,
a.text_lg,
a.font_bold,
a.leading_snug,
]}>
<Trans>Disable this invite link?</Trans>
</Text>
<Text
style={[
a.pb_2xl,
a.text_center,
a.text_sm,
a.leading_snug,
t.atoms.text,
]}>
<Trans>
Anyone who has it will no longer be able to join or request to
join. You can always create a new one.
</Trans>
</Text>
<View style={[a.w_full, a.gap_md, a.justify_end]}>
<Button
color="negative"
disabled={isDisabling}
size="large"
label={l`Disable link`}
onPress={() => {
disableJoinLink()
setStep(Step.MANAGE)
}}>
<ButtonText>
<Trans>Disable link</Trans>
</ButtonText>
</Button>
<Button
color="secondary"
size="large"
label={l`Cancel`}
onPress={() => {
setStep(Step.MANAGE)
}}>
<ButtonText>{l`Cancel`}</ButtonText>
</Button>
</View>
</>
)
break
}
if (!isOwner && (!joinLink || joinLink.enabledStatus === 'disabled')) {
@@ -1,6 +1,7 @@
import {View} from 'react-native'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAddGroupMembers} from '#/state/queries/messages/add-group-members'
@@ -56,18 +57,22 @@ export function MessagesListInfoPanel({
let names: React.ReactNode = null
if (members.length === 1) {
names = <Trans>New chat with {members[0].displayName}</Trans>
names = (
<Trans>New chat with {createSanitizedDisplayName(members[0])}</Trans>
)
} else if (members.length === 2) {
names = (
<Trans>
New chat with {members[0].displayName} and {members[1].displayName}
New chat with {createSanitizedDisplayName(members[0])} and{' '}
{createSanitizedDisplayName(members[1])}
</Trans>
)
} else if (members.length > 2) {
const memberCount = convo.details.memberCount - 2
names = (
<Trans>
New chat with {members[0].displayName}, {members[1].displayName}, and{' '}
New chat with {createSanitizedDisplayName(members[0])},{' '}
{createSanitizedDisplayName(members[1])}, and{' '}
<Plural
value={memberCount}
one={`${memberCount} more`}
@@ -59,7 +59,11 @@ export function useListConvoMembersQuery({
r => r.did === data.member.did,
)
if (newMember) {
mutateList(list => list.concat(newMember))
mutateList(list =>
list.some(m => m.did === newMember.did)
? list
: list.concat(newMember),
)
}
}
} else if (ChatBskyConvoDefs.isLogRemoveMember(log)) {
@@ -1,6 +1,6 @@
import {
type ChatBskyActorDefs,
type ChatBskyConvoDefs,
ChatBskyConvoDefs,
type ChatBskyConvoListConvos,
type ChatBskyGroupRemoveMembers,
} from '@atproto/api'
@@ -56,9 +56,18 @@ export function useRemoveFromGroupChat(
CONVO_KEY(convoId),
prev => {
if (!prev) return
const nextMembers = prev.members.filter(m => !members.includes(m.did))
const removed = prev.members.length - nextMembers.length
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) {
return {...prev, members: nextMembers}
}
return {
...prev,
members: prev.members.filter(m => !members.includes(m.did)),
members: nextMembers,
kind: {
...prev.kind,
memberCount: Math.max(0, prev.kind.memberCount - removed),
},
}
},
)
@@ -73,9 +82,20 @@ export function useRemoveFromGroupChat(
...page,
convos: page.convos.map(convo => {
if (convo.id !== convoId) return convo
const nextMembers = convo.members.filter(
m => !members.includes(m.did),
)
const removed = convo.members.length - nextMembers.length
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
return {...convo, members: nextMembers}
}
return {
...convo,
members: convo.members.filter(m => !members.includes(m.did)),
members: nextMembers,
kind: {
...convo.kind,
memberCount: Math.max(0, convo.kind.memberCount - removed),
},
}
}),
})),