diff --git a/assets/icons/bellPlus_stroke2_corner0_rounded.svg b/assets/icons/bellPlus_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..7e3349427a
--- /dev/null
+++ b/assets/icons/bellPlus_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/bellRinging_filled_corner0_rounded.svg b/assets/icons/bellRinging_filled_corner0_rounded.svg
new file mode 100644
index 0000000000..67be5d99d3
--- /dev/null
+++ b/assets/icons/bellRinging_filled_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx
index 446cf18fc6..fbdb969db6 100644
--- a/src/components/Tooltip/index.tsx
+++ b/src/components/Tooltip/index.tsx
@@ -3,6 +3,7 @@ import {
createContext,
useCallback,
useContext,
+ useEffect,
useMemo,
useRef,
useState,
@@ -30,31 +31,33 @@ const BUBBLE_SHADOW_OFFSET = ARROW_SIZE / 3 // vibes-based, provide more shadow
type TooltipContextType = {
position: 'top' | 'bottom'
- ready: boolean
+ visible: boolean
onVisibleChange: (visible: boolean) => void
}
+type TargetMeasurements = {
+ x: number
+ y: number
+ width: number
+ height: number
+}
+
type TargetContextType = {
- targetMeasurements:
- | {
- x: number
- y: number
- width: number
- height: number
- }
- | undefined
- targetRef: React.RefObject
+ targetMeasurements: TargetMeasurements | undefined
+ setTargetMeasurements: (measurements: TargetMeasurements) => void
+ shouldMeasure: boolean
}
const TooltipContext = createContext({
position: 'bottom',
- ready: false,
+ visible: false,
onVisibleChange: () => {},
})
const TargetContext = createContext({
targetMeasurements: undefined,
- targetRef: {current: null},
+ setTargetMeasurements: () => {},
+ shouldMeasure: false,
})
export function Outer({
@@ -68,21 +71,12 @@ export function Outer({
visible: boolean
onVisibleChange: (visible: boolean) => void
}) {
- /**
- * Whether we have measured the target and are ready to show the tooltip.
- */
- const [ready, setReady] = useState(false)
/**
* Lagging state to track the externally-controlled visibility of the
- * tooltip.
+ * tooltip, which needs to wait for the target to be measured before
+ * actually being shown.
*/
- const [prevRequestVisible, setPrevRequestVisible] = useState<
- boolean | undefined
- >()
- /**
- * Needs to reference the element this Tooltip is attached to.
- */
- const targetRef = useRef(null)
+ const [visible, setVisible] = useState(false)
const [targetMeasurements, setTargetMeasurements] = useState<
| {
x: number
@@ -93,33 +87,24 @@ export function Outer({
| undefined
>(undefined)
- if (requestVisible && !prevRequestVisible) {
- setPrevRequestVisible(true)
-
- if (targetRef.current) {
- /*
- * Once opened, measure the dimensions and position of the target
- */
- targetRef.current.measure((_x, _y, width, height, pageX, pageY) => {
- if (pageX !== undefined && pageY !== undefined && width && height) {
- setTargetMeasurements({x: pageX, y: pageY, width, height})
- setReady(true)
- }
- })
- }
- } else if (!requestVisible && prevRequestVisible) {
- setPrevRequestVisible(false)
+ if (requestVisible && !visible && targetMeasurements) {
+ setVisible(true)
+ } else if (!requestVisible && visible) {
+ setVisible(false)
setTargetMeasurements(undefined)
- setReady(false)
}
const ctx = useMemo(
- () => ({position, ready, onVisibleChange}),
- [position, ready, onVisibleChange],
+ () => ({position, visible, onVisibleChange}),
+ [position, visible, onVisibleChange],
)
const targetCtx = useMemo(
- () => ({targetMeasurements, targetRef}),
- [targetMeasurements, targetRef],
+ () => ({
+ targetMeasurements,
+ setTargetMeasurements,
+ shouldMeasure: requestVisible,
+ }),
+ [requestVisible, targetMeasurements, setTargetMeasurements],
)
return (
@@ -132,7 +117,20 @@ export function Outer({
}
export function Target({children}: {children: React.ReactNode}) {
- const {targetRef} = useContext(TargetContext)
+ const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext)
+ const targetRef = useRef(null)
+
+ useEffect(() => {
+ if (!shouldMeasure) return
+ /*
+ * Once opened, measure the dimensions and position of the target
+ */
+ targetRef.current?.measure((_x, _y, width, height, pageX, pageY) => {
+ if (pageX !== undefined && pageY !== undefined && width && height) {
+ setTargetMeasurements({x: pageX, y: pageY, width, height})
+ }
+ })
+ }, [shouldMeasure, setTargetMeasurements])
return (
@@ -148,13 +146,13 @@ export function Content({
children: React.ReactNode
label: string
}) {
- const {position, ready, onVisibleChange} = useContext(TooltipContext)
+ const {position, visible, onVisibleChange} = useContext(TooltipContext)
const {targetMeasurements} = useContext(TargetContext)
const requestClose = useCallback(() => {
onVisibleChange(false)
}, [onVisibleChange])
- if (!ready || !targetMeasurements) return null
+ if (!visible || !targetMeasurements) return null
return (
diff --git a/src/components/Tooltip/index.web.tsx b/src/components/Tooltip/index.web.tsx
index 739a714cd4..fc5808d7ad 100644
--- a/src/components/Tooltip/index.web.tsx
+++ b/src/components/Tooltip/index.web.tsx
@@ -13,10 +13,12 @@ import {Text} from '#/components/Typography'
type TooltipContextType = {
position: 'top' | 'bottom'
+ onVisibleChange: (open: boolean) => void
}
const TooltipContext = createContext({
position: 'bottom',
+ onVisibleChange: () => {},
})
export function Outer({
@@ -30,7 +32,10 @@ export function Outer({
visible: boolean
onVisibleChange: (visible: boolean) => void
}) {
- const ctx = useMemo(() => ({position}), [position])
+ const ctx = useMemo(
+ () => ({position, onVisibleChange}),
+ [position, onVisibleChange],
+ )
return (
{children}
@@ -54,7 +59,7 @@ export function Content({
label: string
}) {
const t = useTheme()
- const {position} = useContext(TooltipContext)
+ const {position, onVisibleChange} = useContext(TooltipContext)
return (
onVisibleChange(false)}
style={flatten([
a.rounded_sm,
select(t.name, {
diff --git a/src/components/activity-notifications/SubscribeProfileButton.tsx b/src/components/activity-notifications/SubscribeProfileButton.tsx
new file mode 100644
index 0000000000..71253dca9b
--- /dev/null
+++ b/src/components/activity-notifications/SubscribeProfileButton.tsx
@@ -0,0 +1,89 @@
+import {useCallback} from 'react'
+import {type ModerationOpts} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
+import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
+import {Button, ButtonIcon} from '#/components/Button'
+import {useDialogControl} from '#/components/Dialog'
+import {BellPlus_Stroke2_Corner0_Rounded as BellPlusIcon} from '#/components/icons/BellPlus'
+import {BellRinging_Filled_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
+import * as Tooltip from '#/components/Tooltip'
+import {Text} from '#/components/Typography'
+import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
+import type * as bsky from '#/types/bsky'
+import {SubscribeProfileDialog} from './SubscribeProfileDialog'
+
+export function SubscribeProfileButton({
+ profile,
+ moderationOpts,
+}: {
+ profile: bsky.profile.AnyProfileView
+ moderationOpts: ModerationOpts
+}) {
+ const {_} = useLingui()
+ const requireEmailVerification = useRequireEmailVerification()
+ const subscribeDialogControl = useDialogControl()
+ const [activitySubscriptionsNudged, setActivitySubscriptionsNudged] =
+ useActivitySubscriptionsNudged()
+
+ const onDismissTooltip = () => {
+ setActivitySubscriptionsNudged(true)
+ }
+
+ const onPress = useCallback(() => {
+ subscribeDialogControl.open()
+ }, [subscribeDialogControl])
+
+ const name = createSanitizedDisplayName(profile, true)
+
+ const wrappedOnPress = requireEmailVerification(onPress, {
+ instructions: [
+
+ Before you can get notifications for {name}'s posts, you must first
+ verify your email.
+ ,
+ ],
+ })
+
+ const isSubscribed =
+ profile.viewer?.activitySubscription?.post ||
+ profile.viewer?.activitySubscription?.reply
+
+ const Icon = isSubscribed ? BellRingingIcon : BellPlusIcon
+
+ return (
+ <>
+
+
+
+
+
+
+ Get notified about new posts
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/components/activity-notifications/SubscribeProfileDialog.tsx b/src/components/activity-notifications/SubscribeProfileDialog.tsx
new file mode 100644
index 0000000000..1818735cda
--- /dev/null
+++ b/src/components/activity-notifications/SubscribeProfileDialog.tsx
@@ -0,0 +1,283 @@
+import {useMemo, useState} from 'react'
+import {View} from 'react-native'
+import {
+ type AppBskyNotificationDefs,
+ type ModerationOpts,
+ type Un$Typed,
+} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useMutation, useQueryClient} from '@tanstack/react-query'
+
+import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
+import {cleanError} from '#/lib/strings/errors'
+import {sanitizeHandle} from '#/lib/strings/handles'
+import {logger} from '#/logger'
+import {isWeb} from '#/platform/detection'
+import {updateProfileShadow} from '#/state/cache/profile-shadow'
+import {useAgent} from '#/state/session'
+import * as Toast from '#/view/com/util/Toast'
+import {platform, useTheme, web} from '#/alf'
+import {atoms as a} from '#/alf'
+import {Admonition} from '#/components/Admonition'
+import {
+ Button,
+ ButtonIcon,
+ type ButtonProps,
+ ButtonText,
+} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import * as Toggle from '#/components/forms/Toggle'
+import {Loader} from '#/components/Loader'
+import * as ProfileCard from '#/components/ProfileCard'
+import {Text} from '#/components/Typography'
+import type * as bsky from '#/types/bsky'
+
+export function SubscribeProfileDialog({
+ control,
+ profile,
+ moderationOpts,
+ includeProfile,
+}: {
+ control: Dialog.DialogControlProps
+ profile: bsky.profile.AnyProfileView
+ moderationOpts: ModerationOpts
+ includeProfile?: boolean
+}) {
+ return (
+
+
+
+
+ )
+}
+
+function DialogInner({
+ profile,
+ moderationOpts,
+ includeProfile,
+}: {
+ profile: bsky.profile.AnyProfileView
+ moderationOpts: ModerationOpts
+ includeProfile?: boolean
+}) {
+ const {_} = useLingui()
+ const t = useTheme()
+ const agent = useAgent()
+ const control = Dialog.useDialogContext()
+ const queryClient = useQueryClient()
+ const initialState = parseActivitySubscription(
+ profile.viewer?.activitySubscription,
+ )
+ const [state, setState] = useState(initialState)
+
+ const values = useMemo(() => {
+ const {post, reply} = state
+ const res = []
+ if (post) res.push('post')
+ if (reply) res.push('reply')
+ return res
+ }, [state])
+
+ const onChange = (newValues: string[]) => {
+ setState(oldValues => {
+ // ensure you can't have reply without post
+ if (!oldValues.reply && newValues.includes('reply')) {
+ return {
+ post: true,
+ reply: true,
+ }
+ }
+
+ if (oldValues.post && !newValues.includes('post')) {
+ return {
+ post: false,
+ reply: false,
+ }
+ }
+
+ return {
+ post: newValues.includes('post'),
+ reply: newValues.includes('reply'),
+ }
+ })
+ }
+
+ const {
+ mutate: saveChanges,
+ isPending: isSaving,
+ error,
+ } = useMutation({
+ mutationFn: async (
+ activitySubscription: Un$Typed,
+ ) => {
+ await agent.app.bsky.notification.putActivitySubscription({
+ subject: profile.did,
+ activitySubscription,
+ })
+ },
+ onSuccess: (_data, activitySubscription) => {
+ control.close(() =>
+ updateProfileShadow(queryClient, profile.did, {
+ activitySubscription,
+ }),
+ )
+ if (!activitySubscription.post && !activitySubscription.reply) {
+ logger.metric('activitySubscription:disable', {})
+ Toast.show(
+ _(
+ msg`You will no longer receive notifications for ${sanitizeHandle(profile.handle, '@')}`,
+ ),
+ 'check',
+ )
+ } else {
+ logger.metric('activitySubscription:enable', {
+ setting: activitySubscription.reply ? 'posts_and_replies' : 'posts',
+ })
+ if (!initialState.post && !initialState.reply) {
+ Toast.show(
+ _(
+ msg`You'll start receiving notifications for ${sanitizeHandle(profile.handle, '@')}!`,
+ ),
+ 'check',
+ )
+ } else {
+ Toast.show(_(msg`Changes saved`), 'check')
+ }
+ }
+ },
+ onError: err => {
+ logger.error('Could not save activity subscription', {message: err})
+ },
+ })
+
+ const buttonProps: Omit = useMemo(() => {
+ const isDirty =
+ state.post !== initialState.post || state.reply !== initialState.reply
+ const hasAny = state.post || state.reply
+
+ if (isDirty) {
+ return {
+ label: _(msg`Save changes`),
+ color: hasAny ? 'primary' : 'negative',
+ onPress: () => saveChanges(state),
+ disabled: isSaving,
+ }
+ } else {
+ // on web, a disabled save button feels more natural than a massive close button
+ if (isWeb) {
+ return {
+ label: _(msg`Save changes`),
+ color: 'secondary',
+ disabled: true,
+ }
+ } else {
+ return {
+ label: _(msg`Cancel`),
+ color: 'secondary',
+ onPress: () => control.close(),
+ }
+ }
+ }
+ }, [state, initialState, control, _, isSaving, saveChanges])
+
+ const name = createSanitizedDisplayName(profile, false)
+
+ return (
+
+
+
+
+ Keep me posted
+
+
+ Get notified of this account’s activity
+
+
+
+ {includeProfile && (
+
+
+
+
+ )}
+
+
+
+
+
+ Posts
+
+
+
+
+
+ Replies
+
+
+
+
+
+
+ {error && (
+
+ Could not save changes: {cleanError(error)}
+
+ )}
+
+
+
+
+
+
+ )
+}
+
+function parseActivitySubscription(
+ sub?: AppBskyNotificationDefs.ActivitySubscription,
+): Un$Typed {
+ if (!sub) return {post: false, reply: false}
+ const {post, reply} = sub
+ return {post, reply}
+}
diff --git a/src/components/icons/BellPlus.tsx b/src/components/icons/BellPlus.tsx
new file mode 100644
index 0000000000..cd29de1979
--- /dev/null
+++ b/src/components/icons/BellPlus.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const BellPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M12 2a1 1 0 0 1 0 2 5.85 5.85 0 0 0-5.802 5.08L5.143 17h13.715l-.382-2.868-.01-.102a1 1 0 0 1 1.973-.262l.02.1.532 4a1 1 0 0 1-.99 1.132h-3.357c-.905 1.747-2.606 3-4.644 3s-3.74-1.253-4.643-3H4a1 1 0 0 1-.991-1.132l1.207-9.053A7.85 7.85 0 0 1 12 2ZM9.78 19c.61.637 1.397 1 2.22 1s1.611-.363 2.22-1H9.78ZM17 2.5a1 1 0 0 1 1 1V6h2.5a1 1 0 0 1 0 2H18v2.5a1 1 0 0 1-2 0V8h-2.5a1 1 0 1 1 0-2H16V3.5a1 1 0 0 1 1-1Z',
+})
diff --git a/src/components/icons/BellRinging.tsx b/src/components/icons/BellRinging.tsx
index b174fcedc2..11981a7a38 100644
--- a/src/components/icons/BellRinging.tsx
+++ b/src/components/icons/BellRinging.tsx
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
export const BellRinging_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 2a7.854 7.854 0 0 1 7.785 6.815l1.055 7.92.018.224a2 2 0 0 1-2 2.041h-2.215c-.904 1.747-2.605 3-4.643 3s-3.739-1.253-4.643-3H5.142a2 2 0 0 1-1.982-2.265l1.056-7.92.057-.363A7.854 7.854 0 0 1 12 2ZM9.78 19c.609.637 1.398 1 2.22 1s1.611-.363 2.22-1H9.78ZM12 4a5.854 5.854 0 0 0-5.76 4.81l-.041.27L5.142 17h13.716l-1.056-7.92A5.854 5.854 0 0 0 12 4ZM2.718 7.464a1 1 0 1 1-1.953-.427l1.953.427Zm20.518-.427a1 1 0 0 1-1.954.427l1.954-.427ZM3.193 2.105a1 1 0 0 1 1.531 1.287 9.47 9.47 0 0 0-2.006 4.072L.765 7.037a11.46 11.46 0 0 1 2.428-4.932Zm16.205-.123a1 1 0 0 1 1.34.047l.069.076.217.265a11.46 11.46 0 0 1 2.212 4.667l-.978.213-.976.214a9.46 9.46 0 0 0-1.826-3.853l-.18-.22-.062-.081a1 1 0 0 1 .184-1.328Z',
})
+
+export const BellRinging_Filled_Corner0_Rounded = createSinglePathSVG({
+ path: 'M12 2a7.854 7.854 0 0 1 7.784 6.815l1.207 9.053a1 1 0 0 1-.99 1.132h-3.354c-.904 1.748-2.608 3-4.647 3-2.038 0-3.742-1.252-4.646-3H4a1.002 1.002 0 0 1-.991-1.132l1.207-9.053A7.85 7.85 0 0 1 12 2ZM9.78 19c.608.637 1.398 1 2.221 1s1.613-.363 2.222-1H9.779ZM3.193 2.104a1 1 0 0 1 1.53 1.288A9.47 9.47 0 0 0 2.72 7.464a1 1 0 0 1-1.954-.427 11.46 11.46 0 0 1 2.428-4.933Zm16.205-.122a1 1 0 0 1 1.409.122 11.47 11.47 0 0 1 2.429 4.933 1 1 0 0 1-1.954.427 9.47 9.47 0 0 0-2.006-4.072 1 1 0 0 1 .122-1.41Z',
+})
diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts
index 31af1be2b0..bbde6c9bc0 100644
--- a/src/logger/metrics.ts
+++ b/src/logger/metrics.ts
@@ -443,4 +443,8 @@ export type MetricEvents = {
[key: string]: any
}
'thread:click:headerMenuOpen': {}
+ 'activitySubscription:enable': {
+ setting: 'posts' | 'posts_and_replies'
+ }
+ 'activitySubscription:disable': {}
}
diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
index 1639abaf0c..5dbf32c57f 100644
--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
@@ -15,7 +15,6 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow'
-import {type Shadow} from '#/state/cache/types'
import {
useProfileBlockMutationQueue,
useProfileFollowMutationQueue,
@@ -24,6 +23,7 @@ import {useRequireAuth, useSession} from '#/state/session'
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
+import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
@@ -58,8 +58,8 @@ let ProfileHeaderStandard = ({
}: Props): React.ReactNode => {
const t = useTheme()
const {gtMobile} = useBreakpoints()
- const profile: Shadow =
- useProfileShadow(profileUnshadowed)
+ const profile =
+ useProfileShadow(profileUnshadowed)
const {currentAccount, hasSession} = useSession()
const {_} = useLingui()
const moderation = useMemo(
@@ -134,13 +134,26 @@ let ProfileHeaderStandard = ({
}
}, [_, queueUnblock])
- const isMe = React.useMemo(
+ const isMe = useMemo(
() => currentAccount?.did === profile.did,
[currentAccount, profile],
)
const {isActive: live} = useActorStatus(profile)
+ const subscriptionsAllowed = useMemo(() => {
+ switch (profile.associated?.activitySubscription?.allowSubscriptions) {
+ case 'followers':
+ case undefined:
+ return !!profile.viewer?.following
+ case 'mutuals':
+ return !!profile.viewer?.following && !!profile.viewer.followedBy
+ case 'none':
+ default:
+ return false
+ }
+ }, [profile])
+
return (
+ {hasSession && subscriptionsAllowed && (
+
+ )}
{hasSession && }
)
}
diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx
index 2ee3af78d6..7508697e01 100644
--- a/src/screens/Settings/Settings.tsx
+++ b/src/screens/Settings/Settings.tsx
@@ -64,6 +64,7 @@ import {
shouldShowVerificationCheckButton,
VerificationCheckButton,
} from '#/components/verification/VerificationCheckButton'
+import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
type Props = NativeStackScreenProps
export function SettingsScreen({}: Props) {
@@ -364,6 +365,7 @@ function DevOptions() {
const onboardingDispatch = useOnboardingDispatch()
const navigation = useNavigation()
const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
+ const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged()
const resetOnboarding = async () => {
navigation.navigate('Home')
@@ -387,6 +389,10 @@ function DevOptions() {
Toast.show(_(msg`You probably want to restart the app now.`))
}
+ const onPressActySubsUnNudge = () => {
+ setActyNotifNudged(false)
+ }
+
return (
<>
Unsnooze email reminder
+ {actyNotifNudged && (
+
+
+ Reset activity subscription nudge
+
+
+ )}
clearAllStorage()}
label={_(msg`Clear all storage data`)}>
diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts
index 0d3bb1b17d..77c7aea62c 100644
--- a/src/state/cache/profile-shadow.ts
+++ b/src/state/cache/profile-shadow.ts
@@ -1,5 +1,5 @@
import {useEffect, useMemo, useState} from 'react'
-import {type AppBskyActorDefs} from '@atproto/api'
+import {type AppBskyActorDefs, type AppBskyNotificationDefs} from '@atproto/api'
import {type QueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3'
@@ -33,6 +33,7 @@ export interface ProfileShadow {
blockingUri: string | undefined
verification: AppBskyActorDefs.VerificationState
status: AppBskyActorDefs.StatusView | undefined
+ activitySubscription: AppBskyNotificationDefs.ActivitySubscription | undefined
}
const shadows: WeakMap<
@@ -137,6 +138,10 @@ function mergeShadow(
muted: 'muted' in shadow ? shadow.muted : profile.viewer?.muted,
blocking:
'blockingUri' in shadow ? shadow.blockingUri : profile.viewer?.blocking,
+ activitySubscription:
+ 'activitySubscription' in shadow
+ ? shadow.activitySubscription
+ : profile.viewer?.activitySubscription,
},
verification:
'verification' in shadow ? shadow.verification : profile.verification,
diff --git a/src/storage/hooks/activity-subscriptions-nudged.ts b/src/storage/hooks/activity-subscriptions-nudged.ts
new file mode 100644
index 0000000000..0e9c1c39cb
--- /dev/null
+++ b/src/storage/hooks/activity-subscriptions-nudged.ts
@@ -0,0 +1,8 @@
+import {device, useStorage} from '#/storage'
+
+export function useActivitySubscriptionsNudged() {
+ const [activitySubscriptionsNudged = false, setActivitySubscriptionsNudged] =
+ useStorage(device, ['activitySubscriptionsNudged'])
+
+ return [activitySubscriptionsNudged, setActivitySubscriptionsNudged] as const
+}
diff --git a/src/storage/schema.ts b/src/storage/schema.ts
index 7430532a96..19c31834bb 100644
--- a/src/storage/schema.ts
+++ b/src/storage/schema.ts
@@ -11,6 +11,7 @@ export type Device = {
trendingBetaEnabled: boolean
devMode: boolean
demoMode: boolean
+ activitySubscriptionsNudged?: boolean
}
export type Account = {