diff --git a/package.json b/package.json index 6f52679286..c4fd32a853 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.19.11", + "@atproto/api": "^0.19.12", "@atproto/syntax": "0.5.2", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", diff --git a/src/screens/Messages/ConversationSettings/Member.tsx b/src/screens/Messages/ConversationSettings/Member.tsx index 5daefce761..828bec8eaf 100644 --- a/src/screens/Messages/ConversationSettings/Member.tsx +++ b/src/screens/Messages/ConversationSettings/Member.tsx @@ -72,7 +72,7 @@ export function Member({ true, moderateProfile(profile.kind.addedBy, moderationOpts).ui('displayName'), )}` - : `Added by invite link` + : l`Added by invite link` return ( diff --git a/src/screens/Messages/ConversationSettings/index.tsx b/src/screens/Messages/ConversationSettings/index.tsx index d770b4a5cb..c0be0b1864 100644 --- a/src/screens/Messages/ConversationSettings/index.tsx +++ b/src/screens/Messages/ConversationSettings/index.tsx @@ -1,8 +1,8 @@ import {useState} from 'react' import {View} from 'react-native' -import {type ChatBskyConvoDefs} from '@atproto/api' +import {ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' -import {StackActions, useNavigation} from '@react-navigation/native' +import {useNavigation} from '@react-navigation/native' import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' @@ -38,7 +38,6 @@ import { } from '#/components/icons/Bell2' import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' import {type Props as SVGIconProps} from '#/components/icons/common' -import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid' import {EditBig_Stroke2_Corner2_Rounded as EditIcon} from '#/components/icons/EditBig' import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag' import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock' @@ -47,18 +46,13 @@ import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import * as bsky from '#/types/bsky' import {InviteLinkDialog} from '../components/InviteLinkDialog' import {AddMembersLink} from './AddMembersLink' import {Member, MemberPlaceholder} from './Member' import {MembersAndRequests} from './MembersAndRequests' import {EditNamePrompt, LeaveChatPrompt, LockChatPrompt} from './prompts' -const dateFormatter = new Intl.DateTimeFormat(undefined, { - month: 'long', - day: 'numeric', - year: 'numeric', -}) - type Item = | {type: 'MEMBERS_AND_REQUESTS'; key: string} | {type: 'ADD_MEMBERS_LINK'; key: string} @@ -119,11 +113,9 @@ function SettingsInner() { if (!isConvoActive(convoState)) { return ( - - - - - + + + ) } @@ -150,6 +142,19 @@ function keyExtractor(item: Item) { return item.key } +function isGroupMember( + member: ChatBskyActorDefs.ProfileViewBasic, +): member is GroupConvoMember { + // Kind is missing when the account has been deleted. + return ( + member.kind === undefined || + bsky.dangerousIsType( + member.kind, + ChatBskyActorDefs.isGroupConvoMember, + ) + ) +} + function GroupSettings({ convo, }: { @@ -160,12 +165,12 @@ function GroupSettings({ const {currentAccount} = useSession() - const primaryMember = convo?.primaryMember - const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did + const primaryMember = convo.primaryMember + const isOwner = primaryMember.did === currentAccount?.did const {data: memberListData = [], isPending} = useListConvoMembersQuery({ convoId: convo.view.id, - placeholderData: convo?.members, + placeholderData: convo.members, }) // TODO Need this data in order to populate this array. -dsb @@ -193,18 +198,19 @@ function GroupSettings({ ] if (isPending) { // should never be pending if we correctly set the query cache data - Array.from({length: 5}).forEach((_, i) => - items.push({ - type: 'CHAT_MEMBER_PLACEHOLDER', + items.push( + ...Array.from({length: 5}, (_, i) => ({ + type: 'CHAT_MEMBER_PLACEHOLDER' as const, key: `chat-member-placeholder-${i}`, - }), + })), ) } else { items.push( ...memberListData + .filter(isGroupMember) .sort((a, b) => { - const aIsOwner = a.did === primaryMember?.did - const bIsOwner = b.did === primaryMember?.did + const aIsOwner = a.did === primaryMember.did + const bIsOwner = b.did === primaryMember.did const aIsSelf = a.did === currentAccount?.did const bIsSelf = b.did === currentAccount?.did if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1 @@ -215,9 +221,9 @@ function GroupSettings({ (profile): Item => ({ type: 'CHAT_MEMBER', key: profile.did, - profile: profile as GroupConvoMember, + profile, status: - primaryMember?.did === profile.did + primaryMember.did === profile.did ? 'owner' : invites.includes(profile.did) ? 'invited' @@ -239,16 +245,16 @@ function GroupSettings({ /> ) case 'ADD_MEMBERS_LINK': - return convo ? : null + return case 'CHAT_MEMBER': - return convo ? ( + return ( - ) : null + ) case 'CHAT_MEMBER_PLACEHOLDER': return default: @@ -265,13 +271,7 @@ function GroupSettings({ desktopFixedHeight initialNumToRender={initialNumToRender} keyExtractor={keyExtractor} - ListHeaderComponent={ - convo?.kind === 'group' ? ( - - ) : ( - - ) - } + ListHeaderComponent={} renderItem={renderItem} sideBorders={false} windowSize={11} @@ -287,7 +287,7 @@ function SettingsHeader({ isOwner: boolean }) { const t = useTheme() - const {t: l} = useLingui() + const {i18n, t: l} = useLingui() const navigation = useNavigation() @@ -296,24 +296,25 @@ function SettingsHeader({ const lockStatus = convo.details.lockStatus - // TODO Enable this once the feature is working end-to-end. -dsb - // const {joinLink} = convo.details - const isJoinLinkEnabled = false - // const isJoinLinkEnabled = - // isOwner || (!isOwner && joinLink?.enabledStatus === 'enabled') + const {joinLink} = convo.details + const isJoinLinkEnabled = isOwner || joinLink?.enabledStatus === 'enabled' // TODO Enable this once the feature is working end-to-end. -dsb const isReportLinkEnabled = false - const {mutate: editGroupName} = useEditGroupChatName(convo.view.id, { - onError: e => { - setNewGroupName(groupName) - logger.error('Failed to edit group chat name', {message: e}) - Toast.show(l`Failed to edit group chat name`, {type: 'error'}) - }, - }) + const {mutate: editGroupName, isPending: isEditingName} = + useEditGroupChatName(convo.view.id, { + onSuccess: () => { + Toast.show(l({message: 'Group chat name updated', context: 'toast'})) + }, + onError: e => { + setNewGroupName(groupName) + logger.error('Failed to edit group chat name', {message: e}) + Toast.show(l`Failed to edit group chat name`, {type: 'error'}) + }, + }) - const {mutate: muteConvo} = useMuteConvo(convo.view.id, { + const {mutate: muteConvo, isPending: isMuting} = useMuteConvo(convo.view.id, { onSuccess: data => { if (data.convo.muted) { Toast.show(l({message: 'Group chat muted', context: 'toast'})) @@ -327,38 +328,44 @@ function SettingsHeader({ }, }) - const {mutate: leaveConvo} = useLeaveConvo(convo.view.id, { - onSuccess: () => { - // Settings > Chat > Chat list - navigation.dispatch(StackActions.pop(2)) + const {mutate: leaveConvo, isPending: isLeaving} = useLeaveConvo( + convo.view.id, + { + onSuccess: () => { + navigation.replace('Messages', {animation: 'pop'}) + }, + onError: e => { + logger.error('Failed to leave group chat', {message: e}) + Toast.show( + l({message: 'Failed to leave group chat', context: 'toast'}), + {type: 'error'}, + ) + }, }, - onError: e => { - logger.error('Failed to leave group chat', {message: e}) - Toast.show(l({message: 'Failed to leave group chat', context: 'toast'}), { - type: 'error', - }) - }, - }) + ) - const {mutate: lockConvo} = useLockConvo(convo.view.id, { - onSuccess: data => { - const kind = data.convo.kind as ChatBskyConvoDefs.GroupConvo - if (kind.lockStatus === 'locked') { - Toast.show(l({message: 'Group chat locked', context: 'toast'})) - } else { - Toast.show(l({message: 'Group chat unlocked', context: 'toast'})) - } + const {mutate: lockConvo, isPending: isLocking} = useLockConvo( + convo.view.id, + { + onSuccess: data => { + if (!ChatBskyConvoDefs.isGroupConvo(data.convo.kind)) return + if (data.convo.kind.lockStatus === 'locked') { + Toast.show(l({message: 'Group chat locked', context: 'toast'})) + } else { + Toast.show(l({message: 'Group chat unlocked', context: 'toast'})) + } + }, + onError: (e, {lock}) => { + if (lock) { + logger.error('Failed to lock group chat', {message: e}) + Toast.show(l`Failed to lock group chat`, {type: 'error'}) + } else { + logger.error('Failed to unlock group chat', {message: e}) + Toast.show(l`Failed to unlock group chat`, {type: 'error'}) + } + }, }, - onError: (e, {lock}) => { - if (lock) { - logger.error('Failed to lock group chat', {message: e}) - Toast.show(l`Failed to lock group chat`, {type: 'error'}) - } else { - logger.error('Failed to unlock group chat', {message: e}) - Toast.show(l`Failed to unlock group chat`, {type: 'error'}) - } - }, - }) + ) const inviteLinkDialog = Dialog.useDialogControl() const editNamePrompt = Prompt.usePromptControl() @@ -389,9 +396,7 @@ function SettingsHeader({ lockConvo({lock: false}) } - // TODO The creation date doesn't exist yet. -dsb - const showCreatedAt = false - const createdAt = new Date() + const createdAt = new Date(convo.details.createdAt) const canLockGroupChat = isOwner && lockStatus !== 'locked-permanently' @@ -412,18 +417,23 @@ function SettingsHeader({ ]}> {groupName} - {showCreatedAt ? ( - - Created {dateFormatter.format(createdAt)} - - ) : null} + + + Created{' '} + {i18n.date(createdAt, { + month: 'long', + day: 'numeric', + year: 'numeric', + })} + + {isOwner ? ( ) : null} - {isOwner ? null : isReportLinkEnabled ? ( + {!isOwner && isReportLinkEnabled && ( - ) : null} - {isOwner ? null : ( + )} + {!isOwner && ( - - - - - … - - - … - - - - - - - - - ) -} - function SettingsButton({ color = 'secondary', disabled, @@ -606,27 +580,3 @@ function SettingsButton({ ) } - -function SettingsButtonPlaceholder() { - const t = useTheme() - const {t: l} = useLingui() - - return ( - - - - … - - - ) -} diff --git a/src/screens/Messages/ConversationSettings/prompts.tsx b/src/screens/Messages/ConversationSettings/prompts.tsx index f5b980b990..4d58249e4b 100644 --- a/src/screens/Messages/ConversationSettings/prompts.tsx +++ b/src/screens/Messages/ConversationSettings/prompts.tsx @@ -21,33 +21,31 @@ export function EditNamePrompt({ return ( - <> - - - Edit group name - - - - - - - - - - - - + + + Edit group name + + + + + + + + + + + ) } diff --git a/yarn.lock b/yarn.lock index 93fa0b6680..89ba24a985 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20,10 +20,10 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@atproto/api@^0.19.11": - version "0.19.11" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.11.tgz#73885a47959907f22b68d671011ee70f80afd44b" - integrity sha512-7V4Sg6hcv/UxoXobjfvy/Ox2ioKQtZ3DzbsiFndYCcBfsZ5GO8rNEroHPq3hT0CFBJK1NAD6JfOtTBN2z267Xg== +"@atproto/api@^0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.12.tgz#0c21d7de070270f6596f908e6d8dd590b2933395" + integrity sha512-5CxDPBPxWx30ZM0UwjsmY67l+Kl8lTiLnH/8QvymsuJuRwA47CVfqo+QzEwJSkojNm7s0mTp7Ln2mR+OcFGBuw== dependencies: "@atproto/common-web" "^0.4.21" "@atproto/lexicon" "^0.6.2"