Activity notification settings (#8485)
Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Samuel Newman <mozzius@protonmail.com> Co-authored-by: hailey <me@haileyok.com>
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 |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -258,6 +258,7 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/feeds", server.WebGeneric)
|
||||
e.GET("/notifications", server.WebGeneric)
|
||||
e.GET("/notifications/settings", server.WebGeneric)
|
||||
e.GET("/notifications/activity", server.WebGeneric)
|
||||
e.GET("/lists", server.WebGeneric)
|
||||
e.GET("/moderation", server.WebGeneric)
|
||||
e.GET("/moderation/modlists", server.WebGeneric)
|
||||
@@ -275,6 +276,7 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/settings/appearance", server.WebGeneric)
|
||||
e.GET("/settings/account", server.WebGeneric)
|
||||
e.GET("/settings/privacy-and-security", server.WebGeneric)
|
||||
e.GET("/settings/privacy-and-security/activity", server.WebGeneric)
|
||||
e.GET("/settings/content-and-media", server.WebGeneric)
|
||||
e.GET("/settings/interests", server.WebGeneric)
|
||||
e.GET("/settings/about", server.WebGeneric)
|
||||
|
||||
+2
-2
@@ -45,10 +45,10 @@ class BackgroundNotificationHandler(
|
||||
private fun mutateWithOtherReason(remoteMessage: RemoteMessage) {
|
||||
// If oreo or higher
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
// If one of "like", "repost", "follow", "mention", "reply", "quote", "like-via-repost", "repost-via-repost"
|
||||
// If one of "like", "repost", "follow", "mention", "reply", "quote", "like-via-repost", "repost-via-repost", "subscribed-post"
|
||||
// assign to it's eponymous channel. otherwise do nothing, let expo handle it
|
||||
when (remoteMessage.data["reason"]) {
|
||||
"like", "repost", "follow", "mention", "reply", "quote", "like-via-repost", "repost-via-repost" -> {
|
||||
"like", "repost", "follow", "mention", "reply", "quote", "like-via-repost", "repost-via-repost", "subscribed-post" -> {
|
||||
remoteMessage.data["channelId"] = remoteMessage.data["reason"]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -69,7 +69,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.15.16",
|
||||
"@atproto/api": "^0.15.21",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
@@ -218,7 +218,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.144",
|
||||
"@atproto/dev-env": "^0.3.150",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
|
||||
@@ -84,6 +84,7 @@ import {SearchScreen} from '#/screens/Search'
|
||||
import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings'
|
||||
import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings'
|
||||
import {AccountSettingsScreen} from '#/screens/Settings/AccountSettings'
|
||||
import {ActivityPrivacySettingsScreen} from '#/screens/Settings/ActivityPrivacySettings'
|
||||
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
|
||||
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
|
||||
import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords'
|
||||
@@ -109,8 +110,10 @@ import {
|
||||
} from '#/components/dialogs/EmailDialog'
|
||||
import {router} from '#/routes'
|
||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||
import {NotificationsActivityListScreen} from './screens/Notifications/ActivityList'
|
||||
import {LegacyNotificationSettingsScreen} from './screens/Settings/LegacyNotificationSettings'
|
||||
import {NotificationSettingsScreen} from './screens/Settings/NotificationSettings'
|
||||
import {ActivityNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ActivityNotificationSettings'
|
||||
import {LikeNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikeNotificationSettings'
|
||||
import {LikesOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings'
|
||||
import {MentionNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MentionNotificationSettings'
|
||||
@@ -390,6 +393,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ActivityPrivacySettings"
|
||||
getComponent={() => ActivityPrivacySettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Privacy and Security`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NotificationSettings"
|
||||
getComponent={() => NotificationSettingsScreen}
|
||||
@@ -459,6 +470,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ActivityNotificationSettings"
|
||||
getComponent={() => ActivityNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Activity notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="MiscellaneousNotificationSettings"
|
||||
getComponent={() => MiscellaneousNotificationSettingsScreen}
|
||||
@@ -524,6 +543,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
getComponent={() => MessagesInboxScreen}
|
||||
options={{title: title(msg`Chat request inbox`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NotificationsActivityList"
|
||||
getComponent={() => NotificationsActivityListScreen}
|
||||
options={{title: title(msg`Notifications`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="LegacyNotificationSettings"
|
||||
getComponent={() => LegacyNotificationSettingsScreen}
|
||||
|
||||
@@ -11,6 +11,8 @@ import {useLingui} from '@lingui/react'
|
||||
import {useActorStatus} from '#/lib/actor-status'
|
||||
import {getModerationCauseKey} from '#/lib/moderation'
|
||||
import {type LogEvents} from '#/lib/statsig/statsig'
|
||||
import {forceLTR} from '#/lib/strings/bidi'
|
||||
import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
@@ -18,7 +20,7 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
@@ -183,14 +185,77 @@ export function AvatarPlaceholder() {
|
||||
export function NameAndHandle({
|
||||
profile,
|
||||
moderationOpts,
|
||||
inline = false,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
inline?: boolean
|
||||
}) {
|
||||
if (inline) {
|
||||
return (
|
||||
<InlineNameAndHandle profile={profile} moderationOpts={moderationOpts} />
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<View style={[a.flex_1]}>
|
||||
<Name profile={profile} moderationOpts={moderationOpts} />
|
||||
<Handle profile={profile} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function InlineNameAndHandle({
|
||||
profile,
|
||||
moderationOpts,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const verification = useSimpleVerificationState({profile})
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
const name = sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
const handle = sanitizeHandle(profile.handle, '@')
|
||||
return (
|
||||
<View style={[a.flex_1]}>
|
||||
<Name profile={profile} moderationOpts={moderationOpts} />
|
||||
<Handle profile={profile} />
|
||||
<View style={[a.flex_row, a.align_end, a.flex_shrink]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
a.flex_shrink_0,
|
||||
{maxWidth: '70%'},
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{forceLTR(name)}
|
||||
</Text>
|
||||
{verification.showBadge && (
|
||||
<View
|
||||
style={[
|
||||
a.pl_2xs,
|
||||
a.self_center,
|
||||
{marginTop: platform({default: 0, android: -1})},
|
||||
]}>
|
||||
<VerificationCheck
|
||||
width={platform({android: 13, default: 12})}
|
||||
verifier={verification.role === 'verifier'}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<Text
|
||||
emoji
|
||||
style={[
|
||||
a.leading_tight,
|
||||
t.atoms.text_contrast_medium,
|
||||
{flexShrink: 10},
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{NON_BREAKING_SPACE + handle}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<View>
|
||||
targetMeasurements: TargetMeasurements | undefined
|
||||
setTargetMeasurements: (measurements: TargetMeasurements) => void
|
||||
shouldMeasure: boolean
|
||||
}
|
||||
|
||||
const TooltipContext = createContext<TooltipContextType>({
|
||||
position: 'bottom',
|
||||
ready: false,
|
||||
visible: false,
|
||||
onVisibleChange: () => {},
|
||||
})
|
||||
|
||||
const TargetContext = createContext<TargetContextType>({
|
||||
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<View>(null)
|
||||
const [visible, setVisible] = useState<boolean>(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<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 (
|
||||
<View collapsable={false} ref={targetRef}>
|
||||
@@ -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 (
|
||||
<Portal>
|
||||
|
||||
@@ -13,10 +13,12 @@ import {Text} from '#/components/Typography'
|
||||
|
||||
type TooltipContextType = {
|
||||
position: 'top' | 'bottom'
|
||||
onVisibleChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
const TooltipContext = createContext<TooltipContextType>({
|
||||
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 (
|
||||
<Popover.Root open={visible} onOpenChange={onVisibleChange}>
|
||||
<TooltipContext.Provider value={ctx}>{children}</TooltipContext.Provider>
|
||||
@@ -54,7 +59,7 @@ export function Content({
|
||||
label: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {position} = useContext(TooltipContext)
|
||||
const {position, onVisibleChange} = useContext(TooltipContext)
|
||||
return (
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
@@ -63,6 +68,7 @@ export function Content({
|
||||
side={position}
|
||||
sideOffset={4}
|
||||
collisionPadding={MIN_EDGE_SPACE}
|
||||
onInteractOutside={() => onVisibleChange(false)}
|
||||
style={flatten([
|
||||
a.rounded_sm,
|
||||
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,309 @@
|
||||
import {useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
type AppBskyNotificationDefs,
|
||||
type AppBskyNotificationListActivitySubscriptions,
|
||||
type ModerationOpts,
|
||||
type Un$Typed,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {
|
||||
type InfiniteData,
|
||||
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 {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions'
|
||||
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',
|
||||
)
|
||||
|
||||
// filter out the subscription
|
||||
queryClient.setQueryData(
|
||||
RQKEY_getActivitySubscriptions,
|
||||
(
|
||||
old?: InfiniteData<AppBskyNotificationListActivitySubscriptions.OutputSchema>,
|
||||
) => {
|
||||
if (!old) return old
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map(page => ({
|
||||
...page,
|
||||
subscriptions: page.subscriptions.filter(
|
||||
item => item.did !== profile.did,
|
||||
),
|
||||
})),
|
||||
}
|
||||
},
|
||||
)
|
||||
} 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,177 @@
|
||||
import {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
|
||||
import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function ActivitySubscriptionsNUX() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const nuxDialogs = useNuxDialogContext()
|
||||
const control = Dialog.useDialogControl()
|
||||
|
||||
Dialog.useAutoOpen(control)
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
}, [nuxDialogs])
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control} onClose={onClose}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Introducing activity notifications`)}
|
||||
style={[web({maxWidth: 400})]}
|
||||
contentContainerStyle={[
|
||||
{
|
||||
paddingTop: 0,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.align_center,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
gap: isWeb ? 16 : 24,
|
||||
paddingTop: isWeb ? 24 : 48,
|
||||
borderTopLeftRadius: a.rounded_md.borderRadius,
|
||||
borderTopRightRadius: a.rounded_md.borderRadius,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.pl_sm,
|
||||
a.pr_md,
|
||||
a.py_sm,
|
||||
a.rounded_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
{
|
||||
backgroundColor: t.palette.primary_100,
|
||||
},
|
||||
]}>
|
||||
<SparkleIcon fill={t.palette.primary_800} size="sm" />
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
{
|
||||
color: t.palette.primary_800,
|
||||
},
|
||||
]}>
|
||||
<Trans>New Feature</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={[a.relative, a.w_full]}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.shadow_md,
|
||||
{
|
||||
shadowOpacity: 0.4,
|
||||
top: 5,
|
||||
bottom: 0,
|
||||
left: '17%',
|
||||
right: '17%',
|
||||
width: '66%',
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.overflow_hidden,
|
||||
{
|
||||
aspectRatio: 398 / 228,
|
||||
},
|
||||
]}>
|
||||
<Image
|
||||
accessibilityIgnoresInvertColors
|
||||
source={require('../../../../assets/images/activity_notifications_announcement.webp')}
|
||||
style={[
|
||||
a.w_full,
|
||||
{
|
||||
aspectRatio: 398 / 268,
|
||||
},
|
||||
]}
|
||||
alt={_(
|
||||
msg`A screenshot of a profile page with a bell icon next to the follow button, indicating the new activity notifications feature.`,
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
a.align_center,
|
||||
a.px_xl,
|
||||
isWeb ? [a.pt_xl, a.gap_xl, a.pb_sm] : [a.pt_3xl, a.gap_3xl],
|
||||
]}>
|
||||
<View style={[a.gap_md, a.align_center]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_3xl,
|
||||
a.leading_tight,
|
||||
a.font_heavy,
|
||||
a.text_center,
|
||||
{
|
||||
fontSize: isWeb ? 28 : 32,
|
||||
maxWidth: 300,
|
||||
},
|
||||
]}>
|
||||
<Trans>Get notified when someone posts</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
a.text_center,
|
||||
{
|
||||
maxWidth: 340,
|
||||
},
|
||||
]}>
|
||||
<Trans>
|
||||
You can now choose to be notified when specific people post. If
|
||||
there’s someone you want timely updates from, go to their
|
||||
profile and find the new bell icon near the follow button.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{!isWeb && (
|
||||
<Button
|
||||
label={_(msg`Close`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onPress={() => {
|
||||
control.close()
|
||||
}}
|
||||
style={[a.w_full, {maxWidth: 280}]}>
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {type SessionAccount, useSession} from '#/state/session'
|
||||
import {useOnboardingState} from '#/state/shell'
|
||||
import {InitialVerificationAnnouncement} from '#/components/dialogs/nuxs/InitialVerificationAnnouncement'
|
||||
import {ActivitySubscriptionsNUX} from '#/components/dialogs/nuxs/ActivitySubscriptions'
|
||||
/*
|
||||
* NUXs
|
||||
*/
|
||||
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
||||
import {isDaysOld} from '#/components/dialogs/nuxs/utils'
|
||||
import {isExistingUserAsOf} from '#/components/dialogs/nuxs/utils'
|
||||
|
||||
type Context = {
|
||||
activeNux: Nux | undefined
|
||||
@@ -33,9 +33,12 @@ const queuedNuxs: {
|
||||
}) => boolean
|
||||
}[] = [
|
||||
{
|
||||
id: Nux.InitialVerificationAnnouncement,
|
||||
id: Nux.ActivitySubscriptions,
|
||||
enabled: ({currentProfile}) => {
|
||||
return isDaysOld(2, currentProfile.createdAt)
|
||||
return isExistingUserAsOf(
|
||||
'2025-07-03T00:00:00.000Z',
|
||||
currentProfile.createdAt,
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -111,7 +114,7 @@ function Inner({
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (snoozed) return
|
||||
if (snoozed) return // comment this out to test
|
||||
if (!nuxs) return
|
||||
|
||||
for (const {id, enabled} of queuedNuxs) {
|
||||
@@ -119,7 +122,7 @@ function Inner({
|
||||
|
||||
// check if completed first
|
||||
if (nux && nux.completed) {
|
||||
continue
|
||||
continue // comment this out to test
|
||||
}
|
||||
|
||||
// then check gate (track exposure)
|
||||
@@ -172,9 +175,7 @@ function Inner({
|
||||
return (
|
||||
<Context.Provider value={ctx}>
|
||||
{/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/}
|
||||
{activeNux === Nux.InitialVerificationAnnouncement && (
|
||||
<InitialVerificationAnnouncement />
|
||||
)}
|
||||
{activeNux === Nux.ActivitySubscriptions && <ActivitySubscriptionsNUX />}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,3 +16,18 @@ export function isDaysOld(days: number, createdAt?: string) {
|
||||
if (isOldEnough) return true
|
||||
return false
|
||||
}
|
||||
|
||||
export function isExistingUserAsOf(date: string, createdAt?: string) {
|
||||
/*
|
||||
* Should never happen because we gate NUXs to only accounts with a valid
|
||||
* profile and a `createdAt` (see `nuxs/index.tsx`). But if it ever did, the
|
||||
* account is either old enough to be pre-onboarding, or some failure happened
|
||||
* during account creation. Fail closed. - esb
|
||||
*/
|
||||
if (!createdAt) return false
|
||||
|
||||
const threshold = Date.parse(date)
|
||||
const then = new Date(createdAt).getTime()
|
||||
|
||||
return then < threshold
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
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',
|
||||
})
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedGetListFeed as GetListFeed,
|
||||
BskyAgent,
|
||||
type Agent,
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedGetListFeed as GetListFeed,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {FeedAPI, FeedAPIResponse} from './types'
|
||||
import {type FeedAPI, type FeedAPIResponse} from './types'
|
||||
|
||||
export class ListFeedAPI implements FeedAPI {
|
||||
agent: BskyAgent
|
||||
agent: Agent
|
||||
params: GetListFeed.QueryParams
|
||||
|
||||
constructor({
|
||||
agent,
|
||||
feedParams,
|
||||
}: {
|
||||
agent: BskyAgent
|
||||
agent: Agent
|
||||
feedParams: GetListFeed.QueryParams
|
||||
}) {
|
||||
this.agent = agent
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
type Agent,
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedGetPosts,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {type FeedAPI, type FeedAPIResponse} from './types'
|
||||
|
||||
export class PostListFeedAPI implements FeedAPI {
|
||||
agent: Agent
|
||||
params: AppBskyFeedGetPosts.QueryParams
|
||||
peek: AppBskyFeedDefs.FeedViewPost | null = null
|
||||
|
||||
constructor({
|
||||
agent,
|
||||
feedParams,
|
||||
}: {
|
||||
agent: Agent
|
||||
feedParams: AppBskyFeedGetPosts.QueryParams
|
||||
}) {
|
||||
this.agent = agent
|
||||
if (feedParams.uris.length > 25) {
|
||||
logger.warn(
|
||||
`Too many URIs provided - expected 25, got ${feedParams.uris.length}`,
|
||||
)
|
||||
}
|
||||
this.params = {
|
||||
uris: feedParams.uris.slice(0, 25),
|
||||
}
|
||||
}
|
||||
|
||||
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
||||
if (this.peek) return this.peek
|
||||
throw new Error('Has not fetched yet')
|
||||
}
|
||||
|
||||
async fetch({}: {}): Promise<FeedAPIResponse> {
|
||||
const res = await this.agent.app.bsky.feed.getPosts({
|
||||
...this.params,
|
||||
})
|
||||
if (res.success) {
|
||||
this.peek = {post: res.data.posts[0]}
|
||||
return {
|
||||
feed: res.data.posts.map(post => ({post})),
|
||||
}
|
||||
}
|
||||
return {
|
||||
feed: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {useEffect} from 'react'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import {type AppBskyNotificationListNotifications} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {CommonActions, useNavigation} from '@react-navigation/native'
|
||||
@@ -32,6 +32,7 @@ export type NotificationReason =
|
||||
| 'repost-via-repost'
|
||||
| 'verified'
|
||||
| 'unverified'
|
||||
| 'subscribed-post'
|
||||
|
||||
/**
|
||||
* Manually overridden type, but retains the possibility of
|
||||
@@ -112,61 +113,68 @@ export function useNotificationsHandler() {
|
||||
})
|
||||
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'like' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
'like' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Likes`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'repost' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
'repost' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Reposts`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'reply' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
'reply' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Replies`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'mention' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
'mention' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Mentions`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'quote' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
'quote' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Quotes`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'follow' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
'follow' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`New followers`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'like-via-repost' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
'like-via-repost' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Likes of your reposts`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'repost-via-repost' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
'repost-via-repost' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Reposts of your reposts`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'subscribed-post' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Activity from others`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
}, [_])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -220,6 +228,23 @@ export function useNotificationsHandler() {
|
||||
}
|
||||
} else {
|
||||
switch (payload.reason) {
|
||||
case 'subscribed-post':
|
||||
const urip = new AtUri(payload.uri)
|
||||
if (urip.collection === 'app.bsky.feed.post') {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error types are weird here
|
||||
navigation.navigate('HomeTab', {
|
||||
screen: 'PostThread',
|
||||
params: {
|
||||
name: urip.host,
|
||||
rkey: urip.rkey,
|
||||
},
|
||||
})
|
||||
}, 500)
|
||||
} else {
|
||||
resetToTab('NotificationsTab')
|
||||
}
|
||||
break
|
||||
case 'like':
|
||||
case 'repost':
|
||||
case 'follow':
|
||||
@@ -231,6 +256,7 @@ export function useNotificationsHandler() {
|
||||
case 'repost-via-repost':
|
||||
case 'verified':
|
||||
case 'unverified':
|
||||
default:
|
||||
resetToTab('NotificationsTab')
|
||||
break
|
||||
// TODO implement these after we have an idea of how to handle each individual case
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export function createSanitizedDisplayName(
|
||||
profile:
|
||||
| AppBskyActorDefs.ProfileViewBasic
|
||||
| AppBskyActorDefs.ProfileViewDetailed,
|
||||
profile: bsky.profile.AnyProfileView,
|
||||
noAt = false,
|
||||
) {
|
||||
if (profile.displayName != null && profile.displayName !== '') {
|
||||
|
||||
@@ -51,6 +51,7 @@ export type CommonNavigatorParams = {
|
||||
AppearanceSettings: undefined
|
||||
AccountSettings: undefined
|
||||
PrivacyAndSecuritySettings: undefined
|
||||
ActivityPrivacySettings: undefined
|
||||
ContentAndMediaSettings: undefined
|
||||
NotificationSettings: undefined
|
||||
ReplyNotificationSettings: undefined
|
||||
@@ -72,6 +73,7 @@ export type CommonNavigatorParams = {
|
||||
MessagesConversation: {conversation: string; embed?: string; accept?: true}
|
||||
MessagesSettings: undefined
|
||||
MessagesInbox: undefined
|
||||
NotificationsActivityList: {posts: string}
|
||||
LegacyNotificationSettings: undefined
|
||||
Feeds: undefined
|
||||
Start: {name: string; rkey: string}
|
||||
|
||||
@@ -7,7 +7,6 @@ export type Gate =
|
||||
| 'old_postonboarding'
|
||||
| 'onboarding_add_video_feed'
|
||||
| 'post_threads_v2_unspecced'
|
||||
| 'reengagement_features'
|
||||
| 'remove_show_latest_button'
|
||||
| 'test_gate_1'
|
||||
| 'test_gate_2'
|
||||
|
||||
@@ -443,4 +443,17 @@ export type MetricEvents = {
|
||||
[key: string]: any
|
||||
}
|
||||
'thread:click:headerMenuOpen': {}
|
||||
'activitySubscription:enable': {
|
||||
setting: 'posts' | 'posts_and_replies'
|
||||
}
|
||||
'activitySubscription:disable': {}
|
||||
'activityPreference:changeChannels': {
|
||||
name: string
|
||||
push: boolean
|
||||
list: boolean
|
||||
}
|
||||
'activityPreference:changeFilter': {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export const router = new Router<AllNavigatableRoutes>({
|
||||
Search: '/search',
|
||||
Feeds: '/feeds',
|
||||
Notifications: '/notifications',
|
||||
NotificationsActivityList: '/notifications/activity',
|
||||
LegacyNotificationSettings: '/notifications/settings',
|
||||
Settings: '/settings',
|
||||
Lists: '/lists',
|
||||
@@ -50,6 +51,7 @@ export const router = new Router<AllNavigatableRoutes>({
|
||||
SavedFeeds: '/settings/saved-feeds',
|
||||
AccountSettings: '/settings/account',
|
||||
PrivacyAndSecuritySettings: '/settings/privacy-and-security',
|
||||
ActivityPrivacySettings: '/settings/privacy-and-security/activity',
|
||||
ContentAndMediaSettings: '/settings/content-and-media',
|
||||
InterestsSettings: '/settings/interests',
|
||||
AboutSettings: '/settings/about',
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {type AllNavigatorParams} from '#/lib/routes/types'
|
||||
import {PostFeed} from '#/view/com/posts/PostFeed'
|
||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {ListFooter} from '#/components/Lists'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
AllNavigatorParams,
|
||||
'NotificationsActivityList'
|
||||
>
|
||||
export function NotificationsActivityListScreen({
|
||||
route: {
|
||||
params: {posts},
|
||||
},
|
||||
}: Props) {
|
||||
const uris = decodeURIComponent(posts)
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="NotificationsActivityListScreen">
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Notifications</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<PostFeed
|
||||
feed={`posts|${uris}`}
|
||||
disablePoll
|
||||
renderEmptyState={() => (
|
||||
<EmptyState icon="growth" message={_(msg`No posts here`)} />
|
||||
)}
|
||||
renderEndOfFeed={() => <ListFooter />}
|
||||
/>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
@@ -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<AppBskyActorDefs.ProfileViewDetailed> =
|
||||
useProfileShadow(profileUnshadowed)
|
||||
const profile =
|
||||
useProfileShadow<AppBskyActorDefs.ProfileViewDetailed>(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 (
|
||||
<ProfileHeaderShell
|
||||
profile={profile}
|
||||
@@ -198,6 +211,12 @@ let ProfileHeaderStandard = ({
|
||||
)
|
||||
) : !profile.viewer?.blockedBy ? (
|
||||
<>
|
||||
{hasSession && subscriptionsAllowed && (
|
||||
<SubscribeProfileButton
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
)}
|
||||
{hasSession && <MessageProfileButton profile={profile} />}
|
||||
|
||||
<Button
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {
|
||||
useHapticsDisabled,
|
||||
@@ -16,12 +16,10 @@ import {
|
||||
} from '#/state/preferences/large-alt-badge'
|
||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
|
||||
import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
CommonNavigatorParams,
|
||||
@@ -100,19 +98,6 @@ export function AccessibilitySettingsScreen({}: Props) {
|
||||
</SettingsList.Group>
|
||||
</>
|
||||
)}
|
||||
<SettingsList.Item>
|
||||
<Admonition type="info" style={[a.flex_1]}>
|
||||
<Trans>
|
||||
Autoplay options have moved to the{' '}
|
||||
<InlineLinkText
|
||||
to="/settings/content-and-media"
|
||||
label={_(msg`Content and media`)}>
|
||||
Content and Media settings
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
</SettingsList.Item>
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyNotificationDeclaration} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {
|
||||
type AllNavigatorParams,
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {
|
||||
useNotificationDeclarationMutation,
|
||||
useNotificationDeclarationQuery,
|
||||
} from '#/state/queries/activity-subscriptions'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as SettingsList from './components/SettingsList'
|
||||
import {ItemTextWithSubtitle} from './NotificationSettings/components/ItemTextWithSubtitle'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
AllNavigatorParams,
|
||||
'ActivityPrivacySettings'
|
||||
>
|
||||
export function ActivityPrivacySettingsScreen({}: Props) {
|
||||
const {
|
||||
data: notificationDeclaration,
|
||||
isPending,
|
||||
isError,
|
||||
} = useNotificationDeclarationQuery()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Privacy and Security</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<Layout.Content>
|
||||
<SettingsList.Container>
|
||||
<SettingsList.Item style={[a.align_start]}>
|
||||
<SettingsList.ItemIcon icon={BellRingingIcon} />
|
||||
<ItemTextWithSubtitle
|
||||
bold
|
||||
titleText={
|
||||
<Trans>Allow others to be notified of your posts</Trans>
|
||||
}
|
||||
subtitleText={
|
||||
<Trans>
|
||||
This feature allows users to receive notifications for your
|
||||
new posts and replies. Who do you want to enable this for?
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</SettingsList.Item>
|
||||
<View style={[a.px_xl, a.pt_md]}>
|
||||
{isError ? (
|
||||
<Admonition type="error">
|
||||
<Trans>Failed to load preference.</Trans>
|
||||
</Admonition>
|
||||
) : isPending ? (
|
||||
<View style={[a.w_full, a.pt_5xl, a.align_center]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
) : (
|
||||
<Inner notificationDeclaration={notificationDeclaration} />
|
||||
)}
|
||||
</View>
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
export function Inner({
|
||||
notificationDeclaration,
|
||||
}: {
|
||||
notificationDeclaration: {
|
||||
uri?: string
|
||||
cid?: string
|
||||
value: AppBskyNotificationDeclaration.Record
|
||||
}
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {mutate} = useNotificationDeclarationMutation()
|
||||
|
||||
const onChangeFilter = ([declaration]: string[]) => {
|
||||
mutate({
|
||||
$type: 'app.bsky.notification.declaration',
|
||||
allowSubscriptions: declaration,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Toggle.Group
|
||||
type="radio"
|
||||
label={_(
|
||||
msg`Filter who can opt to receive notifications for your activity`,
|
||||
)}
|
||||
values={[notificationDeclaration.value.allowSubscriptions]}
|
||||
onChange={onChangeFilter}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Toggle.Item
|
||||
label={_(msg`Anyone who follows me`)}
|
||||
name="followers"
|
||||
style={[a.flex_row, a.py_xs, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[t.atoms.text, a.font_normal, a.text_md]}>
|
||||
<Trans>Anyone who follows me</Trans>
|
||||
</Toggle.LabelText>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item
|
||||
label={_(msg`Only followers who I follow`)}
|
||||
name="mutuals"
|
||||
style={[a.flex_row, a.py_xs, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[t.atoms.text, a.font_normal, a.text_md]}>
|
||||
<Trans>Only followers who I follow</Trans>
|
||||
</Toggle.LabelText>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item
|
||||
label={_(msg`No one`)}
|
||||
name="none"
|
||||
style={[a.flex_row, a.py_xs, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[t.atoms.text, a.font_normal, a.text_md]}>
|
||||
<Trans>No one</Trans>
|
||||
</Toggle.LabelText>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
)
|
||||
}
|
||||
@@ -7,12 +7,12 @@ import Animated, {
|
||||
LinearTransition,
|
||||
StretchOutY,
|
||||
} from 'react-native-reanimated'
|
||||
import {ComAtprotoServerListAppPasswords} from '@atproto/api'
|
||||
import {type ComAtprotoServerListAppPasswords} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {useCallback} from 'react'
|
||||
import Animated, {
|
||||
FadeInUp,
|
||||
FadeOutUp,
|
||||
@@ -9,14 +9,17 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {IS_INTERNAL} from '#/lib/app-info'
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {
|
||||
type CommonNavigatorParams,
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
|
||||
import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
|
||||
import {atoms as a, native, useAlf, useTheme} from '#/alf'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon'
|
||||
import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone'
|
||||
import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
|
||||
|
||||
@@ -2,9 +2,12 @@ import {Fragment} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {
|
||||
EmbedPlayerSource,
|
||||
type CommonNavigatorParams,
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {
|
||||
type EmbedPlayerSource,
|
||||
externalEmbedLabels,
|
||||
} from '#/lib/strings/embed-player'
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {
|
||||
type CommonNavigatorParams,
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
useSetFeedViewPreferencesMutation,
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {type ListRenderItemInfo, Text as RNText, View} from 'react-native'
|
||||
import {type ModerationOpts} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {
|
||||
type AllNavigatorParams,
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useActivitySubscriptionsQuery} from '#/state/queries/activity-subscriptions'
|
||||
import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {SubscribeProfileDialog} from '#/components/activity-notifications/SubscribeProfileDialog'
|
||||
import * as Admonition from '#/components/Admonition'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {BellRinging_Filled_Corner0_Rounded as BellRingingFilledIcon} from '#/components/icons/BellRinging'
|
||||
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {ListFooter} from '#/components/Lists'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import * as SettingsList from '../components/SettingsList'
|
||||
import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle'
|
||||
import {PreferenceControls} from './components/PreferenceControls'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
AllNavigatorParams,
|
||||
'ActivityNotificationSettings'
|
||||
>
|
||||
export function ActivityNotificationSettingsScreen({}: Props) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {data: preferences, isError} = useNotificationSettingsQuery()
|
||||
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
const {
|
||||
data: subscriptions,
|
||||
isPending,
|
||||
error,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
} = useActivitySubscriptionsQuery()
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (!subscriptions) return []
|
||||
return subscriptions?.pages.flatMap(page => page.subscriptions)
|
||||
}, [subscriptions])
|
||||
|
||||
const renderItem = useCallback(
|
||||
({item}: ListRenderItemInfo<bsky.profile.AnyProfileView>) => {
|
||||
if (!moderationOpts) return null
|
||||
return (
|
||||
<ActivitySubscriptionCard
|
||||
profile={item}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[moderationOpts],
|
||||
)
|
||||
|
||||
const onEndReached = useCallback(async () => {
|
||||
if (isFetchingNextPage || !hasNextPage || isError) return
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more likes', {message: err})
|
||||
}
|
||||
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Notifications</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<List
|
||||
ListHeaderComponent={
|
||||
<SettingsList.Container>
|
||||
<SettingsList.Item style={[a.align_start]}>
|
||||
<SettingsList.ItemIcon icon={BellRingingIcon} />
|
||||
<ItemTextWithSubtitle
|
||||
bold
|
||||
titleText={<Trans>Activity from others</Trans>}
|
||||
subtitleText={
|
||||
<Trans>
|
||||
Get notified about posts and replies from accounts you
|
||||
choose.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</SettingsList.Item>
|
||||
{isError ? (
|
||||
<View style={[a.px_lg, a.pt_md]}>
|
||||
<Admonition.Admonition type="error">
|
||||
<Trans>Failed to load notification settings.</Trans>
|
||||
</Admonition.Admonition>
|
||||
</View>
|
||||
) : (
|
||||
<PreferenceControls
|
||||
name="subscribedPost"
|
||||
preference={preferences?.subscribedPost}
|
||||
/>
|
||||
)}
|
||||
</SettingsList.Container>
|
||||
}
|
||||
data={items}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
onEndReached={onEndReached}
|
||||
onEndReachedThreshold={4}
|
||||
ListEmptyComponent={
|
||||
error ? null : (
|
||||
<View style={[a.px_xl, a.py_md]}>
|
||||
{!isPending ? (
|
||||
<Admonition.Outer type="tip">
|
||||
<Admonition.Row>
|
||||
<Admonition.Icon />
|
||||
<View style={[a.flex_1, a.gap_sm]}>
|
||||
<Admonition.Text>
|
||||
<Trans>
|
||||
Enable notifications for an account by visiting their
|
||||
profile and pressing the{' '}
|
||||
<RNText
|
||||
style={[a.font_bold, t.atoms.text_contrast_high]}>
|
||||
bell icon
|
||||
</RNText>{' '}
|
||||
<BellRingingFilledIcon
|
||||
size="xs"
|
||||
style={t.atoms.text_contrast_high}
|
||||
/>
|
||||
.
|
||||
</Trans>
|
||||
</Admonition.Text>
|
||||
<Admonition.Text>
|
||||
<Trans>
|
||||
If you want to restrict who can receive notifications
|
||||
for your account's activity, you can change this in{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Privacy and Security settings`)}
|
||||
to={{screen: 'ActivityPrivacySettings'}}
|
||||
style={[a.font_bold]}>
|
||||
Settings → Privacy and Security
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
</Admonition.Text>
|
||||
</View>
|
||||
</Admonition.Row>
|
||||
</Admonition.Outer>
|
||||
) : (
|
||||
<View style={[a.flex_1, a.align_center, a.pt_xl]}>
|
||||
<Loader size="lg" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
ListFooterComponent={
|
||||
<ListFooter
|
||||
style={[items.length === 0 && a.border_transparent]}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
error={cleanError(error)}
|
||||
onRetry={fetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
/>
|
||||
}
|
||||
windowSize={11}
|
||||
/>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function keyExtractor(item: bsky.profile.AnyProfileView) {
|
||||
return item.did
|
||||
}
|
||||
|
||||
function ActivitySubscriptionCard({
|
||||
profile: profileUnshadowed,
|
||||
moderationOpts,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
const control = useDialogControl()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const preview = useMemo(() => {
|
||||
const actSub = profile.viewer?.activitySubscription
|
||||
if (actSub?.post && actSub?.reply) {
|
||||
return _(msg`Posts, Replies`)
|
||||
} else if (actSub?.post) {
|
||||
return _(msg`Posts`)
|
||||
} else if (actSub?.reply) {
|
||||
return _(msg`Replies`)
|
||||
}
|
||||
return _(msg`None`)
|
||||
}, [_, profile.viewer?.activitySubscription])
|
||||
|
||||
return (
|
||||
<View style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}>
|
||||
<ProfileCard.Outer>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<View style={[a.flex_1, a.gap_2xs]}>
|
||||
<ProfileCard.NameAndHandle
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
inline
|
||||
/>
|
||||
<Text style={[a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
{preview}
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
label={_(
|
||||
msg`Edit notifications from ${createSanitizedDisplayName(
|
||||
profile,
|
||||
)}`,
|
||||
)}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
onPress={control.open}>
|
||||
<ButtonText>
|
||||
<Trans>Edit</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</ProfileCard.Header>
|
||||
</ProfileCard.Outer>
|
||||
|
||||
<SubscribeProfileDialog
|
||||
control={control}
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
includeProfile
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {type FilterablePreference} from '@atproto/api/dist/client/types/app/bsky
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {useNotificationSettingsUpdateMutation} from '#/state/queries/notifications/settings'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
@@ -28,10 +28,6 @@ export function PreferenceControls({
|
||||
preference?: AppBskyNotificationDefs.Preference | FilterablePreference
|
||||
allowDisableInApp?: boolean
|
||||
}) {
|
||||
const gate = useGate()
|
||||
|
||||
if (!gate('reengagement_features')) return null
|
||||
|
||||
if (!preference)
|
||||
return (
|
||||
<View style={[a.w_full, a.pt_5xl, a.align_center]}>
|
||||
@@ -78,6 +74,12 @@ export function Inner({
|
||||
push: change.includes('push'),
|
||||
} satisfies typeof preference
|
||||
|
||||
logger.metric('activityPreference:changeChannels', {
|
||||
name,
|
||||
push: newPreference.push,
|
||||
list: newPreference.list,
|
||||
})
|
||||
|
||||
mutate({
|
||||
[name]: newPreference,
|
||||
...Object.fromEntries(syncOthers.map(key => [key, newPreference])),
|
||||
@@ -93,6 +95,8 @@ export function Inner({
|
||||
include: change,
|
||||
} satisfies typeof preference
|
||||
|
||||
logger.metric('activityPreference:changeFilter', {name, value: change})
|
||||
|
||||
mutate({
|
||||
[name]: newPreference,
|
||||
...Object.fromEntries(syncOthers.map(key => [key, newPreference])),
|
||||
@@ -114,7 +118,7 @@ export function Inner({
|
||||
a.py_xs,
|
||||
platform({
|
||||
native: [a.justify_between],
|
||||
web: [a.flex_row_reverse, a.gap_md],
|
||||
web: [a.flex_row_reverse, a.gap_sm],
|
||||
}),
|
||||
]}>
|
||||
<Toggle.LabelText
|
||||
@@ -131,7 +135,7 @@ export function Inner({
|
||||
a.py_xs,
|
||||
platform({
|
||||
native: [a.justify_between],
|
||||
web: [a.flex_row_reverse, a.gap_md],
|
||||
web: [a.flex_row_reverse, a.gap_sm],
|
||||
}),
|
||||
]}>
|
||||
<Toggle.LabelText
|
||||
@@ -159,11 +163,7 @@ export function Inner({
|
||||
<Toggle.Item
|
||||
label={_(msg`Everyone`)}
|
||||
name="all"
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.py_xs,
|
||||
platform({native: [a.gap_sm], web: [a.gap_md]}),
|
||||
]}>
|
||||
style={[a.flex_row, a.py_xs, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText
|
||||
style={[
|
||||
@@ -177,11 +177,7 @@ export function Inner({
|
||||
<Toggle.Item
|
||||
label={_(msg`People I follow`)}
|
||||
name="follows"
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.py_xs,
|
||||
platform({native: [a.gap_sm], web: [a.gap_md]}),
|
||||
]}>
|
||||
style={[a.flex_row, a.py_xs, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText
|
||||
style={[
|
||||
|
||||
@@ -16,7 +16,7 @@ import {useNotificationSettingsQuery} from '#/state/queries/notifications/settin
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At'
|
||||
// import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||
import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble'
|
||||
import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
|
||||
import {
|
||||
@@ -183,20 +183,19 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
showSkeleton={!settings}
|
||||
/>
|
||||
</SettingsList.LinkItem>
|
||||
{/* <SettingsList.LinkItem
|
||||
<SettingsList.LinkItem
|
||||
label={_(msg`Settings for activity alerts`)}
|
||||
to={{screen: 'ActivityNotificationSettings'}}
|
||||
contentContainerStyle={[a.align_start]}>
|
||||
<SettingsList.ItemIcon icon={BellRingingIcon} />
|
||||
|
||||
<ItemTextWithSubtitle
|
||||
titleText={<Trans>Activity alerts</Trans>}
|
||||
titleText={<Trans>Activity from others</Trans>}
|
||||
subtitleText={
|
||||
<SettingPreview preference={settings?.subscribedPost} />
|
||||
}
|
||||
showSkeleton={!settings}
|
||||
/>
|
||||
</SettingsList.LinkItem> */}
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
label={_(
|
||||
msg`Settings for notifications for likes of your reposts`,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyNotificationDeclaration} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {useNotificationDeclarationQuery} from '#/state/queries/activity-subscriptions'
|
||||
import {useAppPasswordsQuery} from '#/state/queries/app-passwords'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Admonition from '#/components/Admonition'
|
||||
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash'
|
||||
import {Key_Stroke2_Corner2_Rounded as KeyIcon} from '#/components/icons/Key'
|
||||
import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield'
|
||||
@@ -16,6 +19,7 @@ import * as Layout from '#/components/Layout'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {Email2FAToggle} from './components/Email2FAToggle'
|
||||
import {PwiOptOut} from './components/PwiOptOut'
|
||||
import {ItemTextWithSubtitle} from './NotificationSettings/components/ItemTextWithSubtitle'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
CommonNavigatorParams,
|
||||
@@ -26,6 +30,11 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
|
||||
const t = useTheme()
|
||||
const {data: appPasswords} = useAppPasswordsQuery()
|
||||
const {currentAccount} = useSession()
|
||||
const {
|
||||
data: notificationDeclaration,
|
||||
isPending,
|
||||
isError,
|
||||
} = useNotificationDeclarationQuery()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
@@ -71,6 +80,24 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
|
||||
</SettingsList.BadgeText>
|
||||
)}
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
label={_(msg`Settings for activity alerts`)}
|
||||
to={{screen: 'ActivityPrivacySettings'}}
|
||||
contentContainerStyle={[a.align_start]}>
|
||||
<SettingsList.ItemIcon icon={BellRingingIcon} />
|
||||
<ItemTextWithSubtitle
|
||||
titleText={
|
||||
<Trans>Allow others to be notified of your posts</Trans>
|
||||
}
|
||||
subtitleText={
|
||||
<NotificationDeclaration
|
||||
data={notificationDeclaration}
|
||||
isError={isError}
|
||||
/>
|
||||
}
|
||||
showSkeleton={isPending}
|
||||
/>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.Divider />
|
||||
<SettingsList.Group>
|
||||
<SettingsList.ItemIcon icon={EyeSlashIcon} />
|
||||
@@ -111,3 +138,26 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function NotificationDeclaration({
|
||||
data,
|
||||
isError,
|
||||
}: {
|
||||
data?: {
|
||||
value: AppBskyNotificationDeclaration.Record
|
||||
}
|
||||
isError?: boolean
|
||||
}) {
|
||||
if (isError) {
|
||||
return <Trans>Error loading preference</Trans>
|
||||
}
|
||||
switch (data?.value?.allowSubscriptions) {
|
||||
case 'mutuals':
|
||||
return <Trans>Only followers who I follow</Trans>
|
||||
case 'none':
|
||||
return <Trans>No one</Trans>
|
||||
case 'followers':
|
||||
default:
|
||||
return <Trans>Anyone who follows me</Trans>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {LayoutAnimation, Pressable, View} from 'react-native'
|
||||
import {Linking} from 'react-native'
|
||||
import {useReducedMotion} from 'react-native-reanimated'
|
||||
import {type AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||
import {msg, t, Trans} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
type CommonNavigatorParams,
|
||||
type NavigationProp,
|
||||
} from '#/lib/routes/types'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
@@ -64,6 +63,7 @@ import {
|
||||
shouldShowVerificationCheckButton,
|
||||
VerificationCheckButton,
|
||||
} from '#/components/verification/VerificationCheckButton'
|
||||
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
|
||||
export function SettingsScreen({}: Props) {
|
||||
@@ -82,7 +82,6 @@ export function SettingsScreen({}: Props) {
|
||||
const {pendingDid, onPressSwitchAccount} = useAccountSwitcher()
|
||||
const [showAccounts, setShowAccounts] = useState(false)
|
||||
const [showDevOptions, setShowDevOptions] = useState(false)
|
||||
const gate = useGate()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
@@ -183,16 +182,14 @@ export function SettingsScreen({}: Props) {
|
||||
<Trans>Moderation</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
{gate('reengagement_features') && (
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/notifications"
|
||||
label={_(msg`Notifications`)}>
|
||||
<SettingsList.ItemIcon icon={NotificationIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Notifications</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
)}
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/notifications"
|
||||
label={_(msg`Notifications`)}>
|
||||
<SettingsList.ItemIcon icon={NotificationIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Notifications</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/content-and-media"
|
||||
label={_(msg`Content and media`)}>
|
||||
@@ -364,6 +361,7 @@ function DevOptions() {
|
||||
const onboardingDispatch = useOnboardingDispatch()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
|
||||
const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged()
|
||||
|
||||
const resetOnboarding = async () => {
|
||||
navigation.navigate('Home')
|
||||
@@ -384,7 +382,11 @@ function DevOptions() {
|
||||
...persisted.get('reminders'),
|
||||
lastEmailConfirm: lastEmailConfirm.toISOString(),
|
||||
})
|
||||
Toast.show(t`You probably want to restart the app now.`)
|
||||
Toast.show(_(msg`You probably want to restart the app now.`))
|
||||
}
|
||||
|
||||
const onPressActySubsUnNudge = () => {
|
||||
setActyNotifNudged(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -431,6 +433,15 @@ function DevOptions() {
|
||||
<Trans>Unsnooze email reminder</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</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
|
||||
onPress={() => clearAllStorage()}
|
||||
label={_(msg`Clear all storage data`)}>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import React, {useContext, useMemo} from 'react'
|
||||
import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {createContext, useContext, useMemo} from 'react'
|
||||
import {
|
||||
type GestureResponderEvent,
|
||||
type StyleProp,
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
||||
import * as Button from '#/components/Button'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
|
||||
import {Link, LinkProps} from '#/components/Link'
|
||||
import {Link, type LinkProps} from '#/components/Link'
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const ItemContext = React.createContext({
|
||||
const ItemContext = createContext({
|
||||
destructive: false,
|
||||
withinGroup: false,
|
||||
})
|
||||
@@ -91,7 +96,7 @@ export function Item({
|
||||
a.px_xl,
|
||||
a.py_sm,
|
||||
a.align_center,
|
||||
a.gap_md,
|
||||
a.gap_sm,
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
{minHeight: 48},
|
||||
@@ -100,9 +105,9 @@ export function Item({
|
||||
// existing padding
|
||||
a.pl_xl.paddingLeft +
|
||||
// icon
|
||||
28 +
|
||||
24 +
|
||||
// gap
|
||||
a.gap_md.gap,
|
||||
a.gap_sm.gap,
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
@@ -175,7 +180,7 @@ export function PressableItem({
|
||||
|
||||
export function ItemIcon({
|
||||
icon: Comp,
|
||||
size = 'xl',
|
||||
size = 'lg',
|
||||
color: colorProp,
|
||||
}: Omit<React.ComponentProps<typeof Button.ButtonIcon>, 'position'> & {
|
||||
color?: string
|
||||
|
||||
Vendored
+10
-3
@@ -1,9 +1,10 @@
|
||||
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'
|
||||
|
||||
import {batchedUpdates} from '#/lib/batchedUpdates'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInActivitySubscriptionsQueryData} from '#/state/queries/activity-subscriptions'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInActorSearchQueryData} from '#/state/queries/actor-search'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInExploreFeedPreviewsQueryData} from '#/state/queries/explore-feed-previews'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData} from '#/state/queries/known-followers'
|
||||
@@ -33,6 +34,7 @@ export interface ProfileShadow {
|
||||
blockingUri: string | undefined
|
||||
verification: AppBskyActorDefs.VerificationState
|
||||
status: AppBskyActorDefs.StatusView | undefined
|
||||
activitySubscription: AppBskyNotificationDefs.ActivitySubscription | undefined
|
||||
}
|
||||
|
||||
const shadows: WeakMap<
|
||||
@@ -114,8 +116,8 @@ export function updateProfileShadow(
|
||||
value: Partial<ProfileShadow>,
|
||||
) {
|
||||
const cachedProfiles = findProfilesInCache(queryClient, did)
|
||||
for (let post of cachedProfiles) {
|
||||
shadows.set(post, {...shadows.get(post), ...value})
|
||||
for (let profile of cachedProfiles) {
|
||||
shadows.set(profile, {...shadows.get(profile), ...value})
|
||||
}
|
||||
batchedUpdates(() => {
|
||||
emitter.emit(did, value)
|
||||
@@ -137,6 +139,10 @@ function mergeShadow<TProfileView extends bsky.profile.AnyProfileView>(
|
||||
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,
|
||||
@@ -171,4 +177,5 @@ function* findProfilesInCache(
|
||||
yield* findAllProfilesInPostThreadV2QueryData(queryClient, did)
|
||||
yield* findAllProfilesInKnownFollowersQueryData(queryClient, did)
|
||||
yield* findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did)
|
||||
yield* findAllProfilesInActivitySubscriptionsQueryData(queryClient, did)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
type AppBskyNotificationDeclaration,
|
||||
type AppBskyNotificationListActivitySubscriptions,
|
||||
} from '@atproto/api'
|
||||
import {t} from '@lingui/macro'
|
||||
import {
|
||||
type InfiniteData,
|
||||
type QueryClient,
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
|
||||
export const RQKEY_getActivitySubscriptions = ['activity-subscriptions']
|
||||
export const RQKEY_getNotificationDeclaration = ['notification-declaration']
|
||||
|
||||
export function useActivitySubscriptionsQuery() {
|
||||
const agent = useAgent()
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: RQKEY_getActivitySubscriptions,
|
||||
queryFn: async ({pageParam}) => {
|
||||
const response =
|
||||
await agent.app.bsky.notification.listActivitySubscriptions({
|
||||
cursor: pageParam,
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
initialPageParam: undefined as string | undefined,
|
||||
getNextPageParam: prev => prev.cursor,
|
||||
})
|
||||
}
|
||||
|
||||
export function useNotificationDeclarationQuery() {
|
||||
const agent = useAgent()
|
||||
const {currentAccount} = useSession()
|
||||
return useQuery({
|
||||
queryKey: RQKEY_getNotificationDeclaration,
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await agent.app.bsky.notification.declaration.get({
|
||||
repo: currentAccount!.did,
|
||||
rkey: 'self',
|
||||
})
|
||||
return response
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
err.message.startsWith('Could not locate record')
|
||||
) {
|
||||
return {
|
||||
value: {
|
||||
$type: 'app.bsky.notification.declaration',
|
||||
allowSubscriptions: 'followers',
|
||||
} satisfies AppBskyNotificationDeclaration.Record,
|
||||
}
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useNotificationDeclarationMutation() {
|
||||
const agent = useAgent()
|
||||
const {currentAccount} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (record: AppBskyNotificationDeclaration.Record) => {
|
||||
const response = await agent.app.bsky.notification.declaration.put(
|
||||
{
|
||||
repo: currentAccount!.did,
|
||||
rkey: 'self',
|
||||
},
|
||||
record,
|
||||
)
|
||||
return response
|
||||
},
|
||||
onMutate: value => {
|
||||
queryClient.setQueryData(
|
||||
RQKEY_getNotificationDeclaration,
|
||||
(old?: {
|
||||
uri: string
|
||||
cid: string
|
||||
value: AppBskyNotificationDeclaration.Record
|
||||
}) => {
|
||||
if (!old) return old
|
||||
return {
|
||||
value,
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(t`Failed to update notification declaration`)
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: RQKEY_getNotificationDeclaration,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function* findAllProfilesInQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
): Generator<AppBskyActorDefs.ProfileView, void> {
|
||||
const queryDatas = queryClient.getQueriesData<
|
||||
InfiniteData<AppBskyNotificationListActivitySubscriptions.OutputSchema>
|
||||
>({
|
||||
queryKey: RQKEY_getActivitySubscriptions,
|
||||
})
|
||||
for (const [_queryKey, queryData] of queryDatas) {
|
||||
if (!queryData?.pages) {
|
||||
continue
|
||||
}
|
||||
for (const page of queryData.pages) {
|
||||
for (const subscription of page.subscriptions) {
|
||||
if (subscription.did === did) {
|
||||
yield subscription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
AppBskyGraphDefs,
|
||||
AppBskyGraphGetList,
|
||||
BskyAgent,
|
||||
type AppBskyActorDefs,
|
||||
type AppBskyGraphDefs,
|
||||
type AppBskyGraphGetList,
|
||||
type BskyAgent,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
InfiniteData,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
type InfiniteData,
|
||||
type QueryClient,
|
||||
type QueryKey,
|
||||
useInfiniteQuery,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query'
|
||||
@@ -100,21 +100,16 @@ export function* findAllProfilesInQueryData(
|
||||
queryKey: [RQKEY_ROOT],
|
||||
})
|
||||
for (const [_queryKey, queryData] of queryDatas) {
|
||||
if (!queryData) {
|
||||
if (!queryData?.pages) {
|
||||
continue
|
||||
}
|
||||
for (const [_queryKey, queryData] of queryDatas) {
|
||||
if (!queryData?.pages) {
|
||||
continue
|
||||
for (const page of queryData?.pages) {
|
||||
if (page.list.creator.did === did) {
|
||||
yield page.list.creator
|
||||
}
|
||||
for (const page of queryData?.pages) {
|
||||
if (page.list.creator.did === did) {
|
||||
yield page.list.creator
|
||||
}
|
||||
for (const item of page.items) {
|
||||
if (item.subject.did === did) {
|
||||
yield item.subject
|
||||
}
|
||||
for (const item of page.items) {
|
||||
if (item.subject.did === did) {
|
||||
yield item.subject
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
@@ -19,7 +19,7 @@ export function useUpdateActorDeclaration({
|
||||
return useMutation({
|
||||
mutationFn: async (allowIncoming: 'all' | 'none' | 'following') => {
|
||||
if (!currentAccount) throw new Error('Not signed in')
|
||||
const result = await agent.api.com.atproto.repo.putRecord({
|
||||
const result = await agent.com.atproto.repo.putRecord({
|
||||
repo: currentAccount.did,
|
||||
collection: 'chat.bsky.actor.declaration',
|
||||
rkey: 'self',
|
||||
|
||||
@@ -18,30 +18,30 @@
|
||||
|
||||
import {useCallback, useEffect, useMemo, useRef} from 'react'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
type AppBskyActorDefs,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AtUri,
|
||||
moderatePost,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
InfiniteData,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
type InfiniteData,
|
||||
type QueryClient,
|
||||
type QueryKey,
|
||||
useInfiniteQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies'
|
||||
import {useModerationOpts} from '../../preferences/moderation-opts'
|
||||
import {STALE} from '..'
|
||||
import {
|
||||
didOrHandleUriMatches,
|
||||
embedViewRecordToPostView,
|
||||
getEmbeddedPost,
|
||||
} from '../util'
|
||||
import {FeedPage} from './types'
|
||||
import {type FeedPage} from './types'
|
||||
import {useUnreadNotificationsApi} from './unread'
|
||||
import {fetchPage} from './util'
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ type OtherNotificationType =
|
||||
| 'unverified'
|
||||
| 'like-via-repost'
|
||||
| 'repost-via-repost'
|
||||
| 'subscribed-post'
|
||||
| 'unknown'
|
||||
|
||||
type FeedNotificationBase = {
|
||||
|
||||
@@ -28,6 +28,7 @@ const GROUPABLE_REASONS = [
|
||||
'follow',
|
||||
'like-via-repost',
|
||||
'repost-via-repost',
|
||||
'subscribed-post',
|
||||
]
|
||||
const MS_1HR = 1e3 * 60 * 60
|
||||
const MS_2DAY = MS_1HR * 48
|
||||
@@ -144,7 +145,8 @@ export function groupNotifications(
|
||||
Math.abs(ts2 - ts) < MS_2DAY &&
|
||||
notif.reason === groupedNotif.notification.reason &&
|
||||
notif.reasonSubject === groupedNotif.notification.reasonSubject &&
|
||||
notif.author.did !== groupedNotif.notification.author.did
|
||||
(notif.author.did !== groupedNotif.notification.author.did ||
|
||||
notif.reason === 'subscribed-post')
|
||||
) {
|
||||
const nextIsFollowBack =
|
||||
notif.reason === 'follow' && notif.author.viewer?.following
|
||||
@@ -252,7 +254,8 @@ function toKnownType(
|
||||
notif.reason === 'verified' ||
|
||||
notif.reason === 'unverified' ||
|
||||
notif.reason === 'like-via-repost' ||
|
||||
notif.reason === 'repost-via-repost'
|
||||
notif.reason === 'repost-via-repost' ||
|
||||
notif.reason === 'subscribed-post'
|
||||
) {
|
||||
return notif.reason as NotificationType
|
||||
}
|
||||
@@ -263,7 +266,12 @@ function getSubjectUri(
|
||||
type: NotificationType,
|
||||
notif: AppBskyNotificationListNotifications.Notification,
|
||||
): string | undefined {
|
||||
if (type === 'reply' || type === 'quote' || type === 'mention') {
|
||||
if (
|
||||
type === 'reply' ||
|
||||
type === 'quote' ||
|
||||
type === 'mention' ||
|
||||
type === 'subscribed-post'
|
||||
) {
|
||||
return notif.uri
|
||||
} else if (
|
||||
type === 'post-like' ||
|
||||
|
||||
@@ -6,6 +6,7 @@ export enum Nux {
|
||||
NeueTypography = 'NeueTypography',
|
||||
ExploreInterestsCard = 'ExploreInterestsCard',
|
||||
InitialVerificationAnnouncement = 'InitialVerificationAnnouncement',
|
||||
ActivitySubscriptions = 'ActivitySubscriptions',
|
||||
}
|
||||
|
||||
export const nuxNames = new Set(Object.values(Nux))
|
||||
@@ -23,10 +24,15 @@ export type AppNux = BaseNux<
|
||||
id: Nux.InitialVerificationAnnouncement
|
||||
data: undefined
|
||||
}
|
||||
| {
|
||||
id: Nux.ActivitySubscriptions
|
||||
data: undefined
|
||||
}
|
||||
>
|
||||
|
||||
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
|
||||
[Nux.NeueTypography]: undefined,
|
||||
[Nux.ExploreInterestsCard]: undefined,
|
||||
[Nux.InitialVerificationAnnouncement]: undefined,
|
||||
[Nux.ActivitySubscriptions]: undefined,
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {HomeFeedAPI} from '#/lib/api/feed/home'
|
||||
import {LikesFeedAPI} from '#/lib/api/feed/likes'
|
||||
import {ListFeedAPI} from '#/lib/api/feed/list'
|
||||
import {MergeFeedAPI} from '#/lib/api/feed/merge'
|
||||
import {PostListFeedAPI} from '#/lib/api/feed/posts'
|
||||
import {type FeedAPI, type ReasonFeedSource} from '#/lib/api/feed/types'
|
||||
import {aggregateUserInterests} from '#/lib/api/feed/utils'
|
||||
import {FeedTuner, type FeedTunerFn} from '#/lib/api/feed-manip'
|
||||
@@ -53,6 +54,7 @@ export type AuthorFilter =
|
||||
| 'posts_with_video'
|
||||
type FeedUri = string
|
||||
type ListUri = string
|
||||
type PostsUriList = string
|
||||
|
||||
export type FeedDescriptor =
|
||||
| 'following'
|
||||
@@ -60,6 +62,7 @@ export type FeedDescriptor =
|
||||
| `feedgen|${FeedUri}`
|
||||
| `likes|${ActorDid}`
|
||||
| `list|${ListUri}`
|
||||
| `posts|${PostsUriList}`
|
||||
| 'demo'
|
||||
export interface FeedParams {
|
||||
mergeFeedEnabled?: boolean
|
||||
@@ -488,6 +491,9 @@ function createApi({
|
||||
} else if (feedDesc.startsWith('list')) {
|
||||
const [_, list] = feedDesc.split('|')
|
||||
return new ListFeedAPI({agent, feedParams: {list}})
|
||||
} else if (feedDesc.startsWith('posts')) {
|
||||
const [_, uriList] = feedDesc.split('|')
|
||||
return new PostListFeedAPI({agent, feedParams: {uris: uriList.split(',')}})
|
||||
} else if (feedDesc === 'demo') {
|
||||
return new DemoFeedAPI({agent})
|
||||
} else {
|
||||
|
||||
@@ -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
|
||||
devMode: boolean
|
||||
demoMode: boolean
|
||||
activitySubscriptionsNudged?: boolean
|
||||
}
|
||||
|
||||
export type Account = {
|
||||
|
||||
@@ -52,6 +52,7 @@ import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
||||
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {BellRinging_Filled_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||
import {
|
||||
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
|
||||
ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
|
||||
@@ -114,7 +115,9 @@ let NotificationFeedItem = ({
|
||||
case 'unverified': {
|
||||
return makeProfileLink(item.notification.author)
|
||||
}
|
||||
case 'reply': {
|
||||
case 'reply':
|
||||
case 'mention':
|
||||
case 'quote': {
|
||||
const uripReply = new AtUri(item.notification.uri)
|
||||
return `/profile/${uripReply.host}/post/${uripReply.rkey}`
|
||||
}
|
||||
@@ -126,6 +129,13 @@ let NotificationFeedItem = ({
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'subscribed-post': {
|
||||
const posts: string[] = []
|
||||
for (const post of [item.notification, ...(item.additional ?? [])]) {
|
||||
posts.push(post.uri)
|
||||
}
|
||||
return `/notifications/activity?posts=${encodeURIComponent(posts.slice(0, 25).join(','))}`
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
@@ -155,7 +165,10 @@ let NotificationFeedItem = ({
|
||||
href: makeProfileLink(author),
|
||||
moderation: moderateProfile(author, moderationOpts),
|
||||
})) || []),
|
||||
]
|
||||
].filter(
|
||||
(author, index, arr) =>
|
||||
arr.findIndex(au => au.profile.did === author.profile.did) === index,
|
||||
)
|
||||
}, [item, moderationOpts])
|
||||
|
||||
const niceTimestamp = niceDate(i18n, item.notification.indexedAt)
|
||||
@@ -503,6 +516,42 @@ let NotificationFeedItem = ({
|
||||
<Trans>{firstAuthorLink} reposted your repost</Trans>
|
||||
)
|
||||
icon = <RepostIcon size="xl" style={{color: t.palette.positive_600}} />
|
||||
} else if (item.type === 'subscribed-post') {
|
||||
const postsCount = 1 + (item.additional?.length || 0)
|
||||
a11yLabel = hasMultipleAuthors
|
||||
? _(
|
||||
msg`New posts from ${firstAuthorName} and ${plural(
|
||||
additionalAuthorsCount,
|
||||
{
|
||||
one: `${formattedAuthorsCount} other`,
|
||||
other: `${formattedAuthorsCount} others`,
|
||||
},
|
||||
)}`,
|
||||
)
|
||||
: _(
|
||||
msg`New ${plural(postsCount, {
|
||||
one: 'post',
|
||||
other: 'posts',
|
||||
})} from ${firstAuthorName}`,
|
||||
)
|
||||
notificationContent = hasMultipleAuthors ? (
|
||||
<Trans>
|
||||
New posts from {firstAuthorLink} and{' '}
|
||||
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
|
||||
<Plural
|
||||
value={additionalAuthorsCount}
|
||||
one={`${formattedAuthorsCount} other`}
|
||||
other={`${formattedAuthorsCount} others`}
|
||||
/>
|
||||
</Text>{' '}
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
New <Plural value={postsCount} one="post" other="posts" /> from{' '}
|
||||
{firstAuthorLink}
|
||||
</Trans>
|
||||
)
|
||||
icon = <BellRingingIcon size="xl" style={{color: t.palette.primary_500}} />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@@ -613,7 +662,8 @@ let NotificationFeedItem = ({
|
||||
{item.type === 'post-like' ||
|
||||
item.type === 'repost' ||
|
||||
item.type === 'like-via-repost' ||
|
||||
item.type === 'repost-via-repost' ? (
|
||||
item.type === 'repost-via-repost' ||
|
||||
item.type === 'subscribed-post' ? (
|
||||
<View style={[a.pt_2xs]}>
|
||||
<AdditionalPostText post={item.subject} />
|
||||
</View>
|
||||
|
||||
@@ -96,7 +96,8 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
a.font_bold,
|
||||
t.atoms.text,
|
||||
a.leading_tight,
|
||||
{maxWidth: '70%', flexShrink: 0},
|
||||
a.flex_shrink_0,
|
||||
{maxWidth: '70%'},
|
||||
]}>
|
||||
{forceLTR(
|
||||
sanitizeDisplayName(
|
||||
|
||||
@@ -63,10 +63,10 @@
|
||||
"@atproto/xrpc" "^0.7.0"
|
||||
"@atproto/xrpc-server" "^0.8.0"
|
||||
|
||||
"@atproto/api@^0.15.16":
|
||||
version "0.15.16"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.16.tgz#1962e7067e03a661e17c3164874596ef1e7ed7ad"
|
||||
integrity sha512-ZNBrzBg2l0lHreKik1lJn8lrhAktwlY8NUPBU/hO9dwjAnDHQTiSzNFZt65dp9djmqZ75sX/VJ+heNuaJBvnhQ==
|
||||
"@atproto/api@^0.15.21":
|
||||
version "0.15.21"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.21.tgz#6cd450c49dc30ea7baca4905b9046abf69f9c1bd"
|
||||
integrity sha512-/VsikzVqIjNrdCk3eoJAleNcPUAGOLW8GCU9ymQMyGg1bBOCDb2Gl4eCqvhJ7Zd/UUyU5o8bh2YwLsY8/ikkeA==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.4.2"
|
||||
"@atproto/lexicon" "^0.4.11"
|
||||
@@ -77,14 +77,14 @@
|
||||
tlds "^1.234.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/aws@^0.2.22":
|
||||
version "0.2.22"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.22.tgz#14a664c06e3569945e4ab143d3a8a03400c7d1de"
|
||||
integrity sha512-xZ+0/zHHmpgzdLJGTDkFl5Wd39Wm5MyyMLdGYSzyt0wGTBmH6Ktp7ZgR8rmQVNYN1+VkMcdClAiNhg+BSH3mRw==
|
||||
"@atproto/aws@^0.2.24":
|
||||
version "0.2.24"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.24.tgz#c8e7a804710d70be3aa2fa292c1ece4c05127891"
|
||||
integrity sha512-4XZQGitPJR56tFt1bzPJKOqp3vTVcfVsEAFo9FGWp7Es+jj742aVgfWEe64O0VoZp3ZTiD7XhwsLJArz7NJTlQ==
|
||||
dependencies:
|
||||
"@atproto/common" "^0.4.11"
|
||||
"@atproto/crypto" "^0.4.4"
|
||||
"@atproto/repo" "^0.8.2"
|
||||
"@atproto/repo" "^0.8.4"
|
||||
"@aws-sdk/client-cloudfront" "^3.261.0"
|
||||
"@aws-sdk/client-kms" "^3.196.0"
|
||||
"@aws-sdk/client-s3" "^3.224.0"
|
||||
@@ -94,21 +94,21 @@
|
||||
multiformats "^9.9.0"
|
||||
uint8arrays "3.0.0"
|
||||
|
||||
"@atproto/bsky@^0.0.161":
|
||||
version "0.0.161"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.161.tgz#671280c1d40f5c4eb0cc31d338a9e950acbf0ce0"
|
||||
integrity sha512-L4uzadjt+oyVq3+W7rc1A+X2DyZDsTfeSD15w7k6+6JzICp32qavDuVjut3CIBqXCt7ykvSDujApyLsB/lcWJQ==
|
||||
"@atproto/bsky@^0.0.167":
|
||||
version "0.0.167"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.167.tgz#583eb404ef4de409e34d7c2485bf325e5d1f3ff0"
|
||||
integrity sha512-VLgaVsx0fYeoXcFHP1KM6joda9Ovhb7LsE3JdES6+hhsAF74DFwW57mVzRfYhy1bwWn/m9poUMs1RkCjOR9ZJA==
|
||||
dependencies:
|
||||
"@atproto-labs/fetch-node" "0.1.9"
|
||||
"@atproto-labs/xrpc-utils" "0.0.16"
|
||||
"@atproto/api" "^0.15.16"
|
||||
"@atproto/api" "^0.15.21"
|
||||
"@atproto/common" "^0.4.11"
|
||||
"@atproto/crypto" "^0.4.4"
|
||||
"@atproto/did" "^0.1.5"
|
||||
"@atproto/identity" "^0.4.8"
|
||||
"@atproto/lexicon" "^0.4.11"
|
||||
"@atproto/repo" "^0.8.2"
|
||||
"@atproto/sync" "^0.1.26"
|
||||
"@atproto/repo" "^0.8.4"
|
||||
"@atproto/sync" "^0.1.28"
|
||||
"@atproto/syntax" "^0.4.0"
|
||||
"@atproto/xrpc-server" "^0.8.0"
|
||||
"@bufbuild/protobuf" "^1.5.0"
|
||||
@@ -218,21 +218,21 @@
|
||||
"@noble/hashes" "^1.6.1"
|
||||
uint8arrays "3.0.0"
|
||||
|
||||
"@atproto/dev-env@^0.3.144":
|
||||
version "0.3.144"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.144.tgz#cd2949ff870ca4cde23b4c377b08740a2e64151f"
|
||||
integrity sha512-ND0oGp7itSnXxlAHlFxYjGFyCcu0f4eSucImVtKRxTcW8UeyyTtJcQP8OyNvtC8j13YjbW124r0g25Wlm0j9XQ==
|
||||
"@atproto/dev-env@^0.3.150":
|
||||
version "0.3.150"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.150.tgz#6443206352398be1e3dd8bcfe980e7a21d2cd93a"
|
||||
integrity sha512-LOujaEmOVBCxSnKQqpJb238fe5vYGIgmTA+OMEFH3kZb+6Y6UXfW2Vhs79tP0DiX0VyoXwib/7PH3Lp5cC/ZFQ==
|
||||
dependencies:
|
||||
"@atproto/api" "^0.15.16"
|
||||
"@atproto/bsky" "^0.0.161"
|
||||
"@atproto/api" "^0.15.21"
|
||||
"@atproto/bsky" "^0.0.167"
|
||||
"@atproto/bsync" "^0.0.20"
|
||||
"@atproto/common-web" "^0.4.2"
|
||||
"@atproto/crypto" "^0.4.4"
|
||||
"@atproto/identity" "^0.4.8"
|
||||
"@atproto/lexicon" "^0.4.11"
|
||||
"@atproto/ozone" "^0.1.121"
|
||||
"@atproto/pds" "^0.4.150"
|
||||
"@atproto/sync" "^0.1.26"
|
||||
"@atproto/ozone" "^0.1.126"
|
||||
"@atproto/pds" "^0.4.156"
|
||||
"@atproto/sync" "^0.1.28"
|
||||
"@atproto/syntax" "^0.4.0"
|
||||
"@atproto/xrpc-server" "^0.8.0"
|
||||
"@did-plc/lib" "^0.0.1"
|
||||
@@ -259,18 +259,18 @@
|
||||
"@atproto/common-web" "^0.4.2"
|
||||
"@atproto/crypto" "^0.4.4"
|
||||
|
||||
"@atproto/jwk-jose@0.1.8":
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/jwk-jose/-/jwk-jose-0.1.8.tgz#2dc8ad2cc900e7bc231add293f6518b06dc017ec"
|
||||
integrity sha512-aoU2Q0GpIl388KhCcv9YvAxNscALUv3xzLq5gjVPdJ+zmqw94nGZNcjiNvpnbfS+VQM9e2DrrTuwmDXnxfrrSA==
|
||||
"@atproto/jwk-jose@0.1.9":
|
||||
version "0.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/jwk-jose/-/jwk-jose-0.1.9.tgz#bd4a899ea2d497808300c40106795f5645c01f75"
|
||||
integrity sha512-HT9GcUe6htDxI5OSYXWdeS6QZ9lpuDDvJk508ppi8a48E/1f8eumoM0QhgbFRF9IKAnnFrtnZDOAvljQzFKwwQ==
|
||||
dependencies:
|
||||
"@atproto/jwk" "0.3.0"
|
||||
"@atproto/jwk" "0.4.0"
|
||||
jose "^5.2.0"
|
||||
|
||||
"@atproto/jwk@0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/jwk/-/jwk-0.3.0.tgz#275fa676f6b5988ddedf4ee0475dd285de9b831b"
|
||||
integrity sha512-MIAXyNMGu1tCNHjqW/8jqfE/wgWCIoK2cJ0mR6UxwhNPvkbe35TcpRYJdtQu/E6MUd7TziyDBa/GO4dKAiePhQ==
|
||||
"@atproto/jwk@0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/jwk/-/jwk-0.4.0.tgz#f32265be172492c38434c556a124b954f249cee8"
|
||||
integrity sha512-tvp4iZrzqEzKCeTOKz50/o6WdsZzOuWmWjF6On5QAp04fLwLpsFu2Hixgx/lA1KBO0O4sns7YSGcAqSSX6Rdog==
|
||||
dependencies:
|
||||
multiformats "^9.9.0"
|
||||
zod "^3.23.8"
|
||||
@@ -286,32 +286,32 @@
|
||||
multiformats "^9.9.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/oauth-provider-api@0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.1.4.tgz#a775182e3648dc693a04e3cb604eb62cd9ddfd8c"
|
||||
integrity sha512-3PRrf0gTAVMCETjtIH/3AaQaHBDbjsRBc/OYrlWBZ9IPplchBXtQGH/KcnjE4kK2Ef8p45qQSl3dNWg3EXsbHQ==
|
||||
"@atproto/oauth-provider-api@0.1.6":
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.1.6.tgz#769a70caaac9b5144f9f867518523d1568a6b47c"
|
||||
integrity sha512-4Q6ZCnTmmdiWiA+KMrfbZmqjxTSgMe+YE68+3RccwOCIgPt171TiDHGKIayep9n1RDnuucVQoqvVXOT4kmAsjw==
|
||||
dependencies:
|
||||
"@atproto/jwk" "0.3.0"
|
||||
"@atproto/oauth-types" "0.3.0"
|
||||
"@atproto/jwk" "0.4.0"
|
||||
"@atproto/oauth-types" "0.4.0"
|
||||
|
||||
"@atproto/oauth-provider-frontend@0.1.8":
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.1.8.tgz#21d944566c63f54524f239a10f7c65d150982f40"
|
||||
integrity sha512-uqfHv+n2xq7vTpuBP1Red7PhpaAbbJbwSbRsSfplJQ16XmF5NCMU8dHGCGRTEHngLZ9UquuIefN3w1QTrNzD0w==
|
||||
"@atproto/oauth-provider-frontend@0.1.10":
|
||||
version "0.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.1.10.tgz#d7176819d0ae1401ca5d70f7afec253621901a79"
|
||||
integrity sha512-bOFpi5OIxWv4Q9ci1+PAXEzIZaiu5inepC7pRFYqgqgLoCO0MWH/5Qkn/f6jMpDwPdtBqAiPg9tjE7E3le6NJA==
|
||||
optionalDependencies:
|
||||
"@atproto/oauth-provider-api" "0.1.4"
|
||||
"@atproto/oauth-provider-api" "0.1.6"
|
||||
|
||||
"@atproto/oauth-provider-ui@0.1.9":
|
||||
version "0.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.9.tgz#8c43a1affa94ecb537072e6d569b8a24cdd42e72"
|
||||
integrity sha512-a6/VAeQWRMxpgnqo/TuqXg3EW2tO68jLh8Mv1uyV1NiZbT7fNlgkII/djIl3fLoEa95I3p236NZxjhKELSBbGg==
|
||||
"@atproto/oauth-provider-ui@0.1.11":
|
||||
version "0.1.11"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.11.tgz#cb6194ac0b93f1d4b5d6717f80c55a3a20a8c690"
|
||||
integrity sha512-9fflyDt4Y3RDJIfbonxVeMbQtLLQrkQSDhWhPXp9xbZ/uYBddaAw+svBfFoMY7dxdlJbQeUPobsUctEm3qAILg==
|
||||
optionalDependencies:
|
||||
"@atproto/oauth-provider-api" "0.1.4"
|
||||
"@atproto/oauth-provider-api" "0.1.6"
|
||||
|
||||
"@atproto/oauth-provider@^0.9.1":
|
||||
version "0.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.9.1.tgz#0147b75d1ad444455159f0a687ce87b3b49a2894"
|
||||
integrity sha512-2Gm3jv45cGLmUQV0C4/orCJBsHu4wajy+JTN9f/ATX3vvjnFtAd/1GRvAMKDGXtdF7VIjNFlD+4lqhoDxYJpng==
|
||||
"@atproto/oauth-provider@^0.9.3":
|
||||
version "0.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.9.3.tgz#047b2e520e5cf127385adddc1dca47207b0ca113"
|
||||
integrity sha512-TAhsCYDB/1twEA1vqjLAz7lxKI8W59eNs239MujE35Cc9l4lRHyMopoFv5JmgNnxDvloB5l6RxpTbXVC6wnKpQ==
|
||||
dependencies:
|
||||
"@atproto-labs/fetch" "0.2.3"
|
||||
"@atproto-labs/fetch-node" "0.1.9"
|
||||
@@ -320,12 +320,12 @@
|
||||
"@atproto-labs/simple-store-memory" "0.1.3"
|
||||
"@atproto/common" "^0.4.11"
|
||||
"@atproto/did" "0.1.5"
|
||||
"@atproto/jwk" "0.3.0"
|
||||
"@atproto/jwk-jose" "0.1.8"
|
||||
"@atproto/oauth-provider-api" "0.1.4"
|
||||
"@atproto/oauth-provider-frontend" "0.1.8"
|
||||
"@atproto/oauth-provider-ui" "0.1.9"
|
||||
"@atproto/oauth-types" "0.3.0"
|
||||
"@atproto/jwk" "0.4.0"
|
||||
"@atproto/jwk-jose" "0.1.9"
|
||||
"@atproto/oauth-provider-api" "0.1.6"
|
||||
"@atproto/oauth-provider-frontend" "0.1.10"
|
||||
"@atproto/oauth-provider-ui" "0.1.11"
|
||||
"@atproto/oauth-types" "0.4.0"
|
||||
"@atproto/syntax" "0.4.0"
|
||||
"@hapi/accept" "^6.0.3"
|
||||
"@hapi/address" "^5.1.1"
|
||||
@@ -339,20 +339,20 @@
|
||||
jose "^5.2.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/oauth-types@0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.3.0.tgz#8d49d939486ac281bc13d0b1fe4462b7e519fdf0"
|
||||
integrity sha512-ptfsJARKODXfuOoDQag4a6PpEkDEj4Urz3jOmnQZy2YspPc/TNm1o0HglU0YehELv1vfhh9gEz40BJztPPhiLA==
|
||||
"@atproto/oauth-types@0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.4.0.tgz#fb110717dd1e8593adffc6eaa85e7ab4f0713740"
|
||||
integrity sha512-FrRH9JsPw9H4JxfPDrbrI+pB102tbHTygajfHay7xwz78HPOjSbWPRgWW2hYS4w8vDYdB3PYbBj1jPoKetW7LA==
|
||||
dependencies:
|
||||
"@atproto/jwk" "0.3.0"
|
||||
"@atproto/jwk" "0.4.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/ozone@^0.1.121":
|
||||
version "0.1.121"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.121.tgz#309b7e876f3b598ed4e79bb5a79e2346931588fe"
|
||||
integrity sha512-kc3NxiXSPqQmWz8yXlV5cFnZ469ViQd0AexEMw467AcB8ikK1WSxhLsa1EiNAQuLOOpyeXSmAKGAUFHzSOIMpw==
|
||||
"@atproto/ozone@^0.1.126":
|
||||
version "0.1.126"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.126.tgz#a4502121b9732a494a8b25a04be89b7eb0a4e2dd"
|
||||
integrity sha512-h1yP1NArjjHlOam9wamGIUSrG9tGynkZ0+Y6t21u7dwrg1o/TRpXSXemCYZhtz3zqdd4Yu5VyavoWPtEFdr+rQ==
|
||||
dependencies:
|
||||
"@atproto/api" "^0.15.16"
|
||||
"@atproto/api" "^0.15.21"
|
||||
"@atproto/common" "^0.4.11"
|
||||
"@atproto/crypto" "^0.4.4"
|
||||
"@atproto/identity" "^0.4.8"
|
||||
@@ -377,21 +377,21 @@
|
||||
undici "^6.14.1"
|
||||
ws "^8.12.0"
|
||||
|
||||
"@atproto/pds@^0.4.150":
|
||||
version "0.4.150"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.150.tgz#45686b05b8ed46e265efa5231ab16e6eda72a8e8"
|
||||
integrity sha512-CPT6H2uDTe4ZAyxQbws2dIlmdFFf6GQGwMc0OE3kI1wBBaLHprpexjM2Gd4ObtYNxGOOV0fwoCDAth8qqZ4XVw==
|
||||
"@atproto/pds@^0.4.156":
|
||||
version "0.4.156"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.156.tgz#1815ced4ab8b51cf9fe9a5712cd136a0b1d82392"
|
||||
integrity sha512-/8j/ihTLRhCI1sxkEvs2kuX4ehPKvsnwDxhmhdVvYqbKrjmGRTsDIZDV1K7dVFcYdCypOEPXsgTReh2lVhcC8w==
|
||||
dependencies:
|
||||
"@atproto-labs/fetch-node" "0.1.9"
|
||||
"@atproto-labs/xrpc-utils" "0.0.16"
|
||||
"@atproto/api" "^0.15.16"
|
||||
"@atproto/aws" "^0.2.22"
|
||||
"@atproto/api" "^0.15.21"
|
||||
"@atproto/aws" "^0.2.24"
|
||||
"@atproto/common" "^0.4.11"
|
||||
"@atproto/crypto" "^0.4.4"
|
||||
"@atproto/identity" "^0.4.8"
|
||||
"@atproto/lexicon" "^0.4.11"
|
||||
"@atproto/oauth-provider" "^0.9.1"
|
||||
"@atproto/repo" "^0.8.2"
|
||||
"@atproto/oauth-provider" "^0.9.3"
|
||||
"@atproto/repo" "^0.8.4"
|
||||
"@atproto/syntax" "^0.4.0"
|
||||
"@atproto/xrpc" "^0.7.0"
|
||||
"@atproto/xrpc-server" "^0.8.0"
|
||||
@@ -424,10 +424,10 @@
|
||||
undici "^6.19.8"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/repo@^0.8.2":
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.8.2.tgz#7953cb2c637c94505da76f74a784b2aae050c204"
|
||||
integrity sha512-lP0g5Uw3TUC2Tc7te8YKCpRoIhBYI+Uvn11fupGEaMcMjgLdYtB0Kc0AiqWXF42KqlBG9dAEoJITi2GRzDNHUg==
|
||||
"@atproto/repo@^0.8.4":
|
||||
version "0.8.4"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.8.4.tgz#f6a1b4bce8cf86cd1825069f9cd2916a5f86e774"
|
||||
integrity sha512-WgyARo6UcOnhbRsRVuNjXOH5MPTTHVDsaIavPeQl5erq5foE/pQKC7B7FLTJmhpC6GPZHJ5M2doAyXRXv5UHGA==
|
||||
dependencies:
|
||||
"@atproto/common" "^0.4.11"
|
||||
"@atproto/common-web" "^0.4.2"
|
||||
@@ -439,15 +439,15 @@
|
||||
varint "^6.0.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/sync@^0.1.26":
|
||||
version "0.1.26"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.26.tgz#6be2876be612d9cd704452598ee679b2e912cfe3"
|
||||
integrity sha512-bpUIajtPrE3RgFW8mIfrI4EM/LJ4JjQhI5fsqc78zCHZawuflpllf1aH70roDWWiskMWoiLWnVRxdYXdeEgbXA==
|
||||
"@atproto/sync@^0.1.28":
|
||||
version "0.1.28"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.28.tgz#7c5c469dd899b4be86e5d993af66646c71d63eaf"
|
||||
integrity sha512-faCsOwcYQHxHmNWRPykV0hTccXaG15XoUMZozfmoFOKFSliTgDETTovSAVe05mNSBUvMWUGl8fdEwHRzq1Q8sA==
|
||||
dependencies:
|
||||
"@atproto/common" "^0.4.11"
|
||||
"@atproto/identity" "^0.4.8"
|
||||
"@atproto/lexicon" "^0.4.11"
|
||||
"@atproto/repo" "^0.8.2"
|
||||
"@atproto/repo" "^0.8.4"
|
||||
"@atproto/syntax" "^0.4.0"
|
||||
"@atproto/xrpc-server" "^0.8.0"
|
||||
multiformats "^9.9.0"
|
||||
|
||||
Reference in New Issue
Block a user