Bluesky app fork with some witchin' additions 💫
0
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 ? (