Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
117
fork

Configure Feed

Select the types of activity you want to include in your feed.

Use convo view instead of convo state for driving the UI (#10290)

authored by

DS Boyce and committed by
GitHub
935347c7 feebc6a9

+127 -120
+39 -57
src/components/dms/MessagesListHeader.tsx
··· 2 2 import {View} from 'react-native' 3 3 import { 4 4 type AppBskyActorDefs, 5 + ChatBskyConvoDefs, 5 6 type ModerationCause, 6 7 type ModerationDecision, 7 8 } from '@atproto/api' ··· 11 12 import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 12 13 import {makeProfileLink} from '#/lib/routes/links' 13 14 import {type NavigationProp} from '#/lib/routes/types' 14 - import {logger} from '#/logger' 15 15 import {type Shadow} from '#/state/cache/profile-shadow' 16 - import { 17 - type ActiveConvoStates, 18 - isConvoActive, 19 - useConvo, 20 - } from '#/state/messages/convo' 21 - import {type ConvoItem} from '#/state/messages/convo/types' 22 16 import {useSession} from '#/state/session' 23 17 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 24 18 import {atoms as a, useTheme} from '#/alf' ··· 32 26 import {ProfileBadges} from '#/components/ProfileBadges' 33 27 import {Text} from '#/components/Typography' 34 28 import {IS_LIQUID_GLASS, IS_WEB} from '#/env' 29 + import {type ConvoWithDetails} from './util' 35 30 36 31 const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE 37 32 38 33 export function MessagesListHeader({ 34 + convo, 39 35 profile, 40 36 moderation, 41 37 }: { 38 + convo?: ConvoWithDetails | null 42 39 profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed> 43 40 moderation?: ModerationDecision | null 44 41 }) { 45 42 const t = useTheme() 46 43 47 - const convoState = useConvo() 48 - const isGroupChat = convoState?.isGroup?.() 44 + const isGroupChat = convo?.kind === 'group' 49 45 50 46 const blockInfo = useMemo(() => { 51 47 if (!moderation) return ··· 65 61 <View style={[{minHeight: PFP_SIZE}, a.justify_center]}> 66 62 <Layout.Header.BackButton /> 67 63 </View> 68 - {isConvoActive(convoState) ? ( 64 + {convo ? ( 69 65 moderation && blockInfo && profile && !isGroupChat ? ( 70 66 <ProfileHeaderReady 71 - convoState={convoState} 67 + convo={convo} 72 68 profile={profile} 73 69 moderation={moderation} 74 70 blockInfo={blockInfo} 75 71 /> 76 72 ) : ( 77 73 <GroupHeaderReady 78 - convoState={convoState} 74 + convo={convo} 79 75 profile={profile} 80 76 moderation={moderation} 81 77 /> ··· 111 107 } 112 108 113 109 function ProfileHeaderReady({ 114 - convoState, 110 + convo, 115 111 profile, 116 112 moderation, 117 113 blockInfo, 118 114 }: { 119 - convoState: ActiveConvoStates 115 + convo: ConvoWithDetails 120 116 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> 121 117 moderation: ModerationDecision 122 118 blockInfo: { ··· 132 128 ? l`Deleted Account` 133 129 : createSanitizedDisplayName(profile, true, moderation.ui('displayName')) 134 130 135 - const latestMessageFromOther = convoState.items.findLast( 136 - (item: ConvoItem) => 137 - item.type === 'message' && 138 - item.message.sender.did !== currentAccount?.did, 139 - ) 140 - 141 131 const latestReportableMessage = 142 - latestMessageFromOther?.type === 'message' 143 - ? latestMessageFromOther.message 132 + ChatBskyConvoDefs.isMessageView(convo.view.lastMessage) && 133 + convo.view.lastMessage.sender?.did !== currentAccount?.did 134 + ? convo.view.lastMessage 144 135 : undefined 145 136 146 137 return ( ··· 164 155 </View> 165 156 </Link> 166 157 } 167 - muted={convoState.convo?.muted} 158 + muted={convo.view.muted} 168 159 settings={ 169 - isConvoActive(convoState) ? ( 170 - <ConvoMenu 171 - convo={convoState.convo} 172 - profile={profile} 173 - currentScreen="conversation" 174 - blockInfo={blockInfo} 175 - latestReportableMessage={latestReportableMessage} 176 - /> 177 - ) : null 160 + <ConvoMenu 161 + convo={convo.view} 162 + profile={profile} 163 + currentScreen="conversation" 164 + blockInfo={blockInfo} 165 + latestReportableMessage={latestReportableMessage} 166 + /> 178 167 } 179 168 /> 180 169 ) 181 170 } 182 171 183 172 function GroupHeaderReady({ 184 - convoState, 173 + convo, 185 174 profile, 186 175 moderation, 187 176 }: { 188 - convoState: ActiveConvoStates 177 + convo: ConvoWithDetails 189 178 profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed> 190 179 moderation?: ModerationDecision | null 191 180 }) { ··· 193 182 194 183 const navigation = useNavigation<NavigationProp>() 195 184 196 - const groupInfo = convoState.getGroupInfo?.() 185 + const groupInfo = convo.kind === 'group' ? convo.details : undefined 197 186 198 187 const isDeletedAccount = profile?.handle === 'missing.invalid' 199 188 const displayName = isDeletedAccount ··· 206 195 (displayName ? l`${displayName}’s group chat` : l`Group chat`) 207 196 208 197 const handleNavigateToSettings = () => { 209 - const convoId = convoState.convo?.id 210 - if (convoId) { 211 - navigation.navigate('MessagesConversationSettings', { 212 - conversation: convoId, 213 - }) 214 - } else { 215 - logger.error(`handleNavigateToSettings: missing convo ID`) 216 - } 198 + navigation.navigate('MessagesConversationSettings', { 199 + conversation: convo.view.id, 200 + }) 217 201 } 218 202 219 203 return ( 220 204 <Wrapper 221 205 heading={ 222 206 <> 223 - <AvatarBubbles size="small" profiles={convoState.recipients ?? []} /> 207 + <AvatarBubbles size="small" profiles={convo.members} /> 224 208 <Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}> 225 209 {groupName} 226 210 </Text> 227 211 </> 228 212 } 229 - muted={convoState.convo?.muted} 213 + muted={convo.view.muted} 230 214 settings={ 231 - isConvoActive(convoState) ? ( 232 - <Button 233 - label={l`Open group chat settings`} 234 - size="small" 235 - color="secondary" 236 - shape="round" 237 - variant="ghost" 238 - style={[a.bg_transparent]} 239 - onPress={handleNavigateToSettings}> 240 - <ButtonIcon icon={DotsHorizontalIcon} size="md" /> 241 - </Button> 242 - ) : null 215 + <Button 216 + label={l`Open group chat settings`} 217 + size="small" 218 + color="secondary" 219 + shape="round" 220 + variant="ghost" 221 + style={[a.bg_transparent]} 222 + onPress={handleNavigateToSettings}> 223 + <ButtonIcon icon={DotsHorizontalIcon} size="md" /> 224 + </Button> 243 225 } 244 226 /> 245 227 )
+37 -10
src/screens/Messages/Conversation.tsx
··· 35 35 import {useCurrentConvoId} from '#/state/messages/current-convo-id' 36 36 import {useModerationOpts} from '#/state/preferences/moderation-opts' 37 37 import {useProfileQuery} from '#/state/queries/profile' 38 + import {useSession} from '#/state/session' 38 39 import {useSetMinimalShellMode} from '#/state/shell' 39 40 import {MessagesList} from '#/screens/Messages/components/MessagesList' 40 41 import {atoms as a, useTheme, web} from '#/alf' ··· 46 47 } from '#/components/dialogs/EmailDialog' 47 48 import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter' 48 49 import {MessagesListHeader} from '#/components/dms/MessagesListHeader' 50 + import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util' 49 51 import {Error} from '#/components/Error' 50 52 import * as Layout from '#/components/Layout' 51 53 import {Loader} from '#/components/Loader' ··· 104 106 const t = useTheme() 105 107 const convoState = useConvo() 106 108 const {_} = useLingui() 109 + const {currentAccount} = useSession() 107 110 const isFocused = useIsFocused() 108 111 const {top: topInset} = useSafeAreaInsets() 109 112 113 + const convo = convoState.convo 114 + ? parseConvoView(convoState.convo, currentAccount?.did) 115 + : null 116 + 110 117 const moderationOpts = useModerationOpts() 111 118 const {data: recipientUnshadowed} = useProfileQuery({ 112 119 did: convoState.getPrimaryMember?.()?.did, ··· 144 151 <Layout.Center 145 152 style={[a.w_full, IS_LIQUID_GLASS && {paddingTop: topInset}]}> 146 153 {moderation ? ( 147 - <MessagesListHeader profile={recipient} moderation={moderation} /> 154 + <MessagesListHeader 155 + convo={convo} 156 + profile={recipient} 157 + moderation={moderation} 158 + /> 148 159 ) : ( 149 - <MessagesListHeader /> 160 + <MessagesListHeader convo={convo} /> 150 161 )} 151 162 </Layout.Center> 152 163 <Error ··· 166 177 {!readyToShow && ( 167 178 <View style={IS_LIQUID_GLASS && {paddingTop: topInset}}> 168 179 {moderation ? ( 169 - <MessagesListHeader profile={recipient} moderation={moderation} /> 180 + <MessagesListHeader 181 + convo={convo} 182 + profile={recipient} 183 + moderation={moderation} 184 + /> 170 185 ) : ( 171 - <MessagesListHeader /> 186 + <MessagesListHeader convo={convo} /> 172 187 )} 173 188 </View> 174 189 )} ··· 178 193 recipient={recipient} 179 194 hasScrolled={hasScrolled} 180 195 setHasScrolled={setHasScrolled} 196 + convo={convo} 197 + isActive={isConvoActive(convoState)} 198 + hasMessages={isConvoActive(convoState) && convoState.items.length > 0} 181 199 /> 182 200 {!readyToShow && ( 183 201 <View ··· 205 223 recipient, 206 224 hasScrolled, 207 225 setHasScrolled, 226 + convo, 227 + isActive, 228 + hasMessages, 208 229 }: { 209 230 moderation: ModerationDecision | null 210 231 recipient: Shadow<AppBskyActorDefs.ProfileViewDetailed> | undefined 211 232 hasScrolled: boolean 212 233 setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> 234 + convo: ConvoWithDetails | null 235 + isActive: boolean 236 + hasMessages: boolean 213 237 }) { 214 - const convoState = useConvo() 215 238 const navigation = useNavigation<NavigationProp>() 216 239 const {top: topInset} = useSafeAreaInsets() 217 240 const [headerHeight, setHeaderHeight] = useState(0) ··· 262 285 }, [maybeBlockForEmailVerification]) 263 286 264 287 const header = ( 265 - <MessagesListHeader profile={recipient} moderation={moderation} /> 288 + <MessagesListHeader 289 + convo={convo} 290 + profile={recipient} 291 + moderation={moderation} 292 + /> 266 293 ) 267 294 268 295 return ( ··· 277 304 ) : ( 278 305 header 279 306 )} 280 - {isConvoActive(convoState) && ( 307 + {isActive && ( 281 308 <MessagesList 282 309 hasScrolled={hasScrolled} 283 310 setHasScrolled={setHasScrolled} ··· 285 312 hasAcceptOverride={!!params.accept} 286 313 transparentHeaderHeight={IS_LIQUID_GLASS ? headerHeight : 0} 287 314 footer={ 288 - moderation && recipient ? ( 315 + moderation && recipient && convo ? ( 289 316 <MessagesListBlockedFooter 290 317 recipient={recipient} 291 - convoId={convoState.convo.id} 292 - hasMessages={convoState.items.length > 0} 318 + convoId={convo.view.id} 319 + hasMessages={hasMessages} 293 320 moderation={moderation} 294 321 /> 295 322 ) : null
+51 -53
src/screens/Messages/ConversationSettings.tsx
··· 1 1 import {useMemo, useState} from 'react' 2 2 import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native' 3 - import {type ChatBskyConvoDefs, moderateProfile} from '@atproto/api' 3 + import {moderateProfile} from '@atproto/api' 4 4 import {plural} from '@lingui/core/macro' 5 5 import {Trans, useLingui} from '@lingui/react/macro' 6 6 import {StackActions, useNavigation} from '@react-navigation/native' ··· 34 34 import {Button, type ButtonColor, ButtonIcon} from '#/components/Button' 35 35 import * as Dialog from '#/components/Dialog' 36 36 import {AddMembersFlow} from '#/components/dms/AddMembersFlow' 37 + import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util' 37 38 import {Error} from '#/components/Error' 38 39 import * as TextField from '#/components/forms/TextField' 39 40 import {useInteractionState} from '#/components/hooks/useInteractionState' ··· 126 127 127 128 const convoState = useConvo() 128 129 const {currentAccount} = useSession() 129 - const primaryMember = convoState?.getPrimaryMember?.() 130 + 131 + const convo = convoState.convo 132 + ? parseConvoView(convoState.convo, currentAccount?.did) 133 + : null 134 + const primaryMember = convo?.primaryMember 135 + const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did 130 136 131 - const data: bsky.profile.AnyProfileView[] = convoState.convo?.members ?? [] 137 + const data: bsky.profile.AnyProfileView[] = convo?.members ?? [] 132 138 const invites: string[] = [] 133 139 134 140 const items = [ ··· 163 169 function renderItem({item}: {item: Item}) { 164 170 switch (item.type) { 165 171 case 'MEMBERS_AND_REQUESTS': 166 - return <MembersAndRequests memberCount={data.length} requestCount={5} /> 172 + return ( 173 + <MembersAndRequests 174 + memberCount={data.length} 175 + requestCount={5} 176 + isOwner={isOwner} 177 + /> 178 + ) 167 179 case 'ADD_MEMBERS_LINK': 168 - return <AddMembersLink /> 180 + return <AddMembersLink isOwner={isOwner} /> 169 181 case 'CHAT_MEMBER': 170 - return <Member profile={item.profile} status={item.status} /> 182 + return ( 183 + <Member 184 + profile={item.profile} 185 + status={item.status} 186 + isOwner={isOwner} 187 + /> 188 + ) 171 189 default: 172 190 return null 173 191 } ··· 194 212 initialNumToRender={initialNumToRender} 195 213 keyExtractor={keyExtractor} 196 214 ListHeaderComponent={ 197 - convoState.convo ? ( 198 - <SettingsHeader convo={convoState.convo} profiles={data} /> 215 + convo ? ( 216 + <SettingsHeader convo={convo} isOwner={isOwner} /> 199 217 ) : ( 200 218 <SettingsHeaderPlaceholder /> 201 219 ) ··· 211 229 function MembersAndRequests({ 212 230 memberCount, 213 231 requestCount, 232 + isOwner, 214 233 }: { 215 234 memberCount: number 216 235 requestCount: number 236 + isOwner: boolean 217 237 }) { 218 238 const t = useTheme() 219 239 const {t: l} = useLingui() 220 240 221 - const convoState = useConvo() 222 - const {currentAccount} = useSession() 223 - 224 - const isOwner = 225 - currentAccount?.did == null 226 - ? false 227 - : convoState.getPrimaryMember?.()?.did === currentAccount.did 228 - 229 241 return ( 230 242 <View style={[a.flex_row, a.justify_between, a.mx_xl, a.mt_lg, a.mb_sm]}> 231 243 <View style={[a.flex_row, a.align_center]}> ··· 254 266 ) 255 267 } 256 268 257 - function AddMembersLink() { 269 + function AddMembersLink({isOwner}: {isOwner: boolean}) { 258 270 const t = useTheme() 259 271 const {t: l} = useLingui() 260 272 261 - const convoState = useConvo() 262 - const {currentAccount} = useSession() 263 - 264 273 const addMembersControl = Dialog.useDialogControl() 265 274 266 - const isOwner = 267 - currentAccount?.did == null 268 - ? false 269 - : convoState.getPrimaryMember?.()?.did === currentAccount.did 270 - 271 275 if (!isOwner) { 272 276 return null 273 277 } ··· 354 358 function Member({ 355 359 profile, 356 360 status, 361 + isOwner, 357 362 }: { 358 363 profile: Shadow<bsky.profile.AnyProfileView> 359 364 status: 'owner' | 'member' | 'invited' 365 + isOwner: boolean 360 366 }) { 361 367 const navigation = useNavigation<NavigationProp>() 362 368 const t = useTheme() ··· 388 394 break 389 395 } 390 396 } else { 391 - statusBadge = <MemberMenu profile={profile} type={status} /> 397 + statusBadge = ( 398 + <MemberMenu profile={profile} type={status} isOwner={isOwner} /> 399 + ) 392 400 } 393 401 394 402 return ( ··· 496 504 function MemberMenu({ 497 505 profile, 498 506 type, 507 + isOwner, 499 508 }: { 500 509 profile: Shadow<bsky.profile.AnyProfileView> 501 510 type: 'owner' | 'member' | 'invited' 511 + isOwner: boolean 502 512 }) { 503 513 const navigation = useNavigation<NavigationProp>() 504 514 const t = useTheme() ··· 506 516 const ax = useAnalytics() 507 517 508 518 const requireEmailVerification = useRequireEmailVerification() 509 - const convoState = useConvo() 510 - const {currentAccount} = useSession() 511 519 512 520 const blockMemberPrompt = Prompt.usePromptControl() 513 521 514 - const isOwner = 515 - currentAccount?.did == null 516 - ? false 517 - : convoState.getPrimaryMember?.()?.did === currentAccount.did 518 - 519 522 const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did) 520 523 const {mutate: initiateConvo} = useGetConvoForMembers({ 521 524 onSuccess: ({convo}) => { ··· 706 709 707 710 function SettingsHeader({ 708 711 convo, 709 - profiles, 712 + isOwner, 710 713 }: { 711 - convo: ChatBskyConvoDefs.ConvoView 712 - profiles: bsky.profile.AnyProfileView[] 714 + convo: ConvoWithDetails 715 + isOwner: boolean 713 716 }) { 714 717 const t = useTheme() 715 718 const {t: l} = useLingui() 716 719 717 720 const navigation = useNavigation<NavigationProp>() 718 - const convoState = useConvo() 719 - const {currentAccount} = useSession() 720 721 721 - const groupName = convoState.getGroupInfo?.()?.name ?? '' 722 + const groupName = convo.kind === 'group' ? convo.details.name : '' 722 723 const [newGroupName, setNewGroupName] = useState(groupName) 723 724 724 725 const [isLocked, setIsLocked] = useState(false) 725 726 726 - const isOwner = 727 - currentAccount?.did == null 728 - ? false 729 - : convoState.getPrimaryMember?.()?.did === currentAccount.did 730 - 731 - const {mutate: editGroupName} = useEditGroupName(convo.id, { 727 + const {mutate: editGroupName} = useEditGroupName(convo.view.id, { 732 728 onError: e => { 733 729 setNewGroupName(groupName) 734 730 logger.error('Failed to edit group chat name', {message: e}) ··· 738 734 }, 739 735 }) 740 736 741 - const {mutate: muteConvo} = useMuteConvo(convo.id, { 737 + const {mutate: muteConvo} = useMuteConvo(convo.view.id, { 742 738 onSuccess: data => { 743 739 if (data.convo.muted) { 744 740 Toast.show(l({message: 'Group chat muted', context: 'toast'})) ··· 754 750 }, 755 751 }) 756 752 757 - const {mutate: leaveConvo} = useLeaveConvo(convo.id, { 753 + const {mutate: leaveConvo} = useLeaveConvo(convo.view.id, { 758 754 onMutate: () => { 759 755 navigation.dispatch(StackActions.pop(2)) 760 756 }, ··· 772 768 const leaveChatPrompt = Prompt.usePromptControl() 773 769 774 770 const handleToggleMute = () => { 775 - muteConvo({mute: !convo?.muted}) 771 + muteConvo({mute: !convo.view.muted}) 776 772 } 777 773 778 774 const handleLeaveChat = () => { ··· 815 811 <View 816 812 style={[a.px_xl, a.py_4xl, a.border_b, t.atoms.border_contrast_low]}> 817 813 <View style={[a.align_center, a.justify_center]}> 818 - <AvatarBubbles profiles={profiles} /> 814 + <AvatarBubbles profiles={convo.members} /> 819 815 </View> 820 816 <Text 821 817 style={[ ··· 846 842 a.pt_2xl, 847 843 ]}> 848 844 <SettingsButton 849 - color={convo?.muted ? 'negative_subtle' : 'secondary'} 850 - icon={convo?.muted ? BellOffIcon : BellIcon} 845 + color={convo.view.muted ? 'negative_subtle' : 'secondary'} 846 + icon={convo.view.muted ? BellOffIcon : BellIcon} 851 847 label={ 852 - convo?.muted ? l`Unmute this group chat` : l`Mute this group chat` 848 + convo.view.muted 849 + ? l`Unmute this group chat` 850 + : l`Mute this group chat` 853 851 } 854 - text={convo?.muted ? l`Muted` : l`Mute`} 852 + text={convo.view.muted ? l`Muted` : l`Mute`} 855 853 onPress={handleToggleMute} 856 854 /> 857 855 {isOwner ? (