[🐴] 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 positionzio/stable
parent
2414559b80
commit
24f8794d4d
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Message_Stroke2_Corner0_Rounded as Message} from '../icons/Message'
|
||||||
|
import {Link} from '../Link'
|
||||||
|
|
||||||
|
export function MessageProfileButton({
|
||||||
|
profile,
|
||||||
|
}: {
|
||||||
|
profile: AppBskyActorDefs.ProfileView
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
const {data: convoId} = useMaybeConvoForUser(profile.did)
|
||||||
|
|
||||||
|
if (!convoId) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
testID="dmBtn"
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
variant="solid"
|
||||||
|
shape="round"
|
||||||
|
label={_(msg`Message ${profile.handle}`)}
|
||||||
|
to={`/messages/${convoId}`}
|
||||||
|
style={[a.justify_center, {width: 36, height: 36}]}>
|
||||||
|
<Message
|
||||||
|
style={[t.atoms.text, {marginLeft: 1, marginBottom: 1}]}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
|
@ -128,7 +128,7 @@ let ProfileHeaderLabeler = ({
|
||||||
|
|
||||||
const onPressSubscribe = React.useCallback(
|
const onPressSubscribe = React.useCallback(
|
||||||
() =>
|
() =>
|
||||||
requireAuth(async () => {
|
requireAuth(async (): Promise<void> => {
|
||||||
if (!canSubscribe) {
|
if (!canSubscribe) {
|
||||||
cantSubscribePrompt.open()
|
cantSubscribePrompt.open()
|
||||||
return
|
return
|
||||||
|
@ -197,7 +197,6 @@ let ProfileHeaderLabeler = ({
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
paddingVertical: 12,
|
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
isSubscribed || !canSubscribe
|
isSubscribed || !canSubscribe
|
||||||
? state.hovered || state.pressed
|
? state.hovered || state.pressed
|
||||||
|
@ -207,7 +206,8 @@ let ProfileHeaderLabeler = ({
|
||||||
? tokens.color.temp_purple_dark
|
? tokens.color.temp_purple_dark
|
||||||
: tokens.color.temp_purple,
|
: tokens.color.temp_purple,
|
||||||
},
|
},
|
||||||
a.px_lg,
|
a.py_sm,
|
||||||
|
a.px_md,
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
a.gap_sm,
|
a.gap_sm,
|
||||||
]}>
|
]}>
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
@ -156,7 +157,14 @@ let ProfileHeaderStandard = ({
|
||||||
style={[a.px_lg, a.pt_md, a.pb_sm]}
|
style={[a.px_lg, a.pt_md, a.pb_sm]}
|
||||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||||
<View
|
<View
|
||||||
style={[a.flex_row, a.justify_end, a.gap_sm, a.pb_sm]}
|
style={[
|
||||||
|
{paddingLeft: 90},
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_end,
|
||||||
|
a.gap_sm,
|
||||||
|
a.pb_sm,
|
||||||
|
a.flex_wrap,
|
||||||
|
]}
|
||||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||||
{isMe ? (
|
{isMe ? (
|
||||||
<Button
|
<Button
|
||||||
|
@ -166,7 +174,7 @@ let ProfileHeaderStandard = ({
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onPress={onPressEditProfile}
|
onPress={onPressEditProfile}
|
||||||
label={_(msg`Edit profile`)}
|
label={_(msg`Edit profile`)}
|
||||||
style={a.rounded_full}>
|
style={[a.rounded_full, a.py_sm]}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Edit Profile</Trans>
|
<Trans>Edit Profile</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
|
@ -181,7 +189,7 @@ let ProfileHeaderStandard = ({
|
||||||
label={_(msg`Unblock`)}
|
label={_(msg`Unblock`)}
|
||||||
disabled={!hasSession}
|
disabled={!hasSession}
|
||||||
onPress={() => unblockPromptControl.open()}
|
onPress={() => unblockPromptControl.open()}
|
||||||
style={a.rounded_full}>
|
style={[a.rounded_full, a.py_sm]}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans context="action">Unblock</Trans>
|
<Trans context="action">Unblock</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
|
@ -190,24 +198,30 @@ let ProfileHeaderStandard = ({
|
||||||
) : !profile.viewer?.blockedBy ? (
|
) : !profile.viewer?.blockedBy ? (
|
||||||
<>
|
<>
|
||||||
{hasSession && (
|
{hasSession && (
|
||||||
<Button
|
<>
|
||||||
testID="suggestedFollowsBtn"
|
<MessageProfileButton profile={profile} />
|
||||||
size="small"
|
<Button
|
||||||
color={showSuggestedFollows ? 'primary' : 'secondary'}
|
testID="suggestedFollowsBtn"
|
||||||
variant="solid"
|
size="small"
|
||||||
shape="round"
|
color={showSuggestedFollows ? 'primary' : 'secondary'}
|
||||||
onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
|
variant="solid"
|
||||||
label={_(msg`Show follows similar to ${profile.handle}`)}>
|
shape="round"
|
||||||
<FontAwesomeIcon
|
onPress={() =>
|
||||||
icon="user-plus"
|
setShowSuggestedFollows(!showSuggestedFollows)
|
||||||
style={
|
|
||||||
showSuggestedFollows
|
|
||||||
? {color: t.palette.white}
|
|
||||||
: t.atoms.text
|
|
||||||
}
|
}
|
||||||
size={14}
|
label={_(msg`Show follows similar to ${profile.handle}`)}
|
||||||
/>
|
style={{width: 36, height: 36}}>
|
||||||
</Button>
|
<FontAwesomeIcon
|
||||||
|
icon="user-plus"
|
||||||
|
style={
|
||||||
|
showSuggestedFollows
|
||||||
|
? {color: t.palette.white}
|
||||||
|
: t.atoms.text
|
||||||
|
}
|
||||||
|
size={14}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -223,7 +237,7 @@ let ProfileHeaderStandard = ({
|
||||||
onPress={
|
onPress={
|
||||||
profile.viewer?.following ? onPressUnfollow : onPressFollow
|
profile.viewer?.following ? onPressUnfollow : onPressFollow
|
||||||
}
|
}
|
||||||
style={[a.rounded_full, a.gap_xs]}>
|
style={[a.rounded_full, a.gap_xs, a.py_sm]}>
|
||||||
<ButtonIcon
|
<ButtonIcon
|
||||||
position="left"
|
position="left"
|
||||||
icon={profile.viewer?.following ? Check : Plus}
|
icon={profile.viewer?.following ? Check : Plus}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import {ChatBskyConvoGetConvoForMembers} from '@atproto/api'
|
import {ChatBskyConvoGetConvoForMembers} from '@atproto/api'
|
||||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
import {STALE} from '..'
|
||||||
import {RQKEY as CONVO_KEY} from './conversation'
|
import {RQKEY as CONVO_KEY} from './conversation'
|
||||||
|
|
||||||
|
const RQKEY_ROOT = 'convo-for-user'
|
||||||
|
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||||
|
|
||||||
export function useGetConvoForMembers({
|
export function useGetConvoForMembers({
|
||||||
onSuccess,
|
onSuccess,
|
||||||
onError,
|
onError,
|
||||||
|
@ -35,3 +39,29 @@ export function useGetConvoForMembers({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the conversation ID for a given DID. Returns null if it's not possible to message them.
|
||||||
|
*/
|
||||||
|
export function useMaybeConvoForUser(did: string) {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: RQKEY(did),
|
||||||
|
queryFn: async () => {
|
||||||
|
const convo = await getAgent()
|
||||||
|
.api.chat.bsky.convo.getConvoForMembers(
|
||||||
|
{members: [did]},
|
||||||
|
{headers: DM_SERVICE_HEADERS},
|
||||||
|
)
|
||||||
|
.catch(() => ({success: null}))
|
||||||
|
|
||||||
|
if (convo.success) {
|
||||||
|
return convo.data.convo.id
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
staleTime: STALE.INFINITY,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -1,41 +1,42 @@
|
||||||
import React, {memo} from 'react'
|
import React, {memo} from 'react'
|
||||||
import {TouchableOpacity} from 'react-native'
|
import {TouchableOpacity} from 'react-native'
|
||||||
import {AppBskyActorDefs} from '@atproto/api'
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
import * as Toast from 'view/com/util/Toast'
|
|
||||||
import {EventStopper} from 'view/com/util/EventStopper'
|
import {logger} from '#/logger'
|
||||||
import {useSession} from 'state/session'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import * as Menu from '#/components/Menu'
|
|
||||||
import {useTheme} from '#/alf'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {HITSLOP_10} from 'lib/constants'
|
import {HITSLOP_10} from 'lib/constants'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
import {shareUrl} from 'lib/sharing'
|
import {shareUrl} from 'lib/sharing'
|
||||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {Shadow} from 'state/cache/types'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
|
||||||
import {useModalControls} from 'state/modals'
|
import {useModalControls} from 'state/modals'
|
||||||
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
|
||||||
import {
|
import {
|
||||||
RQKEY as profileQueryKey,
|
RQKEY as profileQueryKey,
|
||||||
useProfileBlockMutationQueue,
|
useProfileBlockMutationQueue,
|
||||||
useProfileFollowMutationQueue,
|
useProfileFollowMutationQueue,
|
||||||
useProfileMuteMutationQueue,
|
useProfileMuteMutationQueue,
|
||||||
} from 'state/queries/profile'
|
} from 'state/queries/profile'
|
||||||
|
import {useSession} from 'state/session'
|
||||||
|
import {EventStopper} from 'view/com/util/EventStopper'
|
||||||
|
import * as Toast from 'view/com/util/Toast'
|
||||||
|
import {useTheme} from '#/alf'
|
||||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||||
|
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
||||||
import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle'
|
import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle'
|
||||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
|
||||||
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
|
||||||
import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck'
|
import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck'
|
||||||
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
|
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
|
||||||
import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
|
|
||||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
import {logger} from '#/logger'
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||||
import {Shadow} from 'state/cache/types'
|
import * as Menu from '#/components/Menu'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||||
|
|
||||||
let ProfileMenu = ({
|
let ProfileMenu = ({
|
||||||
profile,
|
profile,
|
||||||
|
@ -192,9 +193,8 @@ let ProfileMenu = ({
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingVertical: 10,
|
padding: 8,
|
||||||
borderRadius: 50,
|
borderRadius: 50,
|
||||||
paddingHorizontal: 16,
|
|
||||||
},
|
},
|
||||||
pal.btn,
|
pal.btn,
|
||||||
]}>
|
]}>
|
||||||
|
|
Loading…
Reference in New Issue