diff --git a/assets/images/invite_friends_announcement_nux.webp b/assets/images/invite_friends_announcement_nux.webp new file mode 100644 index 0000000000..53e79e1d54 Binary files /dev/null and b/assets/images/invite_friends_announcement_nux.webp differ diff --git a/src/components/dialogs/nuxs/InviteFriendsAnnouncement.tsx b/src/components/dialogs/nuxs/InviteFriendsAnnouncement.tsx index 7e052da1bd..8a8577d807 100644 --- a/src/components/dialogs/nuxs/InviteFriendsAnnouncement.tsx +++ b/src/components/dialogs/nuxs/InviteFriendsAnnouncement.tsx @@ -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() { ]}> - - - - - New Feature - - - {/* - TODO: drop promo image - */} + style={[ + a.absolute, + a.align_center, + {top: 0, left: 0, right: 0, paddingTop: IS_WEB ? 24 : 20}, + ]}> + + + + New Feature + + + @@ -151,8 +147,6 @@ export function InviteFriendsAnnouncement() { - - ) } diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index 2516b22bd3..3e9aac4ce5 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -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({ activeNux: undefined, dismissActiveNux: () => {}, + openInviteFriends: () => {}, }) Context.displayName = 'NuxDialogContext' @@ -105,6 +115,7 @@ function Inner({ const [activeNux, setActiveNux] = useState() 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 ( @@ -198,6 +210,12 @@ function Inner({ {activeNux === Nux.InviteFriendsAnnouncement && ( )} + {/* + Mounted persistently (not inside the announcement) so the invite dialog + survives the announcement NUX being dismissed during the "Try it" + handoff. + */} + ) } diff --git a/src/features/inviteFriends/InviteFriendsDialog.tsx b/src/features/inviteFriends/InviteFriendsDialog.tsx index 39d13c8777..2ba8bd1c33 100644 --- a/src/features/inviteFriends/InviteFriendsDialog.tsx +++ b/src/features/inviteFriends/InviteFriendsDialog.tsx @@ -3,12 +3,17 @@ import {InviteFriendsDialogInner} from './InviteFriendsDialogInner' export function InviteFriendsDialog({ control, + onClose, }: { control: Dialog.DialogControlProps + onClose?: () => void }) { return ( - - + + {/* */} ) diff --git a/src/features/inviteFriends/InviteFriendsDialog.web.tsx b/src/features/inviteFriends/InviteFriendsDialog.web.tsx index 1a28607194..155c533918 100644 --- a/src/features/inviteFriends/InviteFriendsDialog.web.tsx +++ b/src/features/inviteFriends/InviteFriendsDialog.web.tsx @@ -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 } diff --git a/src/features/inviteFriends/InviteFriendsDialogInner.tsx b/src/features/inviteFriends/InviteFriendsDialogInner.tsx index dc1cdf6483..4a57bb3a00 100644 --- a/src/features/inviteFriends/InviteFriendsDialogInner.tsx +++ b/src/features/inviteFriends/InviteFriendsDialogInner.tsx @@ -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 ( + ( + + )}> {l`Invite Friends`} }> - + diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index b63e79f7fb..5fd53cde39 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -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) { Content and media - {IS_NATIVE && ( - inviteFriendsControl.open()}> - - - Find and invite friends - - - - )} {IS_NATIVE && findContactsEnabled && !ax.features.enabled(ax.features.ImportContactsSettingsDisable) && ( + label={l`Find and invite friends`}> - Find friends from contacts + Find and invite friends )} @@ -316,7 +302,6 @@ export function SettingsScreen({}: Props) { /> - ) } diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 2ae286a110..bdd69d9e9a 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -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 && ( - [ - { - 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, - }, - ]}> - - - )} + style={[a.gap_sm, a.pr_lg]}> {profile?.displayName || account.handle} {profile && } + {onPressShare && ( + [ + a.ml_auto, + { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: t.palette.contrast_50, + alignItems: 'center', + justifyContent: 'center', + opacity: pressed ? 0.7 : 1, + }, + ]}> + + + )} ): React.ReactNode => { account={currentAccount} onPressProfile={onPressDrawerHeaderProfile} onPressShare={() => { - setDrawerOpen(false) inviteFriendsControl.open() }} />