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()
}}
/>