Bluesky app fork with some witchin' additions ๐Ÿ’ซ
0
fork

Configure Feed

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

[๐Ÿด] DM button on profile (#4097)

* add profile button

* separate out button to component

* normalise subscribe to labeller button size

* infinite staletime

* use Link rather than Button and change icon

* adjust icon position

authored by

Samuel Newman and committed by
GitHub
24f8794d 2414559b

+125 -42
+39
src/components/dms/MessageProfileButton.tsx
··· 1 + import React from 'react' 2 + import {AppBskyActorDefs} from '@atproto/api' 3 + import {msg} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members' 7 + import {atoms as a, useTheme} from '#/alf' 8 + import {Message_Stroke2_Corner0_Rounded as Message} from '../icons/Message' 9 + import {Link} from '../Link' 10 + 11 + export function MessageProfileButton({ 12 + profile, 13 + }: { 14 + profile: AppBskyActorDefs.ProfileView 15 + }) { 16 + const {_} = useLingui() 17 + const t = useTheme() 18 + 19 + const {data: convoId} = useMaybeConvoForUser(profile.did) 20 + 21 + if (!convoId) return null 22 + 23 + return ( 24 + <Link 25 + testID="dmBtn" 26 + size="small" 27 + color="secondary" 28 + variant="solid" 29 + shape="round" 30 + label={_(msg`Message ${profile.handle}`)} 31 + to={`/messages/${convoId}`} 32 + style={[a.justify_center, {width: 36, height: 36}]}> 33 + <Message 34 + style={[t.atoms.text, {marginLeft: 1, marginBottom: 1}]} 35 + size="md" 36 + /> 37 + </Link> 38 + ) 39 + }
+3 -3
src/screens/Profile/Header/ProfileHeaderLabeler.tsx
··· 128 128 129 129 const onPressSubscribe = React.useCallback( 130 130 () => 131 - requireAuth(async () => { 131 + requireAuth(async (): Promise<void> => { 132 132 if (!canSubscribe) { 133 133 cantSubscribePrompt.open() 134 134 return ··· 197 197 <View 198 198 style={[ 199 199 { 200 - paddingVertical: 12, 201 200 backgroundColor: 202 201 isSubscribed || !canSubscribe 203 202 ? state.hovered || state.pressed ··· 207 206 ? tokens.color.temp_purple_dark 208 207 : tokens.color.temp_purple, 209 208 }, 210 - a.px_lg, 209 + a.py_sm, 210 + a.px_md, 211 211 a.rounded_sm, 212 212 a.gap_sm, 213 213 ]}>
+35 -21
src/screens/Profile/Header/ProfileHeaderStandard.tsx
··· 28 28 import * as Toast from '#/view/com/util/Toast' 29 29 import {atoms as a, useTheme} from '#/alf' 30 30 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 31 + import {MessageProfileButton} from '#/components/dms/MessageProfileButton' 31 32 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 32 33 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 33 34 import * as Prompt from '#/components/Prompt' ··· 156 157 style={[a.px_lg, a.pt_md, a.pb_sm]} 157 158 pointerEvents={isIOS ? 'auto' : 'box-none'}> 158 159 <View 159 - style={[a.flex_row, a.justify_end, a.gap_sm, a.pb_sm]} 160 + style={[ 161 + {paddingLeft: 90}, 162 + a.flex_row, 163 + a.justify_end, 164 + a.gap_sm, 165 + a.pb_sm, 166 + a.flex_wrap, 167 + ]} 160 168 pointerEvents={isIOS ? 'auto' : 'box-none'}> 161 169 {isMe ? ( 162 170 <Button ··· 166 174 variant="solid" 167 175 onPress={onPressEditProfile} 168 176 label={_(msg`Edit profile`)} 169 - style={a.rounded_full}> 177 + style={[a.rounded_full, a.py_sm]}> 170 178 <ButtonText> 171 179 <Trans>Edit Profile</Trans> 172 180 </ButtonText> ··· 181 189 label={_(msg`Unblock`)} 182 190 disabled={!hasSession} 183 191 onPress={() => unblockPromptControl.open()} 184 - style={a.rounded_full}> 192 + style={[a.rounded_full, a.py_sm]}> 185 193 <ButtonText> 186 194 <Trans context="action">Unblock</Trans> 187 195 </ButtonText> ··· 190 198 ) : !profile.viewer?.blockedBy ? ( 191 199 <> 192 200 {hasSession && ( 193 - <Button 194 - testID="suggestedFollowsBtn" 195 - size="small" 196 - color={showSuggestedFollows ? 'primary' : 'secondary'} 197 - variant="solid" 198 - shape="round" 199 - onPress={() => setShowSuggestedFollows(!showSuggestedFollows)} 200 - label={_(msg`Show follows similar to ${profile.handle}`)}> 201 - <FontAwesomeIcon 202 - icon="user-plus" 203 - style={ 204 - showSuggestedFollows 205 - ? {color: t.palette.white} 206 - : t.atoms.text 201 + <> 202 + <MessageProfileButton profile={profile} /> 203 + <Button 204 + testID="suggestedFollowsBtn" 205 + size="small" 206 + color={showSuggestedFollows ? 'primary' : 'secondary'} 207 + variant="solid" 208 + shape="round" 209 + onPress={() => 210 + setShowSuggestedFollows(!showSuggestedFollows) 207 211 } 208 - size={14} 209 - /> 210 - </Button> 212 + label={_(msg`Show follows similar to ${profile.handle}`)} 213 + style={{width: 36, height: 36}}> 214 + <FontAwesomeIcon 215 + icon="user-plus" 216 + style={ 217 + showSuggestedFollows 218 + ? {color: t.palette.white} 219 + : t.atoms.text 220 + } 221 + size={14} 222 + /> 223 + </Button> 224 + </> 211 225 )} 212 226 213 227 <Button ··· 223 237 onPress={ 224 238 profile.viewer?.following ? onPressUnfollow : onPressFollow 225 239 } 226 - style={[a.rounded_full, a.gap_xs]}> 240 + style={[a.rounded_full, a.gap_xs, a.py_sm]}> 227 241 <ButtonIcon 228 242 position="left" 229 243 icon={profile.viewer?.following ? Check : Plus}
+31 -1
src/state/queries/messages/get-convo-for-members.ts
··· 1 1 import {ChatBskyConvoGetConvoForMembers} from '@atproto/api' 2 - import {useMutation, useQueryClient} from '@tanstack/react-query' 2 + import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' 3 3 4 4 import {logger} from '#/logger' 5 5 import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const' 6 6 import {useAgent} from '#/state/session' 7 + import {STALE} from '..' 7 8 import {RQKEY as CONVO_KEY} from './conversation' 9 + 10 + const RQKEY_ROOT = 'convo-for-user' 11 + export const RQKEY = (did: string) => [RQKEY_ROOT, did] 8 12 9 13 export function useGetConvoForMembers({ 10 14 onSuccess, ··· 35 39 }, 36 40 }) 37 41 } 42 + 43 + /** 44 + * Gets the conversation ID for a given DID. Returns null if it's not possible to message them. 45 + */ 46 + export function useMaybeConvoForUser(did: string) { 47 + const {getAgent} = useAgent() 48 + 49 + return useQuery({ 50 + queryKey: RQKEY(did), 51 + queryFn: async () => { 52 + const convo = await getAgent() 53 + .api.chat.bsky.convo.getConvoForMembers( 54 + {members: [did]}, 55 + {headers: DM_SERVICE_HEADERS}, 56 + ) 57 + .catch(() => ({success: null})) 58 + 59 + if (convo.success) { 60 + return convo.data.convo.id 61 + } else { 62 + return null 63 + } 64 + }, 65 + staleTime: STALE.INFINITY, 66 + }) 67 + }
+17 -17
src/view/com/profile/ProfileMenu.tsx
··· 1 1 import React, {memo} from 'react' 2 2 import {TouchableOpacity} from 'react-native' 3 3 import {AppBskyActorDefs} from '@atproto/api' 4 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 5 import {msg, Trans} from '@lingui/macro' 5 6 import {useLingui} from '@lingui/react' 6 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 7 7 import {useQueryClient} from '@tanstack/react-query' 8 - import * as Toast from 'view/com/util/Toast' 9 - import {EventStopper} from 'view/com/util/EventStopper' 10 - import {useSession} from 'state/session' 11 - import * as Menu from '#/components/Menu' 12 - import {useTheme} from '#/alf' 13 - import {usePalette} from 'lib/hooks/usePalette' 8 + 9 + import {logger} from '#/logger' 10 + import {useAnalytics} from 'lib/analytics/analytics' 14 11 import {HITSLOP_10} from 'lib/constants' 12 + import {usePalette} from 'lib/hooks/usePalette' 13 + import {makeProfileLink} from 'lib/routes/links' 15 14 import {shareUrl} from 'lib/sharing' 16 15 import {toShareUrl} from 'lib/strings/url-helpers' 17 - import {makeProfileLink} from 'lib/routes/links' 18 - import {useAnalytics} from 'lib/analytics/analytics' 16 + import {Shadow} from 'state/cache/types' 19 17 import {useModalControls} from 'state/modals' 20 - import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' 21 18 import { 22 19 RQKEY as profileQueryKey, 23 20 useProfileBlockMutationQueue, 24 21 useProfileFollowMutationQueue, 25 22 useProfileMuteMutationQueue, 26 23 } from 'state/queries/profile' 24 + import {useSession} from 'state/session' 25 + import {EventStopper} from 'view/com/util/EventStopper' 26 + import * as Toast from 'view/com/util/Toast' 27 + import {useTheme} from '#/alf' 27 28 import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' 29 + import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' 28 30 import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle' 29 31 import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' 30 - import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' 31 - import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' 32 + import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2' 32 33 import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck' 33 34 import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX' 34 - import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2' 35 35 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 36 - import {logger} from '#/logger' 37 - import {Shadow} from 'state/cache/types' 36 + import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' 37 + import * as Menu from '#/components/Menu' 38 38 import * as Prompt from '#/components/Prompt' 39 + import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' 39 40 40 41 let ProfileMenu = ({ 41 42 profile, ··· 192 193 flexDirection: 'row', 193 194 alignItems: 'center', 194 195 justifyContent: 'center', 195 - paddingVertical: 10, 196 + padding: 8, 196 197 borderRadius: 50, 197 - paddingHorizontal: 16, 198 198 }, 199 199 pal.btn, 200 200 ]}>