Invite friends QA polish
- Drawer: move the share button inline with the display name (right-aligned) and keep the drawer open when opening the share sheet - Settings: remove the redundant "Find and invite friends" item and rename the existing contacts link to "Find and invite friends" - Invite dialog: add a "Done" button and a full-width header divider using the Dialog.Header renderLeft + edge-to-edge content padding pattern - NUX announcement: wire up the promo illustration as the full-bleed header background with the "New Feature" tag overlaid - Fix "Try it": mount the invite dialog persistently in NuxDialogs so it survives the announcement being dismissed, then open it after the handoff
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -1,6 +1,6 @@
|
||||
import {useCallback, useState} from 'react'
|
||||
import {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {Image} from 'expo-image'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -12,7 +12,6 @@ import {useNuxDialogContext} from '#/components/dialogs/nuxs'
|
||||
import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_E2E, IS_NATIVE, IS_WEB} from '#/env'
|
||||
import {InviteFriendsDialog} from '#/features/inviteFriends'
|
||||
import {createIsEnabledCheck} from './utils'
|
||||
|
||||
export const enabled = createIsEnabledCheck(() => {
|
||||
@@ -26,25 +25,23 @@ export function InviteFriendsAnnouncement() {
|
||||
const {_} = useLingui()
|
||||
const nuxDialogs = useNuxDialogContext()
|
||||
const control = Dialog.useDialogControl()
|
||||
const inviteFriendsControl = Dialog.useDialogControl()
|
||||
const [openInviteAfterClose, setOpenInviteAfterClose] = useState(false)
|
||||
|
||||
Dialog.useAutoOpen(control)
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
if (openInviteAfterClose) {
|
||||
// Defer to the next tick so the close animation completes before
|
||||
// we open the next dialog (avoids visual glitch on iOS).
|
||||
setOpenInviteAfterClose(false)
|
||||
inviteFriendsControl.open()
|
||||
}
|
||||
}, [nuxDialogs, openInviteAfterClose, inviteFriendsControl])
|
||||
}, [nuxDialogs])
|
||||
|
||||
const onPressTryIt = useCallback(() => {
|
||||
setOpenInviteAfterClose(true)
|
||||
control.close()
|
||||
}, [control])
|
||||
// Close this announcement (which dismisses + unmounts the NUX), then open
|
||||
// the invite dialog. The invite dialog is mounted persistently by NuxDialogs
|
||||
// (not here) so it survives the dismissal - the native bottom sheet cannot
|
||||
// hand off to a second sheet mounted in this same subtree. Defer the open to
|
||||
// the next tick so the announcement's teardown completes first.
|
||||
control.close(() => {
|
||||
setTimeout(() => nuxDialogs.openInviteFriends())
|
||||
})
|
||||
}, [control, nuxDialogs])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -62,38 +59,37 @@ export function InviteFriendsAnnouncement() {
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.align_center,
|
||||
a.w_full,
|
||||
a.relative,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
paddingTop: IS_WEB ? 24 : 40,
|
||||
paddingBottom: 16,
|
||||
borderTopLeftRadius: a.rounded_md.borderRadius,
|
||||
borderTopRightRadius: a.rounded_md.borderRadius,
|
||||
},
|
||||
]}>
|
||||
<LinearGradient
|
||||
colors={[t.palette.primary_100, t.palette.primary_200]}
|
||||
locations={[0, 1]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 0, y: 1}}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
{/* The image is the full header background; the tag sits on top of it. */}
|
||||
<Image
|
||||
accessibilityIgnoresInvertColors
|
||||
source={require('../../../../assets/images/invite_friends_announcement_nux.webp')}
|
||||
style={[a.w_full, {aspectRatio: 754 / 440}]}
|
||||
alt={_(
|
||||
msg`An illustration of the Bluesky app with a paper airplane flying out of it, representing inviting friends`,
|
||||
)}
|
||||
/>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<SparkleIcon fill={t.palette.primary_800} size="sm" />
|
||||
<Text style={[a.font_semi_bold, {color: t.palette.primary_800}]}>
|
||||
<Trans>New Feature</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
{/*
|
||||
TODO: drop promo image
|
||||
*/}
|
||||
<View
|
||||
style={{
|
||||
width: 220,
|
||||
height: 140,
|
||||
marginTop: 12,
|
||||
}}
|
||||
/>
|
||||
style={[
|
||||
a.absolute,
|
||||
a.align_center,
|
||||
{top: 0, left: 0, right: 0, paddingTop: IS_WEB ? 24 : 20},
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<SparkleIcon fill={t.palette.primary_800} size="sm" />
|
||||
<Text
|
||||
style={[a.font_semi_bold, {color: t.palette.primary_800}]}>
|
||||
<Trans>New Feature</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[a.align_center, a.px_xl, a.pt_xl, a.gap_2xl, a.pb_sm]}>
|
||||
<View style={[a.gap_sm, a.align_center]}>
|
||||
@@ -151,8 +147,6 @@ export function InviteFriendsAnnouncement() {
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
|
||||
<InviteFriendsDialog control={inviteFriendsControl} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,12 +28,21 @@ import {
|
||||
} from '#/components/dialogs/nuxs/InviteFriendsAnnouncement'
|
||||
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
||||
import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {useGeolocation} from '#/geolocation'
|
||||
import {InviteFriendsDialog} from '#/features/inviteFriends'
|
||||
|
||||
type Context = {
|
||||
activeNux: Nux | undefined
|
||||
dismissActiveNux: () => void
|
||||
/**
|
||||
* Opens the invite-friends dialog. It is mounted persistently by NuxDialogs
|
||||
* (not inside the announcement NUX) so it survives the announcement being
|
||||
* dismissed - the native bottom sheet cannot hand off between two sheets
|
||||
* mounted in the same subtree.
|
||||
*/
|
||||
openInviteFriends: () => void
|
||||
}
|
||||
|
||||
const queuedNuxs: {
|
||||
@@ -53,6 +62,7 @@ const queuedNuxs: {
|
||||
const Context = createContext<Context>({
|
||||
activeNux: undefined,
|
||||
dismissActiveNux: () => {},
|
||||
openInviteFriends: () => {},
|
||||
})
|
||||
Context.displayName = 'NuxDialogContext'
|
||||
|
||||
@@ -105,6 +115,7 @@ function Inner({
|
||||
const [activeNux, setActiveNux] = useState<Nux | undefined>()
|
||||
const {mutateAsync: saveNux} = useSaveNux()
|
||||
const {mutate: resetNuxs} = useResetNuxs()
|
||||
const inviteFriendsControl = Dialog.useDialogControl()
|
||||
|
||||
const snoozeNuxDialog = useCallback(() => {
|
||||
snooze()
|
||||
@@ -188,8 +199,9 @@ function Inner({
|
||||
return {
|
||||
activeNux,
|
||||
dismissActiveNux,
|
||||
openInviteFriends: () => inviteFriendsControl.open(),
|
||||
}
|
||||
}, [activeNux, dismissActiveNux])
|
||||
}, [activeNux, dismissActiveNux, inviteFriendsControl])
|
||||
|
||||
return (
|
||||
<Context.Provider value={ctx}>
|
||||
@@ -198,6 +210,12 @@ function Inner({
|
||||
{activeNux === Nux.InviteFriendsAnnouncement && (
|
||||
<InviteFriendsAnnouncement />
|
||||
)}
|
||||
{/*
|
||||
Mounted persistently (not inside the announcement) so the invite dialog
|
||||
survives the announcement NUX being dismissed during the "Try it"
|
||||
handoff.
|
||||
*/}
|
||||
<InviteFriendsDialog control={inviteFriendsControl} />
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,17 @@ import {InviteFriendsDialogInner} from './InviteFriendsDialogInner'
|
||||
|
||||
export function InviteFriendsDialog({
|
||||
control,
|
||||
onClose,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
onClose?: () => void
|
||||
}) {
|
||||
return (
|
||||
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
onClose={onClose}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
{/* <Dialog.Handle /> */}
|
||||
<InviteFriendsDialogInner control={control} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -5,6 +5,9 @@ import {type DialogControlProps} from '#/components/Dialog'
|
||||
* Callers may still create a Dialog control and pass it; opening it does
|
||||
* nothing on web.
|
||||
*/
|
||||
export function InviteFriendsDialog(_props: {control: DialogControlProps}) {
|
||||
export function InviteFriendsDialog(_props: {
|
||||
control: DialogControlProps
|
||||
onClose?: () => void
|
||||
}) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {logger} from '#/logger'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {Error as ErrorMessage} from '#/components/Error'
|
||||
@@ -120,13 +121,24 @@ export function InviteFriendsDialogInner({
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={l`Invite friends`}
|
||||
contentContainerStyle={[a.pt_sm]}
|
||||
contentContainerStyle={[a.pt_0, a.px_0]}
|
||||
header={
|
||||
<Dialog.Header>
|
||||
<Dialog.Header
|
||||
renderLeft={() => (
|
||||
<Button
|
||||
label={l`Done`}
|
||||
onPress={() => control.close()}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="ghost"
|
||||
style={[a.rounded_full]}>
|
||||
<ButtonText style={[a.text_md]}>{l`Done`}</ButtonText>
|
||||
</Button>
|
||||
)}>
|
||||
<Dialog.HeaderText>{l`Invite Friends`}</Dialog.HeaderText>
|
||||
</Dialog.Header>
|
||||
}>
|
||||
<View style={[a.align_center, a.pt_xl]}>
|
||||
<View style={[a.align_center, a.pt_xl, a.px_xl]}>
|
||||
<ThemePicker value={themeKey} onChange={setThemeKey} />
|
||||
|
||||
<View style={[a.mt_5xl]}>
|
||||
|
||||
@@ -64,7 +64,6 @@ import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env'
|
||||
import {InviteFriendsDialog} from '#/features/inviteFriends'
|
||||
import {useActorStatus} from '#/features/liveNow'
|
||||
import {device, useStorage} from '#/storage'
|
||||
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
|
||||
@@ -77,7 +76,6 @@ export function SettingsScreen({}: Props) {
|
||||
const {logoutEveryAccount} = useSessionApi()
|
||||
const {accounts, currentAccount} = useSession()
|
||||
const switchAccountControl = useDialogControl()
|
||||
const inviteFriendsControl = useDialogControl()
|
||||
const signOutPromptControl = Prompt.usePromptControl()
|
||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||
const {data: otherProfiles} = useProfilesQuery({
|
||||
@@ -210,27 +208,15 @@ export function SettingsScreen({}: Props) {
|
||||
<Trans>Content and media</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
{IS_NATIVE && (
|
||||
<SettingsList.PressableItem
|
||||
label={l`Find and invite friends`}
|
||||
accessibilityHint={l`Opens the invite friends sheet to share your profile`}
|
||||
onPress={() => inviteFriendsControl.open()}>
|
||||
<SettingsList.ItemIcon icon={ContactsIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Find and invite friends</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<SettingsList.Chevron />
|
||||
</SettingsList.PressableItem>
|
||||
)}
|
||||
{IS_NATIVE &&
|
||||
findContactsEnabled &&
|
||||
!ax.features.enabled(ax.features.ImportContactsSettingsDisable) && (
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/find-contacts"
|
||||
label={l`Find friends from contacts`}>
|
||||
label={l`Find and invite friends`}>
|
||||
<SettingsList.ItemIcon icon={ContactsIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Find friends from contacts</Trans>
|
||||
<Trans>Find and invite friends</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
)}
|
||||
@@ -316,7 +302,6 @@ export function SettingsScreen({}: Props) {
|
||||
/>
|
||||
|
||||
<SwitchAccountDialog control={switchAccountControl} />
|
||||
<InviteFriendsDialog control={inviteFriendsControl} />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
+36
-34
@@ -91,38 +91,7 @@ let DrawerProfileCard = ({
|
||||
accessibilityLabel={_(msg`Profile`)}
|
||||
accessibilityHint={_(msg`Navigates to your profile`)}
|
||||
onPress={onPressProfile}
|
||||
style={[a.gap_sm, a.pr_lg, {position: 'relative'}]}>
|
||||
{onPressShare && (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Invite friends`)}
|
||||
accessibilityHint={_(
|
||||
msg`Opens the invite friends sheet to share your profile`,
|
||||
)}
|
||||
onPress={onPressShare}
|
||||
hitSlop={8}
|
||||
style={({pressed}) => [
|
||||
{
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
right: 16,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: t.palette.contrast_50,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
opacity: pressed ? 0.7 : 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
]}>
|
||||
<ArrowShareRight
|
||||
width={16}
|
||||
height={16}
|
||||
fill={t.palette.primary_500}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
style={[a.gap_sm, a.pr_lg]}>
|
||||
<UserAvatar
|
||||
size={52}
|
||||
avatar={profile?.avatar}
|
||||
@@ -135,11 +104,45 @@ let DrawerProfileCard = ({
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.font_bold, a.text_xl, a.mt_2xs, a.leading_tight]}
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.text_xl,
|
||||
a.mt_2xs,
|
||||
a.leading_tight,
|
||||
a.flex_shrink,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{profile?.displayName || account.handle}
|
||||
</Text>
|
||||
{profile && <ProfileBadges profile={profile} size="lg" />}
|
||||
{onPressShare && (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Invite friends`)}
|
||||
accessibilityHint={_(
|
||||
msg`Opens the invite friends sheet to share your profile`,
|
||||
)}
|
||||
onPress={onPressShare}
|
||||
hitSlop={8}
|
||||
style={({pressed}) => [
|
||||
a.ml_auto,
|
||||
{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: t.palette.contrast_50,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
opacity: pressed ? 0.7 : 1,
|
||||
},
|
||||
]}>
|
||||
<ArrowShareRight
|
||||
width={16}
|
||||
height={16}
|
||||
fill={t.palette.primary_500}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
<Text
|
||||
emoji
|
||||
@@ -325,7 +328,6 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
|
||||
account={currentAccount}
|
||||
onPressProfile={onPressDrawerHeaderProfile}
|
||||
onPressShare={() => {
|
||||
setDrawerOpen(false)
|
||||
inviteFriendsControl.open()
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user