diff options
-rw-r--r-- | src/components/NewskieDialog.tsx | 210 | ||||
-rw-r--r-- | src/components/StarterPack/QrCode.tsx | 20 | ||||
-rw-r--r-- | src/components/StarterPack/QrCodeDialog.tsx | 113 | ||||
-rw-r--r-- | src/components/StarterPack/ShareDialog.tsx | 54 | ||||
-rw-r--r-- | src/components/dialogs/EmbedConsent.tsx | 6 | ||||
-rw-r--r-- | src/screens/Settings/components/ExportCarDialog.tsx | 13 |
6 files changed, 221 insertions, 195 deletions
diff --git a/src/components/NewskieDialog.tsx b/src/components/NewskieDialog.tsx index 0644ba704..30f70f549 100644 --- a/src/components/NewskieDialog.tsx +++ b/src/components/NewskieDialog.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {useMemo, useState} from 'react' import {View} from 'react-native' import {type AppBskyActorDefs, moderateProfile} from '@atproto/api' import {msg, Trans} from '@lingui/macro' @@ -27,30 +27,12 @@ export function NewskieDialog({ disabled?: boolean }) { const {_} = useLingui() - const t = useTheme() - const moderationOpts = useModerationOpts() - const {currentAccount} = useSession() - const timeAgo = useGetTimeAgo() const control = useDialogControl() - const isMe = profile.did === currentAccount?.did const createdAt = profile.createdAt as string | undefined - const profileName = React.useMemo(() => { - const name = profile.displayName || profile.handle - - if (isMe) { - return _(msg`You`) - } - - if (!moderationOpts) return name - const moderation = moderateProfile(profile, moderationOpts) - - return sanitizeDisplayName(name, moderation.ui('displayName')) - }, [_, isMe, moderationOpts, profile]) - - const [now] = React.useState(() => Date.now()) - const daysOld = React.useMemo(() => { + const [now] = useState(() => Date.now()) + const daysOld = useMemo(() => { if (!createdAt) return Infinity return differenceInSeconds(now, new Date(createdAt)) / 86400 }, [createdAt, now]) @@ -77,88 +59,116 @@ export function NewskieDialog({ )} </Button> - <Dialog.Outer control={control}> + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> <Dialog.Handle /> - <Dialog.ScrollableInner - label={_(msg`New user info dialog`)} - style={web({width: 'auto', maxWidth: 400, minWidth: 200})}> - <View style={[a.gap_md]}> - <View style={[a.align_center]}> - <View - style={[ - { - height: 60, - width: 64, - }, - ]}> - <Newskie - width={64} - height={64} - fill="#FFC404" - style={[a.absolute, a.inset_0]} - /> - </View> - <Text style={[a.font_bold, a.text_xl]}> - {isMe ? ( - <Trans>Welcome, friend!</Trans> - ) : ( - <Trans>Say hello!</Trans> - )} - </Text> - </View> - <Text style={[a.text_md, a.text_center, a.leading_snug]}> - {profile.joinedViaStarterPack ? ( - <Trans> - {profileName} joined Bluesky using a starter pack{' '} - {timeAgo(createdAt, now, {format: 'long'})} ago - </Trans> - ) : ( - <Trans> - {profileName} joined Bluesky{' '} - {timeAgo(createdAt, now, {format: 'long'})} ago - </Trans> - )} - </Text> - {profile.joinedViaStarterPack ? ( - <StarterPackCard.Link - starterPack={profile.joinedViaStarterPack} - onPress={() => { - control.close() - }}> - <View - style={[ - a.w_full, - a.mt_sm, - a.p_lg, - a.border, - a.rounded_sm, - t.atoms.border_contrast_low, - ]}> - <StarterPackCard.Card - starterPack={profile.joinedViaStarterPack} - /> - </View> - </StarterPackCard.Link> - ) : null} + <DialogInner profile={profile} createdAt={createdAt} now={now} /> + </Dialog.Outer> + </View> + ) +} - {isNative && ( - <Button - label={_(msg`Close`)} - variant="solid" - color="secondary" - size="small" - style={[a.mt_sm]} - onPress={() => control.close()}> - <ButtonText> - <Trans>Close</Trans> - </ButtonText> - </Button> - )} +function DialogInner({ + profile, + createdAt, + now, +}: { + profile: AppBskyActorDefs.ProfileViewDetailed + createdAt: string + now: number +}) { + const control = Dialog.useDialogContext() + const {_} = useLingui() + const t = useTheme() + const moderationOpts = useModerationOpts() + const {currentAccount} = useSession() + const timeAgo = useGetTimeAgo() + const isMe = profile.did === currentAccount?.did + + const profileName = useMemo(() => { + const name = profile.displayName || profile.handle + + if (isMe) { + return _(msg`You`) + } + + if (!moderationOpts) return name + const moderation = moderateProfile(profile, moderationOpts) + + return sanitizeDisplayName(name, moderation.ui('displayName')) + }, [_, isMe, moderationOpts, profile]) + + return ( + <Dialog.ScrollableInner + label={_(msg`New user info dialog`)} + style={web({maxWidth: 400})}> + <View style={[a.gap_md]}> + <View style={[a.align_center]}> + <View + style={[ + { + height: 60, + width: 64, + }, + ]}> + <Newskie + width={64} + height={64} + fill="#FFC404" + style={[a.absolute, a.inset_0]} + /> </View> + <Text style={[a.font_bold, a.text_xl]}> + {isMe ? <Trans>Welcome, friend!</Trans> : <Trans>Say hello!</Trans>} + </Text> + </View> + <Text style={[a.text_md, a.text_center, a.leading_snug]}> + {profile.joinedViaStarterPack ? ( + <Trans> + {profileName} joined Bluesky using a starter pack{' '} + {timeAgo(createdAt, now, {format: 'long'})} ago + </Trans> + ) : ( + <Trans> + {profileName} joined Bluesky{' '} + {timeAgo(createdAt, now, {format: 'long'})} ago + </Trans> + )} + </Text> + {profile.joinedViaStarterPack ? ( + <StarterPackCard.Link + starterPack={profile.joinedViaStarterPack} + onPress={() => control.close()}> + <View + style={[ + a.w_full, + a.mt_sm, + a.p_lg, + a.border, + a.rounded_sm, + t.atoms.border_contrast_low, + ]}> + <StarterPackCard.Card + starterPack={profile.joinedViaStarterPack} + /> + </View> + </StarterPackCard.Link> + ) : null} - <Dialog.Close /> - </Dialog.ScrollableInner> - </Dialog.Outer> - </View> + {isNative && ( + <Button + label={_(msg`Close`)} + color="secondary" + size="small" + style={[a.mt_sm]} + onPress={() => control.close()}> + <ButtonText> + <Trans>Close</Trans> + </ButtonText> + </Button> + )} + </View> + + <Dialog.Close /> + </Dialog.ScrollableInner> ) } diff --git a/src/components/StarterPack/QrCode.tsx b/src/components/StarterPack/QrCode.tsx index 86f1aa1e6..4c28a41c5 100644 --- a/src/components/StarterPack/QrCode.tsx +++ b/src/components/StarterPack/QrCode.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {lazy} from 'react' import {View} from 'react-native' // @ts-expect-error missing types import QRCode from 'react-native-qrcode-styled' @@ -15,20 +15,20 @@ import {LinearGradientBackground} from '#/components/LinearGradientBackground' import {Text} from '#/components/Typography' import * as bsky from '#/types/bsky' -const LazyViewShot = React.lazy( +const LazyViewShot = lazy( // @ts-expect-error dynamic import () => import('react-native-view-shot/src/index'), ) -interface Props { +export function QrCode({ + starterPack, + link, + ref, +}: { starterPack: AppBskyGraphDefs.StarterPackView link: string -} - -export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode( - {starterPack, link}, - ref, -) { + ref: React.Ref<ViewShot> +}) { const {record} = starterPack if ( @@ -93,7 +93,7 @@ export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode( </LinearGradientBackground> </LazyViewShot> ) -}) +} export function QrCodeInner({link}: {link: string}) { const t = useTheme() diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx index 6a66e92bd..4c40ccb10 100644 --- a/src/components/StarterPack/QrCodeDialog.tsx +++ b/src/components/StarterPack/QrCodeDialog.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {Suspense, useRef, useState} from 'react' import {View} from 'react-native' import type ViewShot from 'react-native-view-shot' import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' @@ -8,16 +8,18 @@ import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' -import * as Toast from '#/view/com/util/Toast' -import {atoms as a} from '#/alf' -import {Button, ButtonText} from '#/components/Button' +import {atoms as a, useBreakpoints} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {type DialogControlProps} from '#/components/Dialog' +import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox' +import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' +import {FloppyDisk_Stroke2_Corner0_Rounded as FloppyDiskIcon} from '#/components/icons/FloppyDisk' import {Loader} from '#/components/Loader' import {QrCode} from '#/components/StarterPack/QrCode' +import * as Toast from '#/components/Toast' import * as bsky from '#/types/bsky' export function QrCodeDialog({ @@ -30,9 +32,11 @@ export function QrCodeDialog({ control: DialogControlProps }) { const {_} = useLingui() - const [isProcessing, setIsProcessing] = React.useState(false) + const {gtMobile} = useBreakpoints() + const [isSaveProcessing, setIsSaveProcessing] = useState(false) + const [isCopyProcessing, setIsCopyProcessing] = useState(false) - const ref = React.useRef<ViewShot>(null) + const ref = useRef<ViewShot>(null) const getCanvas = (base64: string): Promise<HTMLCanvasElement> => { return new Promise(resolve => { @@ -68,15 +72,14 @@ export function QrCodeDialog({ try { await createAssetAsync(`file://${uri}`) } catch (e: unknown) { - Toast.show( - _(msg`An error occurred while saving the QR code!`), - 'xmark', - ) + Toast.show(_(msg`An error occurred while saving the QR code!`), { + type: 'error', + }) logger.error('Failed to save QR code', {error: e}) return } } else { - setIsProcessing(true) + setIsSaveProcessing(true) if ( !bsky.validate( @@ -101,12 +104,12 @@ export function QrCodeDialog({ link.click() } - logEvent('starterPack:share', { + logger.metric('starterPack:share', { starterPack: starterPack.uri, shareType: 'qrcode', qrShareType: 'save', }) - setIsProcessing(false) + setIsSaveProcessing(false) Toast.show( isWeb ? _(msg`QR code has been downloaded!`) @@ -117,7 +120,7 @@ export function QrCodeDialog({ } const onCopyPress = async () => { - setIsProcessing(true) + setIsCopyProcessing(true) ref.current?.capture?.().then(async (uri: string) => { const canvas = await getCanvas(uri) // @ts-expect-error web only @@ -126,13 +129,13 @@ export function QrCodeDialog({ navigator.clipboard.write([item]) }) - logEvent('starterPack:share', { + logger.metric('starterPack:share', { starterPack: starterPack.uri, shareType: 'qrcode', qrShareType: 'copy', }) Toast.show(_(msg`QR code copied to your clipboard!`)) - setIsProcessing(false) + setIsCopyProcessing(false) control.close() }) } @@ -142,7 +145,7 @@ export function QrCodeDialog({ control.close(() => { Sharing.shareAsync(uri, {mimeType: 'image/png', UTI: 'image/png'}).then( () => { - logEvent('starterPack:share', { + logger.metric('starterPack:share', { starterPack: starterPack.uri, shareType: 'qrcode', qrShareType: 'share', @@ -154,49 +157,57 @@ export function QrCodeDialog({ } return ( - <Dialog.Outer control={control}> + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> <Dialog.Handle /> <Dialog.ScrollableInner label={_(msg`Create a QR code for a starter pack`)}> <View style={[a.flex_1, a.align_center, a.gap_5xl]}> - <React.Suspense fallback={<Loading />}> + <Suspense fallback={<Loading />}> {!link ? ( <Loading /> ) : ( <> <QrCode starterPack={starterPack} link={link} ref={ref} /> - {isProcessing ? ( - <View> - <Loader size="xl" /> - </View> - ) : ( - <View - style={[a.w_full, a.gap_md, isWeb && [a.flex_row_reverse]]}> - <Button - label={_(msg`Copy QR code`)} - variant="solid" - color="secondary" - size="small" - onPress={isWeb ? onCopyPress : onSharePress}> - <ButtonText> - {isWeb ? <Trans>Copy</Trans> : <Trans>Share</Trans>} - </ButtonText> - </Button> - <Button - label={_(msg`Save QR code`)} - variant="solid" - color="secondary" - size="small" - onPress={onSavePress}> - <ButtonText> - <Trans>Save</Trans> - </ButtonText> - </Button> - </View> - )} + <View + style={[ + a.w_full, + a.gap_md, + gtMobile && [a.flex_row, a.justify_center, a.flex_wrap], + ]}> + <Button + label={_(msg`Copy QR code`)} + color="primary_subtle" + size="large" + onPress={isWeb ? onCopyPress : onSharePress}> + <ButtonIcon + icon={ + isCopyProcessing + ? Loader + : isWeb + ? ChainLinkIcon + : ShareIcon + } + /> + <ButtonText> + {isWeb ? <Trans>Copy</Trans> : <Trans>Share</Trans>} + </ButtonText> + </Button> + <Button + label={_(msg`Save QR code`)} + color="secondary" + size="large" + onPress={onSavePress}> + <ButtonIcon + icon={isSaveProcessing ? Loader : FloppyDiskIcon} + /> + <ButtonText> + <Trans>Save</Trans> + </ButtonText> + </Button> + </View> </> )} - </React.Suspense> + </Suspense> </View> <Dialog.Close /> </Dialog.ScrollableInner> @@ -206,7 +217,7 @@ export function QrCodeDialog({ function Loading() { return ( - <View style={[a.align_center, a.p_xl]}> + <View style={[a.align_center, a.justify_center, {minHeight: 400}]}> <Loader size="xl" /> </View> ) diff --git a/src/components/StarterPack/ShareDialog.tsx b/src/components/StarterPack/ShareDialog.tsx index c159b42dd..32932fe2d 100644 --- a/src/components/StarterPack/ShareDialog.tsx +++ b/src/components/StarterPack/ShareDialog.tsx @@ -4,16 +4,18 @@ import {type AppBskyGraphDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useSaveImageToMediaLibrary} from '#/lib/media/save-image' import {shareUrl} from '#/lib/sharing' -import {logEvent} from '#/lib/statsig/statsig' import {getStarterPackOgCard} from '#/lib/strings/starter-pack' +import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' -import {atoms as a, useTheme} from '#/alf' -import {Button, ButtonText} from '#/components/Button' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {type DialogControlProps} from '#/components/Dialog' import * as Dialog from '#/components/Dialog' +import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' +import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download' +import {QrCode_Stroke2_Corner0_Rounded as QrCodeIcon} from '#/components/icons/QrCode' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' @@ -27,7 +29,9 @@ interface Props { export function ShareDialog(props: Props) { return ( - <Dialog.Outer control={props.control}> + <Dialog.Outer + control={props.control} + nativeOptions={{preventExpansion: true}}> <Dialog.Handle /> <ShareDialogInner {...props} /> </Dialog.Outer> @@ -43,14 +47,14 @@ function ShareDialogInner({ }: Props) { const {_} = useLingui() const t = useTheme() - const {isTabletOrDesktop} = useWebMediaQueries() + const {gtMobile} = useBreakpoints() const imageUrl = getStarterPackOgCard(starterPack) const onShareLink = async () => { if (!link) return shareUrl(link) - logEvent('starterPack:share', { + logger.metric('starterPack:share', { starterPack: starterPack.uri, shareType: 'link', }) @@ -67,12 +71,12 @@ function ShareDialogInner({ <> <Dialog.ScrollableInner label={_(msg`Share link dialog`)}> {!imageLoaded || !link ? ( - <View style={[a.p_xl, a.align_center]}> + <View style={[a.align_center, a.justify_center, {minHeight: 350}]}> <Loader size="xl" /> </View> ) : ( - <View style={[!isTabletOrDesktop && a.gap_lg]}> - <View style={[a.gap_sm, isTabletOrDesktop && a.pb_lg]}> + <View style={[!gtMobile && a.gap_lg]}> + <View style={[a.gap_sm, gtMobile && a.pb_lg]}> <Text style={[a.font_bold, a.text_2xl]}> <Trans>Invite people to this starter pack!</Trans> </Text> @@ -89,8 +93,8 @@ function ShareDialogInner({ a.rounded_sm, { aspectRatio: 1200 / 630, - transform: [{scale: isTabletOrDesktop ? 0.85 : 1}], - marginTop: isTabletOrDesktop ? -20 : 0, + transform: [{scale: gtMobile ? 0.85 : 1}], + marginTop: gtMobile ? -20 : 0, }, ]} accessibilityIgnoresInvertColors={true} @@ -98,30 +102,33 @@ function ShareDialogInner({ <View style={[ a.gap_md, - isWeb && [a.gap_sm, a.flex_row_reverse, {marginLeft: 'auto'}], + gtMobile && [ + a.gap_sm, + a.justify_center, + a.flex_row, + a.flex_wrap, + ], ]}> <Button label={isWeb ? _(msg`Copy link`) : _(msg`Share link`)} - variant="solid" - color="secondary" - size="small" - style={[isWeb && a.self_center]} + color="primary_subtle" + size="large" onPress={onShareLink}> + <ButtonIcon icon={ChainLinkIcon} /> <ButtonText> {isWeb ? <Trans>Copy Link</Trans> : <Trans>Share link</Trans>} </ButtonText> </Button> <Button label={_(msg`Share QR code`)} - variant="solid" - color="secondary" - size="small" - style={[isWeb && a.self_center]} + color="primary_subtle" + size="large" onPress={() => { control.close(() => { qrDialogControl.open() }) }}> + <ButtonIcon icon={QrCodeIcon} /> <ButtonText> <Trans>Share QR code</Trans> </ButtonText> @@ -129,11 +136,10 @@ function ShareDialogInner({ {isNative && ( <Button label={_(msg`Save image`)} - variant="ghost" color="secondary" - size="small" - style={[isWeb && a.self_center]} + size="large" onPress={onSave}> + <ButtonIcon icon={DownloadIcon} /> <ButtonText> <Trans>Save image</Trans> </ButtonText> diff --git a/src/components/dialogs/EmbedConsent.tsx b/src/components/dialogs/EmbedConsent.tsx index 086d43f95..fe8609544 100644 --- a/src/components/dialogs/EmbedConsent.tsx +++ b/src/components/dialogs/EmbedConsent.tsx @@ -10,9 +10,9 @@ import { } from '#/lib/strings/embed-player' import {useSetExternalEmbedPref} from '#/state/preferences' import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {Button, ButtonText} from '../Button' -import {Text} from '../Typography' +import {Text} from '#/components/Typography' export function EmbedConsentDialog({ control, @@ -48,7 +48,7 @@ export function EmbedConsentDialog({ }, [control, setExternalEmbedPref, source]) return ( - <Dialog.Outer control={control}> + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> <Dialog.Handle /> <Dialog.ScrollableInner label={_(msg`External Media`)} diff --git a/src/screens/Settings/components/ExportCarDialog.tsx b/src/screens/Settings/components/ExportCarDialog.tsx index edeccd128..d5131c5c6 100644 --- a/src/screens/Settings/components/ExportCarDialog.tsx +++ b/src/screens/Settings/components/ExportCarDialog.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {useCallback, useState} from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -18,14 +18,14 @@ import {Text} from '#/components/Typography' export function ExportCarDialog({ control, }: { - control: Dialog.DialogOuterProps['control'] + control: Dialog.DialogControlProps }) { const {_} = useLingui() const t = useTheme() const agent = useAgent() - const [loading, setLoading] = React.useState(false) + const [loading, setLoading] = useState(false) - const download = React.useCallback(async () => { + const download = useCallback(async () => { if (!agent.session) { return // shouldnt ever happen } @@ -52,7 +52,7 @@ export function ExportCarDialog({ }, [_, control, agent]) return ( - <Dialog.Outer control={control}> + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> <Dialog.Handle /> <Dialog.ScrollableInner accessibilityDescribedBy="dialog-description" @@ -63,7 +63,7 @@ export function ExportCarDialog({ </Text> <Text nativeID="dialog-description" - style={[a.text_sm, a.leading_normal, t.atoms.text_contrast_high]}> + style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_high]}> <Trans> Your account repository, containing all public data records, can be downloaded as a "CAR" file. This file does not include media @@ -73,7 +73,6 @@ export function ExportCarDialog({ </Text> <Button - variant="solid" color="primary" size="large" label={_(msg`Download CAR file`)} |