Germ DM button (#9848)
* germ link (wip) * add todos * just yeet declaration for now * ensure not proxied * allow undo delete * tweak styles * ignore unknown values * tweak styles * fix boolean logic * skip IAB * fix mutationFn * add link warning interstitial * Logging and metrics * Little more error handling * Update copy --------- Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
@@ -875,4 +875,9 @@ export type Events = {
|
|||||||
'liveEvents:unhideAllFeedBanners': {
|
'liveEvents:unhideAllFeedBanners': {
|
||||||
context: LiveEventFeedMetricContext
|
context: LiveEventFeedMetricContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
'profile:associated:germ:click-to-chat': {}
|
||||||
|
'profile:associated:germ:click-self-info': {}
|
||||||
|
'profile:associated:germ:self-disconnect': {}
|
||||||
|
'profile:associated:germ:self-reconnect': {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,27 @@ export function LinkWarningDialog() {
|
|||||||
webOptions={{alignCenter: true}}
|
webOptions={{alignCenter: true}}
|
||||||
onClose={linkWarningDialogControl.clear}>
|
onClose={linkWarningDialogControl.clear}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<InAppBrowserConsentInner link={linkWarningDialogControl.value} />
|
<LinkWarningDialogInner link={linkWarningDialogControl.value} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function InAppBrowserConsentInner({
|
export function CustomLinkWarningDialog({
|
||||||
|
control,
|
||||||
|
link,
|
||||||
|
}: {
|
||||||
|
control: Dialog.DialogControlProps
|
||||||
|
link?: {href: string; displayText: string; share?: boolean}
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
<LinkWarningDialogInner link={link} />
|
||||||
|
</Dialog.Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkWarningDialogInner({
|
||||||
link,
|
link,
|
||||||
}: {
|
}: {
|
||||||
link?: {href: string; displayText: string; share?: boolean}
|
link?: {href: string; displayText: string; share?: boolean}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import * as Toast from '#/components/Toast'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
||||||
import {IS_IOS} from '#/env'
|
import {IS_IOS} from '#/env'
|
||||||
|
import {GermButton} from '../components/GermButton'
|
||||||
import {EditProfileDialog} from './EditProfileDialog'
|
import {EditProfileDialog} from './EditProfileDialog'
|
||||||
import {ProfileHeaderHandle} from './Handle'
|
import {ProfileHeaderHandle} from './Handle'
|
||||||
import {ProfileHeaderMetrics} from './Metrics'
|
import {ProfileHeaderMetrics} from './Metrics'
|
||||||
@@ -163,6 +164,10 @@ let ProfileHeaderStandard = ({
|
|||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
|
{profile.associated?.germ && (
|
||||||
|
<GermButton germ={profile.associated.germ} profile={profile} />
|
||||||
|
)}
|
||||||
|
|
||||||
{!isMe &&
|
{!isMe &&
|
||||||
!isBlockedUser &&
|
!isBlockedUser &&
|
||||||
shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
|
shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
import {Platform, View} from 'react-native'
|
||||||
|
import {Image} from 'expo-image'
|
||||||
|
import {
|
||||||
|
type AppBskyActorDefs,
|
||||||
|
type AppBskyActorGetProfile,
|
||||||
|
type AtpAgent,
|
||||||
|
} from '@atproto/api'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {until} from '#/lib/async/until'
|
||||||
|
import {isNetworkError} from '#/lib/strings/errors'
|
||||||
|
import {RQKEY} from '#/state/queries/profile'
|
||||||
|
import {useAgent, useSession} from '#/state/session'
|
||||||
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {CustomLinkWarningDialog} from '#/components/dialogs/LinkWarning'
|
||||||
|
import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRightIcon} from '#/components/icons/Arrow'
|
||||||
|
import {Link} from '#/components/Link'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import * as Toast from '#/components/Toast'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
|
export function GermButton({
|
||||||
|
germ,
|
||||||
|
profile,
|
||||||
|
}: {
|
||||||
|
germ: AppBskyActorDefs.ProfileAssociatedGerm
|
||||||
|
profile: bsky.profile.AnyProfileView
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const linkWarningControl = Dialog.useDialogControl()
|
||||||
|
|
||||||
|
// exclude `none` and all unknown values
|
||||||
|
if (
|
||||||
|
!(germ.showButtonTo === 'everyone' || germ.showButtonTo === 'usersIFollow')
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentAccount?.did === profile.did) {
|
||||||
|
return <GermSelfButton did={currentAccount.did} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (germ.showButtonTo === 'usersIFollow' && !profile.viewer?.followedBy) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = constructGermUrl(germ, profile, currentAccount?.did)
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
to={url}
|
||||||
|
onPress={evt => {
|
||||||
|
ax.metric('profile:associated:germ:click-to-chat', {})
|
||||||
|
if (isCustomGermDomain(url)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
linkWarningControl.open()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
label={_(msg`Open Germ DM`)}
|
||||||
|
overridePresentation={false}
|
||||||
|
shouldProxy={false}
|
||||||
|
style={[
|
||||||
|
t.atoms.bg_contrast_50,
|
||||||
|
a.rounded_full,
|
||||||
|
a.self_start,
|
||||||
|
{padding: 6},
|
||||||
|
]}>
|
||||||
|
<GermLogo size="small" />
|
||||||
|
<Text style={[a.text_sm, a.font_medium, a.ml_xs]}>
|
||||||
|
<Trans>Germ DM</Trans>
|
||||||
|
</Text>
|
||||||
|
<ArrowTopRightIcon style={[t.atoms.text, a.mx_2xs]} width={14} />
|
||||||
|
</Link>
|
||||||
|
<CustomLinkWarningDialog
|
||||||
|
control={linkWarningControl}
|
||||||
|
link={{
|
||||||
|
href: url,
|
||||||
|
displayText: '',
|
||||||
|
share: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function GermLogo({size}: {size: 'small' | 'large'}) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
source={require('../../../../assets/images/germ_logo.webp')}
|
||||||
|
accessibilityIgnoresInvertColors={false}
|
||||||
|
contentFit="cover"
|
||||||
|
style={[
|
||||||
|
a.rounded_full,
|
||||||
|
size === 'large' ? {width: 32, height: 32} : {width: 16, height: 16},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function GermSelfButton({did}: {did: string}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const selfExplanationDialogControl = Dialog.useDialogControl()
|
||||||
|
const agent = useAgent()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const {mutate: deleteDeclaration, isPending} = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const previousRecord = await agent.com.germnetwork.declaration
|
||||||
|
.get({
|
||||||
|
repo: did,
|
||||||
|
rkey: 'self',
|
||||||
|
})
|
||||||
|
.then(res => res.value)
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
await agent.com.germnetwork.declaration.delete({
|
||||||
|
repo: did,
|
||||||
|
rkey: 'self',
|
||||||
|
})
|
||||||
|
|
||||||
|
await whenAppViewReady(agent, did, res => !res.data.associated?.germ)
|
||||||
|
|
||||||
|
return previousRecord
|
||||||
|
},
|
||||||
|
onSuccess: previousRecord => {
|
||||||
|
ax.metric('profile:associated:germ:self-disconnect', {})
|
||||||
|
|
||||||
|
async function undo() {
|
||||||
|
if (!previousRecord) return
|
||||||
|
try {
|
||||||
|
await agent.com.germnetwork.declaration.put(
|
||||||
|
{
|
||||||
|
repo: did,
|
||||||
|
rkey: 'self',
|
||||||
|
},
|
||||||
|
previousRecord,
|
||||||
|
)
|
||||||
|
await whenAppViewReady(agent, did, res => !!res.data.associated?.germ)
|
||||||
|
await queryClient.refetchQueries({queryKey: RQKEY(did)})
|
||||||
|
|
||||||
|
Toast.show(_(msg`Germ DM reconnected`))
|
||||||
|
ax.metric('profile:associated:germ:self-reconnect', {})
|
||||||
|
} catch (e: any) {
|
||||||
|
Toast.show(
|
||||||
|
_(msg`Failed to reconnect Germ DM. Error: ${e?.message}`),
|
||||||
|
{
|
||||||
|
type: 'error',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (!isNetworkError(e)) {
|
||||||
|
ax.logger.error('Failed to reconnect Germ DM link', {
|
||||||
|
safeMessage: e,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selfExplanationDialogControl.close(() => {
|
||||||
|
void queryClient.refetchQueries({queryKey: RQKEY(did)})
|
||||||
|
Toast.show(
|
||||||
|
<Toast.Outer>
|
||||||
|
<Toast.Icon />
|
||||||
|
<Toast.Text>
|
||||||
|
<Trans>Germ DM disconnected</Trans>
|
||||||
|
</Toast.Text>
|
||||||
|
{previousRecord && (
|
||||||
|
<Toast.Action label={_(msg`Undo`)} onPress={() => void undo()}>
|
||||||
|
<Trans>Undo</Trans>
|
||||||
|
</Toast.Action>
|
||||||
|
)}
|
||||||
|
</Toast.Outer>,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: error => {
|
||||||
|
Toast.show(
|
||||||
|
_(msg`Failed to disconnect Germ DM. Error: ${error?.message}`),
|
||||||
|
{
|
||||||
|
type: 'error',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (!isNetworkError(error)) {
|
||||||
|
ax.logger.error('Failed to disconnect Germ DM link', {
|
||||||
|
safeMessage: error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Learn more about your Germ DM link`)}
|
||||||
|
onPress={() => {
|
||||||
|
ax.metric('profile:associated:germ:click-self-info', {})
|
||||||
|
selfExplanationDialogControl.open()
|
||||||
|
}}
|
||||||
|
style={[
|
||||||
|
t.atoms.bg_contrast_50,
|
||||||
|
a.rounded_full,
|
||||||
|
a.self_start,
|
||||||
|
{padding: 6, paddingRight: 10},
|
||||||
|
]}>
|
||||||
|
<GermLogo size="small" />
|
||||||
|
<Text style={[a.text_sm, a.font_medium, a.ml_xs]}>
|
||||||
|
<Trans>Germ DM</Trans>
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dialog.Outer
|
||||||
|
control={selfExplanationDialogControl}
|
||||||
|
nativeOptions={{preventExpansion: true}}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
<Dialog.ScrollableInner
|
||||||
|
label={_(msg`Germ DM Link`)}
|
||||||
|
style={web([{maxWidth: 400, borderRadius: 36}])}>
|
||||||
|
<View style={[a.flex_row, a.align_center, {gap: 6}]}>
|
||||||
|
<GermLogo size="large" />
|
||||||
|
<Text style={[a.text_2xl, a.font_bold]}>
|
||||||
|
<Trans>Germ DM Link</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={[a.text_md, a.leading_snug, a.mt_sm]}>
|
||||||
|
<Trans>
|
||||||
|
This button lets others open the Germ DM app to send you a
|
||||||
|
message. You can manage its visibility from the Germ DM app, or
|
||||||
|
you can disconnect your Bluesky account from Germ DM altogether by
|
||||||
|
clicking the button below.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[a.mt_2xl, a.gap_md]}>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Got it`)}
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
onPress={() => selfExplanationDialogControl.close()}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Got it</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Disconnect Germ DM`)}
|
||||||
|
size="large"
|
||||||
|
color="secondary"
|
||||||
|
onPress={() => deleteDeclaration()}
|
||||||
|
disabled={isPending}>
|
||||||
|
{isPending && <ButtonIcon icon={Loader} />}
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Disconnect Germ DM</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
</Dialog.Outer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructGermUrl(
|
||||||
|
declaration: AppBskyActorDefs.ProfileAssociatedGerm,
|
||||||
|
profile: bsky.profile.AnyProfileView,
|
||||||
|
viewerDid?: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const urlp = new URL(declaration.messageMeUrl)
|
||||||
|
|
||||||
|
if (urlp.pathname.endsWith('/')) {
|
||||||
|
urlp.pathname = urlp.pathname.slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
urlp.pathname += `/${platform()}`
|
||||||
|
|
||||||
|
if (viewerDid) {
|
||||||
|
urlp.hash = `#${profile.did}+${viewerDid}`
|
||||||
|
} else {
|
||||||
|
urlp.hash = `#${profile.did}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlp.toString()
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCustomGermDomain(url: string) {
|
||||||
|
try {
|
||||||
|
const urlp = new URL(url)
|
||||||
|
return urlp.hostname !== 'landing.ger.mx'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function platform() {
|
||||||
|
switch (Platform.OS) {
|
||||||
|
case 'ios':
|
||||||
|
return 'iOS'
|
||||||
|
case 'android':
|
||||||
|
return 'android'
|
||||||
|
default:
|
||||||
|
return 'web'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function whenAppViewReady(
|
||||||
|
agent: AtpAgent,
|
||||||
|
actor: string,
|
||||||
|
fn: (res: AppBskyActorGetProfile.Response) => boolean,
|
||||||
|
) {
|
||||||
|
await until(
|
||||||
|
5, // 5 tries
|
||||||
|
1e3, // 1s delay between tries
|
||||||
|
fn,
|
||||||
|
() => agent.app.bsky.actor.getProfile({actor}),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -430,7 +430,7 @@ export function useRequireAuth() {
|
|||||||
const {signinDialogControl} = useGlobalDialogsControlContext()
|
const {signinDialogControl} = useGlobalDialogsControlContext()
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
(fn: () => void) => {
|
(fn: () => unknown) => {
|
||||||
if (hasSession) {
|
if (hasSession) {
|
||||||
fn()
|
fn()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user