Activity notifications (#8557)
* add button + dialog
* rename feature
* rm joke
* add button
* add dialog to settings
* tweak web styles
* tweak switch logic
* integrate api
* add new icons
* implement gating of button
* add tooltip
* fix import
* add activity notifs to shadow
* add metrics
* rm timeout
* Fix visibility state of native tooltip (#8581)
* Fix visibility state of native tooltip
* rm onLayout
---------
Co-authored-by: Eric Bailey <git@esb.lol>
* reset nudge in settings
* Make sure web tooltip closes on interaction with target
* Add route to server
* Remove unused label
* Revert "Add route to server"
This reverts commit 66bc325aed.
---------
Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="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"/></svg>
|
||||||
|
After Width: | Height: | Size: 511 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="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.5 9.5 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.5 11.5 0 0 1 2.429 4.933 1 1 0 0 1-1.954.427 9.5 9.5 0 0 0-2.006-4.072 1 1 0 0 1 .122-1.41Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 594 B |
@@ -3,6 +3,7 @@ import {
|
|||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@@ -30,31 +31,33 @@ const BUBBLE_SHADOW_OFFSET = ARROW_SIZE / 3 // vibes-based, provide more shadow
|
|||||||
|
|
||||||
type TooltipContextType = {
|
type TooltipContextType = {
|
||||||
position: 'top' | 'bottom'
|
position: 'top' | 'bottom'
|
||||||
ready: boolean
|
visible: boolean
|
||||||
onVisibleChange: (visible: boolean) => void
|
onVisibleChange: (visible: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TargetMeasurements = {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
type TargetContextType = {
|
type TargetContextType = {
|
||||||
targetMeasurements:
|
targetMeasurements: TargetMeasurements | undefined
|
||||||
| {
|
setTargetMeasurements: (measurements: TargetMeasurements) => void
|
||||||
x: number
|
shouldMeasure: boolean
|
||||||
y: number
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
targetRef: React.RefObject<View>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TooltipContext = createContext<TooltipContextType>({
|
const TooltipContext = createContext<TooltipContextType>({
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
ready: false,
|
visible: false,
|
||||||
onVisibleChange: () => {},
|
onVisibleChange: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const TargetContext = createContext<TargetContextType>({
|
const TargetContext = createContext<TargetContextType>({
|
||||||
targetMeasurements: undefined,
|
targetMeasurements: undefined,
|
||||||
targetRef: {current: null},
|
setTargetMeasurements: () => {},
|
||||||
|
shouldMeasure: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function Outer({
|
export function Outer({
|
||||||
@@ -68,21 +71,12 @@ export function Outer({
|
|||||||
visible: boolean
|
visible: boolean
|
||||||
onVisibleChange: (visible: boolean) => void
|
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
|
* 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<
|
const [visible, setVisible] = useState<boolean>(false)
|
||||||
boolean | undefined
|
|
||||||
>()
|
|
||||||
/**
|
|
||||||
* Needs to reference the element this Tooltip is attached to.
|
|
||||||
*/
|
|
||||||
const targetRef = useRef<View>(null)
|
|
||||||
const [targetMeasurements, setTargetMeasurements] = useState<
|
const [targetMeasurements, setTargetMeasurements] = useState<
|
||||||
| {
|
| {
|
||||||
x: number
|
x: number
|
||||||
@@ -93,33 +87,24 @@ export function Outer({
|
|||||||
| undefined
|
| undefined
|
||||||
>(undefined)
|
>(undefined)
|
||||||
|
|
||||||
if (requestVisible && !prevRequestVisible) {
|
if (requestVisible && !visible && targetMeasurements) {
|
||||||
setPrevRequestVisible(true)
|
setVisible(true)
|
||||||
|
} else if (!requestVisible && visible) {
|
||||||
if (targetRef.current) {
|
setVisible(false)
|
||||||
/*
|
|
||||||
* 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)
|
|
||||||
setTargetMeasurements(undefined)
|
setTargetMeasurements(undefined)
|
||||||
setReady(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = useMemo(
|
const ctx = useMemo(
|
||||||
() => ({position, ready, onVisibleChange}),
|
() => ({position, visible, onVisibleChange}),
|
||||||
[position, ready, onVisibleChange],
|
[position, visible, onVisibleChange],
|
||||||
)
|
)
|
||||||
const targetCtx = useMemo(
|
const targetCtx = useMemo(
|
||||||
() => ({targetMeasurements, targetRef}),
|
() => ({
|
||||||
[targetMeasurements, targetRef],
|
targetMeasurements,
|
||||||
|
setTargetMeasurements,
|
||||||
|
shouldMeasure: requestVisible,
|
||||||
|
}),
|
||||||
|
[requestVisible, targetMeasurements, setTargetMeasurements],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -132,7 +117,20 @@ export function Outer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Target({children}: {children: React.ReactNode}) {
|
export function Target({children}: {children: React.ReactNode}) {
|
||||||
const {targetRef} = useContext(TargetContext)
|
const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext)
|
||||||
|
const targetRef = useRef<View>(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 (
|
return (
|
||||||
<View collapsable={false} ref={targetRef}>
|
<View collapsable={false} ref={targetRef}>
|
||||||
@@ -148,13 +146,13 @@ export function Content({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
label: string
|
label: string
|
||||||
}) {
|
}) {
|
||||||
const {position, ready, onVisibleChange} = useContext(TooltipContext)
|
const {position, visible, onVisibleChange} = useContext(TooltipContext)
|
||||||
const {targetMeasurements} = useContext(TargetContext)
|
const {targetMeasurements} = useContext(TargetContext)
|
||||||
const requestClose = useCallback(() => {
|
const requestClose = useCallback(() => {
|
||||||
onVisibleChange(false)
|
onVisibleChange(false)
|
||||||
}, [onVisibleChange])
|
}, [onVisibleChange])
|
||||||
|
|
||||||
if (!ready || !targetMeasurements) return null
|
if (!visible || !targetMeasurements) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ import {Text} from '#/components/Typography'
|
|||||||
|
|
||||||
type TooltipContextType = {
|
type TooltipContextType = {
|
||||||
position: 'top' | 'bottom'
|
position: 'top' | 'bottom'
|
||||||
|
onVisibleChange: (open: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const TooltipContext = createContext<TooltipContextType>({
|
const TooltipContext = createContext<TooltipContextType>({
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
|
onVisibleChange: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export function Outer({
|
export function Outer({
|
||||||
@@ -30,7 +32,10 @@ export function Outer({
|
|||||||
visible: boolean
|
visible: boolean
|
||||||
onVisibleChange: (visible: boolean) => void
|
onVisibleChange: (visible: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
const ctx = useMemo(() => ({position}), [position])
|
const ctx = useMemo(
|
||||||
|
() => ({position, onVisibleChange}),
|
||||||
|
[position, onVisibleChange],
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<Popover.Root open={visible} onOpenChange={onVisibleChange}>
|
<Popover.Root open={visible} onOpenChange={onVisibleChange}>
|
||||||
<TooltipContext.Provider value={ctx}>{children}</TooltipContext.Provider>
|
<TooltipContext.Provider value={ctx}>{children}</TooltipContext.Provider>
|
||||||
@@ -54,7 +59,7 @@ export function Content({
|
|||||||
label: string
|
label: string
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {position} = useContext(TooltipContext)
|
const {position, onVisibleChange} = useContext(TooltipContext)
|
||||||
return (
|
return (
|
||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
@@ -63,6 +68,7 @@ export function Content({
|
|||||||
side={position}
|
side={position}
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
collisionPadding={MIN_EDGE_SPACE}
|
collisionPadding={MIN_EDGE_SPACE}
|
||||||
|
onInteractOutside={() => onVisibleChange(false)}
|
||||||
style={flatten([
|
style={flatten([
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
select(t.name, {
|
select(t.name, {
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
<Trans key="message">
|
||||||
|
Before you can get notifications for {name}'s posts, you must first
|
||||||
|
verify your email.
|
||||||
|
</Trans>,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const isSubscribed =
|
||||||
|
profile.viewer?.activitySubscription?.post ||
|
||||||
|
profile.viewer?.activitySubscription?.reply
|
||||||
|
|
||||||
|
const Icon = isSubscribed ? BellRingingIcon : BellPlusIcon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip.Outer
|
||||||
|
visible={!activitySubscriptionsNudged}
|
||||||
|
onVisibleChange={onDismissTooltip}
|
||||||
|
position="bottom">
|
||||||
|
<Tooltip.Target>
|
||||||
|
<Button
|
||||||
|
accessibilityRole="button"
|
||||||
|
testID="dmBtn"
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
variant="solid"
|
||||||
|
shape="round"
|
||||||
|
label={_(msg`Get notified when ${name} posts`)}
|
||||||
|
onPress={wrappedOnPress}>
|
||||||
|
<ButtonIcon icon={Icon} size="md" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip.Target>
|
||||||
|
<Tooltip.TextBubble>
|
||||||
|
<Text>
|
||||||
|
<Trans>Get notified about new posts</Trans>
|
||||||
|
</Text>
|
||||||
|
</Tooltip.TextBubble>
|
||||||
|
</Tooltip.Outer>
|
||||||
|
|
||||||
|
<SubscribeProfileDialog
|
||||||
|
control={subscribeDialogControl}
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
<DialogInner
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
includeProfile={includeProfile}
|
||||||
|
/>
|
||||||
|
</Dialog.Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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<AppBskyNotificationDefs.ActivitySubscription>,
|
||||||
|
) => {
|
||||||
|
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<ButtonProps, 'children'> = 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 (
|
||||||
|
<Dialog.ScrollableInner
|
||||||
|
style={web({maxWidth: 400})}
|
||||||
|
label={_(msg`Get notified of new posts from ${name}`)}>
|
||||||
|
<View style={[a.gap_lg]}>
|
||||||
|
<View style={[a.gap_xs]}>
|
||||||
|
<Text style={[a.font_heavy, a.text_2xl]}>
|
||||||
|
<Trans>Keep me posted</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[t.atoms.text_contrast_medium, a.text_md]}>
|
||||||
|
<Trans>Get notified of this account’s activity</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{includeProfile && (
|
||||||
|
<ProfileCard.Header>
|
||||||
|
<ProfileCard.Avatar
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
disabledPreview
|
||||||
|
/>
|
||||||
|
<ProfileCard.NameAndHandle
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
</ProfileCard.Header>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Toggle.Group
|
||||||
|
label={_(msg`Subscribe to account activity`)}
|
||||||
|
values={values}
|
||||||
|
onChange={onChange}>
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
|
<Toggle.Item
|
||||||
|
label={_(msg`Posts`)}
|
||||||
|
name="post"
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.py_xs,
|
||||||
|
platform({
|
||||||
|
native: [a.justify_between],
|
||||||
|
web: [a.flex_row_reverse, a.gap_sm],
|
||||||
|
}),
|
||||||
|
]}>
|
||||||
|
<Toggle.LabelText
|
||||||
|
style={[t.atoms.text, a.font_normal, a.text_md, a.flex_1]}>
|
||||||
|
<Trans>Posts</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
<Toggle.Switch />
|
||||||
|
</Toggle.Item>
|
||||||
|
<Toggle.Item
|
||||||
|
label={_(msg`Replies`)}
|
||||||
|
name="reply"
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.py_xs,
|
||||||
|
platform({
|
||||||
|
native: [a.justify_between],
|
||||||
|
web: [a.flex_row_reverse, a.gap_sm],
|
||||||
|
}),
|
||||||
|
]}>
|
||||||
|
<Toggle.LabelText
|
||||||
|
style={[t.atoms.text, a.font_normal, a.text_md, a.flex_1]}>
|
||||||
|
<Trans>Replies</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
<Toggle.Switch />
|
||||||
|
</Toggle.Item>
|
||||||
|
</View>
|
||||||
|
</Toggle.Group>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Admonition type="error">
|
||||||
|
<Trans>Could not save changes: {cleanError(error)}</Trans>
|
||||||
|
</Admonition>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button {...buttonProps} size="large" variant="solid">
|
||||||
|
<ButtonText>{buttonProps.label}</ButtonText>
|
||||||
|
{isSaving && <ButtonIcon icon={Loader} />}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Dialog.Close />
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseActivitySubscription(
|
||||||
|
sub?: AppBskyNotificationDefs.ActivitySubscription,
|
||||||
|
): Un$Typed<AppBskyNotificationDefs.ActivitySubscription> {
|
||||||
|
if (!sub) return {post: false, reply: false}
|
||||||
|
const {post, reply} = sub
|
||||||
|
return {post, reply}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
})
|
||||||
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
|
|||||||
export const BellRinging_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
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',
|
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',
|
||||||
|
})
|
||||||
|
|||||||
@@ -443,4 +443,8 @@ export type MetricEvents = {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
'thread:click:headerMenuOpen': {}
|
'thread:click:headerMenuOpen': {}
|
||||||
|
'activitySubscription:enable': {
|
||||||
|
setting: 'posts' | 'posts_and_replies'
|
||||||
|
}
|
||||||
|
'activitySubscription:disable': {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {sanitizeHandle} from '#/lib/strings/handles'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isIOS} from '#/platform/detection'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {type Shadow} from '#/state/cache/types'
|
|
||||||
import {
|
import {
|
||||||
useProfileBlockMutationQueue,
|
useProfileBlockMutationQueue,
|
||||||
useProfileFollowMutationQueue,
|
useProfileFollowMutationQueue,
|
||||||
@@ -24,6 +23,7 @@ import {useRequireAuth, useSession} from '#/state/session'
|
|||||||
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
|
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
|
||||||
|
import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
|
import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
|
||||||
@@ -58,8 +58,8 @@ let ProfileHeaderStandard = ({
|
|||||||
}: Props): React.ReactNode => {
|
}: Props): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
|
const profile =
|
||||||
useProfileShadow(profileUnshadowed)
|
useProfileShadow<AppBskyActorDefs.ProfileViewDetailed>(profileUnshadowed)
|
||||||
const {currentAccount, hasSession} = useSession()
|
const {currentAccount, hasSession} = useSession()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const moderation = useMemo(
|
const moderation = useMemo(
|
||||||
@@ -134,13 +134,26 @@ let ProfileHeaderStandard = ({
|
|||||||
}
|
}
|
||||||
}, [_, queueUnblock])
|
}, [_, queueUnblock])
|
||||||
|
|
||||||
const isMe = React.useMemo(
|
const isMe = useMemo(
|
||||||
() => currentAccount?.did === profile.did,
|
() => currentAccount?.did === profile.did,
|
||||||
[currentAccount, profile],
|
[currentAccount, profile],
|
||||||
)
|
)
|
||||||
|
|
||||||
const {isActive: live} = useActorStatus(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 (
|
return (
|
||||||
<ProfileHeaderShell
|
<ProfileHeaderShell
|
||||||
profile={profile}
|
profile={profile}
|
||||||
@@ -198,6 +211,12 @@ let ProfileHeaderStandard = ({
|
|||||||
)
|
)
|
||||||
) : !profile.viewer?.blockedBy ? (
|
) : !profile.viewer?.blockedBy ? (
|
||||||
<>
|
<>
|
||||||
|
{hasSession && subscriptionsAllowed && (
|
||||||
|
<SubscribeProfileButton
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{hasSession && <MessageProfileButton profile={profile} />}
|
{hasSession && <MessageProfileButton profile={profile} />}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import {useActivitySubscriptionsQuery} from '#/state/queries/activity-subscripti
|
|||||||
import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
|
import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
|
||||||
import {List} from '#/view/com/util/List'
|
import {List} from '#/view/com/util/List'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {SubscribeProfileDialog} from '#/components/activity-notifications/SubscribeProfileDialog'
|
||||||
import * as Admonition from '#/components/Admonition'
|
import * as Admonition from '#/components/Admonition'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {Bell_Filled_Corner0_Rounded as BellIcon} from '#/components/icons/Bell'
|
import {Bell_Filled_Corner0_Rounded as BellIcon} from '#/components/icons/Bell'
|
||||||
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
@@ -197,6 +199,7 @@ function ActivitySubscriptionCard({
|
|||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
}) {
|
}) {
|
||||||
|
const control = useDialogControl()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
@@ -226,7 +229,8 @@ function ActivitySubscriptionCard({
|
|||||||
)}
|
)}
|
||||||
size="small"
|
size="small"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="solid">
|
variant="solid"
|
||||||
|
onPress={control.open}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Edit</Trans>
|
<Trans>Edit</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
@@ -234,6 +238,13 @@ function ActivitySubscriptionCard({
|
|||||||
</ProfileCard.Header>
|
</ProfileCard.Header>
|
||||||
{/* <ProfileCard.Labels profile={profile} moderationOpts={moderationOpts} /> */}
|
{/* <ProfileCard.Labels profile={profile} moderationOpts={moderationOpts} /> */}
|
||||||
</ProfileCard.Outer>
|
</ProfileCard.Outer>
|
||||||
|
|
||||||
|
<SubscribeProfileDialog
|
||||||
|
control={control}
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
includeProfile
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import {
|
|||||||
shouldShowVerificationCheckButton,
|
shouldShowVerificationCheckButton,
|
||||||
VerificationCheckButton,
|
VerificationCheckButton,
|
||||||
} from '#/components/verification/VerificationCheckButton'
|
} from '#/components/verification/VerificationCheckButton'
|
||||||
|
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
|
||||||
export function SettingsScreen({}: Props) {
|
export function SettingsScreen({}: Props) {
|
||||||
@@ -364,6 +365,7 @@ function DevOptions() {
|
|||||||
const onboardingDispatch = useOnboardingDispatch()
|
const onboardingDispatch = useOnboardingDispatch()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
|
const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
|
||||||
|
const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged()
|
||||||
|
|
||||||
const resetOnboarding = async () => {
|
const resetOnboarding = async () => {
|
||||||
navigation.navigate('Home')
|
navigation.navigate('Home')
|
||||||
@@ -387,6 +389,10 @@ function DevOptions() {
|
|||||||
Toast.show(_(msg`You probably want to restart the app now.`))
|
Toast.show(_(msg`You probably want to restart the app now.`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onPressActySubsUnNudge = () => {
|
||||||
|
setActyNotifNudged(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsList.PressableItem
|
<SettingsList.PressableItem
|
||||||
@@ -431,6 +437,15 @@ function DevOptions() {
|
|||||||
<Trans>Unsnooze email reminder</Trans>
|
<Trans>Unsnooze email reminder</Trans>
|
||||||
</SettingsList.ItemText>
|
</SettingsList.ItemText>
|
||||||
</SettingsList.PressableItem>
|
</SettingsList.PressableItem>
|
||||||
|
{actyNotifNudged && (
|
||||||
|
<SettingsList.PressableItem
|
||||||
|
onPress={onPressActySubsUnNudge}
|
||||||
|
label={_(msg`Reset activity subscription nudge`)}>
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Reset activity subscription nudge</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
</SettingsList.PressableItem>
|
||||||
|
)}
|
||||||
<SettingsList.PressableItem
|
<SettingsList.PressableItem
|
||||||
onPress={() => clearAllStorage()}
|
onPress={() => clearAllStorage()}
|
||||||
label={_(msg`Clear all storage data`)}>
|
label={_(msg`Clear all storage data`)}>
|
||||||
|
|||||||
Vendored
+6
-1
@@ -1,5 +1,5 @@
|
|||||||
import {useEffect, useMemo, useState} from 'react'
|
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 {type QueryClient} from '@tanstack/react-query'
|
||||||
import EventEmitter from 'eventemitter3'
|
import EventEmitter from 'eventemitter3'
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ export interface ProfileShadow {
|
|||||||
blockingUri: string | undefined
|
blockingUri: string | undefined
|
||||||
verification: AppBskyActorDefs.VerificationState
|
verification: AppBskyActorDefs.VerificationState
|
||||||
status: AppBskyActorDefs.StatusView | undefined
|
status: AppBskyActorDefs.StatusView | undefined
|
||||||
|
activitySubscription: AppBskyNotificationDefs.ActivitySubscription | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const shadows: WeakMap<
|
const shadows: WeakMap<
|
||||||
@@ -137,6 +138,10 @@ function mergeShadow<TProfileView extends bsky.profile.AnyProfileView>(
|
|||||||
muted: 'muted' in shadow ? shadow.muted : profile.viewer?.muted,
|
muted: 'muted' in shadow ? shadow.muted : profile.viewer?.muted,
|
||||||
blocking:
|
blocking:
|
||||||
'blockingUri' in shadow ? shadow.blockingUri : profile.viewer?.blocking,
|
'blockingUri' in shadow ? shadow.blockingUri : profile.viewer?.blocking,
|
||||||
|
activitySubscription:
|
||||||
|
'activitySubscription' in shadow
|
||||||
|
? shadow.activitySubscription
|
||||||
|
: profile.viewer?.activitySubscription,
|
||||||
},
|
},
|
||||||
verification:
|
verification:
|
||||||
'verification' in shadow ? shadow.verification : profile.verification,
|
'verification' in shadow ? shadow.verification : profile.verification,
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import {device, useStorage} from '#/storage'
|
||||||
|
|
||||||
|
export function useActivitySubscriptionsNudged() {
|
||||||
|
const [activitySubscriptionsNudged = false, setActivitySubscriptionsNudged] =
|
||||||
|
useStorage(device, ['activitySubscriptionsNudged'])
|
||||||
|
|
||||||
|
return [activitySubscriptionsNudged, setActivitySubscriptionsNudged] as const
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ export type Device = {
|
|||||||
trendingBetaEnabled: boolean
|
trendingBetaEnabled: boolean
|
||||||
devMode: boolean
|
devMode: boolean
|
||||||
demoMode: boolean
|
demoMode: boolean
|
||||||
|
activitySubscriptionsNudged?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Account = {
|
export type Account = {
|
||||||
|
|||||||
Reference in New Issue
Block a user