From bc072570d27e1f397406daea355570f5aec95647 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 2 Jul 2025 00:36:04 +0300 Subject: [PATCH] Activity notification settings (#8485) Co-authored-by: Eric Bailey Co-authored-by: Samuel Newman Co-authored-by: hailey --- .../bellPlus_stroke2_corner0_rounded.svg | 1 + .../bellRinging_filled_corner0_rounded.svg | 1 + .../activity_notifications_announcement.webp | Bin 0 -> 22058 bytes bskyweb/cmd/bskyweb/server.go | 2 + .../BackgroundNotificationHandler.kt | 4 +- package.json | 4 +- src/Navigation.tsx | 24 ++ src/components/ProfileCard.tsx | 73 ++++- src/components/Tooltip/index.tsx | 94 +++--- src/components/Tooltip/index.web.tsx | 10 +- .../SubscribeProfileButton.tsx | 89 +++++ .../SubscribeProfileDialog.tsx | 309 ++++++++++++++++++ .../dialogs/nuxs/ActivitySubscriptions.tsx | 177 ++++++++++ src/components/dialogs/nuxs/index.tsx | 19 +- src/components/dialogs/nuxs/utils.ts | 15 + src/components/icons/BellPlus.tsx | 5 + src/components/icons/BellRinging.tsx | 4 + src/lib/api/feed/list.ts | 12 +- src/lib/api/feed/posts.ts | 52 +++ src/lib/hooks/useNotificationHandler.ts | 44 ++- .../create-sanitized-display-name.ts | 7 +- src/lib/routes/types.ts | 2 + src/lib/statsig/gates.ts | 1 - src/logger/metrics.ts | 13 + src/routes.ts | 2 + src/screens/Notifications/ActivityList.tsx | 44 +++ .../Profile/Header/ProfileHeaderStandard.tsx | 27 +- .../Settings/AccessibilitySettings.tsx | 19 +- .../Settings/ActivityPrivacySettings.tsx | 140 ++++++++ src/screens/Settings/AppPasswords.tsx | 6 +- src/screens/Settings/AppearanceSettings.tsx | 9 +- .../Settings/ExternalMediaPreferences.tsx | 7 +- .../Settings/FollowingFeedPreferences.tsx | 5 +- .../ActivityNotificationSettings.tsx | 263 +++++++++++++++ .../components/PreferenceControls.tsx | 30 +- .../Settings/NotificationSettings/index.tsx | 9 +- .../Settings/PrivacyAndSecuritySettings.tsx | 50 +++ src/screens/Settings/Settings.tsx | 39 ++- .../Settings/components/SettingsList.tsx | 23 +- src/state/cache/profile-shadow.ts | 13 +- src/state/queries/activity-subscriptions.ts | 130 ++++++++ src/state/queries/list-members.ts | 33 +- .../queries/messages/actor-declaration.ts | 4 +- src/state/queries/notifications/feed.ts | 14 +- src/state/queries/notifications/types.ts | 1 + src/state/queries/notifications/util.ts | 14 +- src/state/queries/nuxs/definitions.ts | 6 + src/state/queries/post-feed.ts | 6 + .../hooks/activity-subscriptions-nudged.ts | 8 + src/storage/schema.ts | 1 + .../notifications/NotificationFeedItem.tsx | 56 +++- src/view/com/util/PostMeta.tsx | 3 +- yarn.lock | 174 +++++----- 53 files changed, 1810 insertions(+), 288 deletions(-) create mode 100644 assets/icons/bellPlus_stroke2_corner0_rounded.svg create mode 100644 assets/icons/bellRinging_filled_corner0_rounded.svg create mode 100644 assets/images/activity_notifications_announcement.webp create mode 100644 src/components/activity-notifications/SubscribeProfileButton.tsx create mode 100644 src/components/activity-notifications/SubscribeProfileDialog.tsx create mode 100644 src/components/dialogs/nuxs/ActivitySubscriptions.tsx create mode 100644 src/components/icons/BellPlus.tsx create mode 100644 src/lib/api/feed/posts.ts create mode 100644 src/screens/Notifications/ActivityList.tsx create mode 100644 src/screens/Settings/ActivityPrivacySettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx create mode 100644 src/state/queries/activity-subscriptions.ts create mode 100644 src/storage/hooks/activity-subscriptions-nudged.ts diff --git a/assets/icons/bellPlus_stroke2_corner0_rounded.svg b/assets/icons/bellPlus_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..7e3349427a --- /dev/null +++ b/assets/icons/bellPlus_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/bellRinging_filled_corner0_rounded.svg b/assets/icons/bellRinging_filled_corner0_rounded.svg new file mode 100644 index 0000000000..67be5d99d3 --- /dev/null +++ b/assets/icons/bellRinging_filled_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/images/activity_notifications_announcement.webp b/assets/images/activity_notifications_announcement.webp new file mode 100644 index 0000000000000000000000000000000000000000..885d1b7d1fee2f896f34d70a3b419e496ccc8c39 GIT binary patch literal 22058 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 literal 0 HcmV?d00001 diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index ef796920d6..755f2fa3f6 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -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) diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt index 9fdfcfd89d..4f8a6b892a 100644 --- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt @@ -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"] } } diff --git a/package.json b/package.json index 7c1c3dee92..aae35f96ab 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index f1a9c569d0..26a2b2a2ae 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -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, }} /> + ActivityPrivacySettingsScreen} + options={{ + title: title(msg`Privacy and Security`), + requireAuth: true, + }} + /> NotificationSettingsScreen} @@ -459,6 +470,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { requireAuth: true, }} /> + ActivityNotificationSettingsScreen} + options={{ + title: title(msg`Activity notifications`), + requireAuth: true, + }} + /> MiscellaneousNotificationSettingsScreen} @@ -524,6 +543,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { getComponent={() => MessagesInboxScreen} options={{title: title(msg`Chat request inbox`), requireAuth: true}} /> + NotificationsActivityListScreen} + options={{title: title(msg`Notifications`), requireAuth: true}} + /> LegacyNotificationSettingsScreen} diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index 30b26beadf..4aec74880a 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -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 ( + + ) + } else { + return ( + + + + + ) + } +} + +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 ( - - - + + + {forceLTR(name)} + + {verification.showBadge && ( + + + + )} + + {NON_BREAKING_SPACE + handle} + ) } diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx index 446cf18fc6..fbdb969db6 100644 --- a/src/components/Tooltip/index.tsx +++ b/src/components/Tooltip/index.tsx @@ -3,6 +3,7 @@ import { createContext, useCallback, useContext, + useEffect, useMemo, useRef, useState, @@ -30,31 +31,33 @@ const BUBBLE_SHADOW_OFFSET = ARROW_SIZE / 3 // vibes-based, provide more shadow type TooltipContextType = { position: 'top' | 'bottom' - ready: boolean + visible: boolean onVisibleChange: (visible: boolean) => void } +type TargetMeasurements = { + x: number + y: number + width: number + height: number +} + type TargetContextType = { - targetMeasurements: - | { - x: number - y: number - width: number - height: number - } - | undefined - targetRef: React.RefObject + targetMeasurements: TargetMeasurements | undefined + setTargetMeasurements: (measurements: TargetMeasurements) => void + shouldMeasure: boolean } const TooltipContext = createContext({ position: 'bottom', - ready: false, + visible: false, onVisibleChange: () => {}, }) const TargetContext = createContext({ targetMeasurements: undefined, - targetRef: {current: null}, + setTargetMeasurements: () => {}, + shouldMeasure: false, }) export function Outer({ @@ -68,21 +71,12 @@ export function Outer({ visible: boolean onVisibleChange: (visible: boolean) => void }) { - /** - * Whether we have measured the target and are ready to show the tooltip. - */ - const [ready, setReady] = useState(false) /** * Lagging state to track the externally-controlled visibility of the - * tooltip. + * tooltip, which needs to wait for the target to be measured before + * actually being shown. */ - const [prevRequestVisible, setPrevRequestVisible] = useState< - boolean | undefined - >() - /** - * Needs to reference the element this Tooltip is attached to. - */ - const targetRef = useRef(null) + const [visible, setVisible] = useState(false) const [targetMeasurements, setTargetMeasurements] = useState< | { x: number @@ -93,33 +87,24 @@ export function Outer({ | undefined >(undefined) - if (requestVisible && !prevRequestVisible) { - setPrevRequestVisible(true) - - if (targetRef.current) { - /* - * Once opened, measure the dimensions and position of the target - */ - targetRef.current.measure((_x, _y, width, height, pageX, pageY) => { - if (pageX !== undefined && pageY !== undefined && width && height) { - setTargetMeasurements({x: pageX, y: pageY, width, height}) - setReady(true) - } - }) - } - } else if (!requestVisible && prevRequestVisible) { - setPrevRequestVisible(false) + if (requestVisible && !visible && targetMeasurements) { + setVisible(true) + } else if (!requestVisible && visible) { + setVisible(false) setTargetMeasurements(undefined) - setReady(false) } const ctx = useMemo( - () => ({position, ready, onVisibleChange}), - [position, ready, onVisibleChange], + () => ({position, visible, onVisibleChange}), + [position, visible, onVisibleChange], ) const targetCtx = useMemo( - () => ({targetMeasurements, targetRef}), - [targetMeasurements, targetRef], + () => ({ + targetMeasurements, + setTargetMeasurements, + shouldMeasure: requestVisible, + }), + [requestVisible, targetMeasurements, setTargetMeasurements], ) return ( @@ -132,7 +117,20 @@ export function Outer({ } export function Target({children}: {children: React.ReactNode}) { - const {targetRef} = useContext(TargetContext) + const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext) + const targetRef = useRef(null) + + useEffect(() => { + if (!shouldMeasure) return + /* + * Once opened, measure the dimensions and position of the target + */ + targetRef.current?.measure((_x, _y, width, height, pageX, pageY) => { + if (pageX !== undefined && pageY !== undefined && width && height) { + setTargetMeasurements({x: pageX, y: pageY, width, height}) + } + }) + }, [shouldMeasure, setTargetMeasurements]) return ( @@ -148,13 +146,13 @@ export function Content({ children: React.ReactNode label: string }) { - const {position, ready, onVisibleChange} = useContext(TooltipContext) + const {position, visible, onVisibleChange} = useContext(TooltipContext) const {targetMeasurements} = useContext(TargetContext) const requestClose = useCallback(() => { onVisibleChange(false) }, [onVisibleChange]) - if (!ready || !targetMeasurements) return null + if (!visible || !targetMeasurements) return null return ( diff --git a/src/components/Tooltip/index.web.tsx b/src/components/Tooltip/index.web.tsx index 739a714cd4..fc5808d7ad 100644 --- a/src/components/Tooltip/index.web.tsx +++ b/src/components/Tooltip/index.web.tsx @@ -13,10 +13,12 @@ import {Text} from '#/components/Typography' type TooltipContextType = { position: 'top' | 'bottom' + onVisibleChange: (open: boolean) => void } const TooltipContext = createContext({ position: 'bottom', + onVisibleChange: () => {}, }) export function Outer({ @@ -30,7 +32,10 @@ export function Outer({ visible: boolean onVisibleChange: (visible: boolean) => void }) { - const ctx = useMemo(() => ({position}), [position]) + const ctx = useMemo( + () => ({position, onVisibleChange}), + [position, onVisibleChange], + ) return ( {children} @@ -54,7 +59,7 @@ export function Content({ label: string }) { const t = useTheme() - const {position} = useContext(TooltipContext) + const {position, onVisibleChange} = useContext(TooltipContext) return ( onVisibleChange(false)} style={flatten([ a.rounded_sm, select(t.name, { diff --git a/src/components/activity-notifications/SubscribeProfileButton.tsx b/src/components/activity-notifications/SubscribeProfileButton.tsx new file mode 100644 index 0000000000..71253dca9b --- /dev/null +++ b/src/components/activity-notifications/SubscribeProfileButton.tsx @@ -0,0 +1,89 @@ +import {useCallback} from 'react' +import {type ModerationOpts} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' +import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' +import {Button, ButtonIcon} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {BellPlus_Stroke2_Corner0_Rounded as BellPlusIcon} from '#/components/icons/BellPlus' +import {BellRinging_Filled_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging' +import * as Tooltip from '#/components/Tooltip' +import {Text} from '#/components/Typography' +import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' +import type * as bsky from '#/types/bsky' +import {SubscribeProfileDialog} from './SubscribeProfileDialog' + +export function SubscribeProfileButton({ + profile, + moderationOpts, +}: { + profile: bsky.profile.AnyProfileView + moderationOpts: ModerationOpts +}) { + const {_} = useLingui() + const requireEmailVerification = useRequireEmailVerification() + const subscribeDialogControl = useDialogControl() + const [activitySubscriptionsNudged, setActivitySubscriptionsNudged] = + useActivitySubscriptionsNudged() + + const onDismissTooltip = () => { + setActivitySubscriptionsNudged(true) + } + + const onPress = useCallback(() => { + subscribeDialogControl.open() + }, [subscribeDialogControl]) + + const name = createSanitizedDisplayName(profile, true) + + const wrappedOnPress = requireEmailVerification(onPress, { + instructions: [ + + Before you can get notifications for {name}'s posts, you must first + verify your email. + , + ], + }) + + const isSubscribed = + profile.viewer?.activitySubscription?.post || + profile.viewer?.activitySubscription?.reply + + const Icon = isSubscribed ? BellRingingIcon : BellPlusIcon + + return ( + <> + + + + + + + Get notified about new posts + + + + + + + ) +} diff --git a/src/components/activity-notifications/SubscribeProfileDialog.tsx b/src/components/activity-notifications/SubscribeProfileDialog.tsx new file mode 100644 index 0000000000..d1ab2842d0 --- /dev/null +++ b/src/components/activity-notifications/SubscribeProfileDialog.tsx @@ -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 ( + + + + + ) +} + +function DialogInner({ + profile, + moderationOpts, + includeProfile, +}: { + profile: bsky.profile.AnyProfileView + moderationOpts: ModerationOpts + includeProfile?: boolean +}) { + const {_} = useLingui() + const t = useTheme() + const agent = useAgent() + const control = Dialog.useDialogContext() + const queryClient = useQueryClient() + const initialState = parseActivitySubscription( + profile.viewer?.activitySubscription, + ) + const [state, setState] = useState(initialState) + + const values = useMemo(() => { + const {post, reply} = state + const res = [] + if (post) res.push('post') + if (reply) res.push('reply') + return res + }, [state]) + + const onChange = (newValues: string[]) => { + setState(oldValues => { + // ensure you can't have reply without post + if (!oldValues.reply && newValues.includes('reply')) { + return { + post: true, + reply: true, + } + } + + if (oldValues.post && !newValues.includes('post')) { + return { + post: false, + reply: false, + } + } + + return { + post: newValues.includes('post'), + reply: newValues.includes('reply'), + } + }) + } + + const { + mutate: saveChanges, + isPending: isSaving, + error, + } = useMutation({ + mutationFn: async ( + activitySubscription: Un$Typed, + ) => { + await agent.app.bsky.notification.putActivitySubscription({ + subject: profile.did, + activitySubscription, + }) + }, + onSuccess: (_data, activitySubscription) => { + control.close(() => { + updateProfileShadow(queryClient, profile.did, { + activitySubscription, + }) + + if (!activitySubscription.post && !activitySubscription.reply) { + logger.metric('activitySubscription:disable', {}) + Toast.show( + _( + msg`You will no longer receive notifications for ${sanitizeHandle(profile.handle, '@')}`, + ), + 'check', + ) + + // filter out the subscription + queryClient.setQueryData( + RQKEY_getActivitySubscriptions, + ( + old?: InfiniteData, + ) => { + 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 = useMemo(() => { + const isDirty = + state.post !== initialState.post || state.reply !== initialState.reply + const hasAny = state.post || state.reply + + if (isDirty) { + return { + label: _(msg`Save changes`), + color: hasAny ? 'primary' : 'negative', + onPress: () => saveChanges(state), + disabled: isSaving, + } + } else { + // on web, a disabled save button feels more natural than a massive close button + if (isWeb) { + return { + label: _(msg`Save changes`), + color: 'secondary', + disabled: true, + } + } else { + return { + label: _(msg`Cancel`), + color: 'secondary', + onPress: () => control.close(), + } + } + } + }, [state, initialState, control, _, isSaving, saveChanges]) + + const name = createSanitizedDisplayName(profile, false) + + return ( + + + + + Keep me posted + + + Get notified of this account’s activity + + + + {includeProfile && ( + + + + + )} + + + + + + Posts + + + + + + Replies + + + + + + + {error && ( + + Could not save changes: {cleanError(error)} + + )} + + + + + + + ) +} + +function parseActivitySubscription( + sub?: AppBskyNotificationDefs.ActivitySubscription, +): Un$Typed { + if (!sub) return {post: false, reply: false} + const {post, reply} = sub + return {post, reply} +} diff --git a/src/components/dialogs/nuxs/ActivitySubscriptions.tsx b/src/components/dialogs/nuxs/ActivitySubscriptions.tsx new file mode 100644 index 0000000000..b9f3979ed4 --- /dev/null +++ b/src/components/dialogs/nuxs/ActivitySubscriptions.tsx @@ -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 ( + + + + + + + + + New Feature + + + + + + + {_( + + + + + + + Get notified when someone posts + + + + 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. + + + + + {!isWeb && ( + + )} + + + + + + ) +} diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index 11377e1ded..8096a0141f 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -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 ( {/*For example, activeNux === Nux.NeueTypography && */} - {activeNux === Nux.InitialVerificationAnnouncement && ( - - )} + {activeNux === Nux.ActivitySubscriptions && } ) } diff --git a/src/components/dialogs/nuxs/utils.ts b/src/components/dialogs/nuxs/utils.ts index 0cc5104840..ba8f0169d6 100644 --- a/src/components/dialogs/nuxs/utils.ts +++ b/src/components/dialogs/nuxs/utils.ts @@ -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 +} diff --git a/src/components/icons/BellPlus.tsx b/src/components/icons/BellPlus.tsx new file mode 100644 index 0000000000..cd29de1979 --- /dev/null +++ b/src/components/icons/BellPlus.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const BellPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a1 1 0 0 1 0 2 5.85 5.85 0 0 0-5.802 5.08L5.143 17h13.715l-.382-2.868-.01-.102a1 1 0 0 1 1.973-.262l.02.1.532 4a1 1 0 0 1-.99 1.132h-3.357c-.905 1.747-2.606 3-4.644 3s-3.74-1.253-4.643-3H4a1 1 0 0 1-.991-1.132l1.207-9.053A7.85 7.85 0 0 1 12 2ZM9.78 19c.61.637 1.397 1 2.22 1s1.611-.363 2.22-1H9.78ZM17 2.5a1 1 0 0 1 1 1V6h2.5a1 1 0 0 1 0 2H18v2.5a1 1 0 0 1-2 0V8h-2.5a1 1 0 1 1 0-2H16V3.5a1 1 0 0 1 1-1Z', +}) diff --git a/src/components/icons/BellRinging.tsx b/src/components/icons/BellRinging.tsx index b174fcedc2..11981a7a38 100644 --- a/src/components/icons/BellRinging.tsx +++ b/src/components/icons/BellRinging.tsx @@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE' export const BellRinging_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M12 2a7.854 7.854 0 0 1 7.785 6.815l1.055 7.92.018.224a2 2 0 0 1-2 2.041h-2.215c-.904 1.747-2.605 3-4.643 3s-3.739-1.253-4.643-3H5.142a2 2 0 0 1-1.982-2.265l1.056-7.92.057-.363A7.854 7.854 0 0 1 12 2ZM9.78 19c.609.637 1.398 1 2.22 1s1.611-.363 2.22-1H9.78ZM12 4a5.854 5.854 0 0 0-5.76 4.81l-.041.27L5.142 17h13.716l-1.056-7.92A5.854 5.854 0 0 0 12 4ZM2.718 7.464a1 1 0 1 1-1.953-.427l1.953.427Zm20.518-.427a1 1 0 0 1-1.954.427l1.954-.427ZM3.193 2.105a1 1 0 0 1 1.531 1.287 9.47 9.47 0 0 0-2.006 4.072L.765 7.037a11.46 11.46 0 0 1 2.428-4.932Zm16.205-.123a1 1 0 0 1 1.34.047l.069.076.217.265a11.46 11.46 0 0 1 2.212 4.667l-.978.213-.976.214a9.46 9.46 0 0 0-1.826-3.853l-.18-.22-.062-.081a1 1 0 0 1 .184-1.328Z', }) + +export const BellRinging_Filled_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a7.854 7.854 0 0 1 7.784 6.815l1.207 9.053a1 1 0 0 1-.99 1.132h-3.354c-.904 1.748-2.608 3-4.647 3-2.038 0-3.742-1.252-4.646-3H4a1.002 1.002 0 0 1-.991-1.132l1.207-9.053A7.85 7.85 0 0 1 12 2ZM9.78 19c.608.637 1.398 1 2.221 1s1.613-.363 2.222-1H9.779ZM3.193 2.104a1 1 0 0 1 1.53 1.288A9.47 9.47 0 0 0 2.72 7.464a1 1 0 0 1-1.954-.427 11.46 11.46 0 0 1 2.428-4.933Zm16.205-.122a1 1 0 0 1 1.409.122 11.47 11.47 0 0 1 2.429 4.933 1 1 0 0 1-1.954.427 9.47 9.47 0 0 0-2.006-4.072 1 1 0 0 1 .122-1.41Z', +}) diff --git a/src/lib/api/feed/list.ts b/src/lib/api/feed/list.ts index 9744e3d4ce..9697b0aaf3 100644 --- a/src/lib/api/feed/list.ts +++ b/src/lib/api/feed/list.ts @@ -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 diff --git a/src/lib/api/feed/posts.ts b/src/lib/api/feed/posts.ts new file mode 100644 index 0000000000..33eff50997 --- /dev/null +++ b/src/lib/api/feed/posts.ts @@ -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 { + if (this.peek) return this.peek + throw new Error('Has not fetched yet') + } + + async fetch({}: {}): Promise { + 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: [], + } + } +} diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts index 311f38a793..6c3e7deb82 100644 --- a/src/lib/hooks/useNotificationHandler.ts +++ b/src/lib/hooks/useNotificationHandler.ts @@ -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 diff --git a/src/lib/moderation/create-sanitized-display-name.ts b/src/lib/moderation/create-sanitized-display-name.ts index 4f9584f918..4c62a5c03c 100644 --- a/src/lib/moderation/create-sanitized-display-name.ts +++ b/src/lib/moderation/create-sanitized-display-name.ts @@ -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 !== '') { diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index c92be34c23..b1db5caa64 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -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} diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index fca3f609af..3b1106480d 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -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' diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 31af1be2b0..d18e69122d 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -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 + } } diff --git a/src/routes.ts b/src/routes.ts index b66a0ae53f..7fc673e2be 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -11,6 +11,7 @@ export const router = new Router({ 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({ 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', diff --git a/src/screens/Notifications/ActivityList.tsx b/src/screens/Notifications/ActivityList.tsx new file mode 100644 index 0000000000..f87e340082 --- /dev/null +++ b/src/screens/Notifications/ActivityList.tsx @@ -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 ( + + + + + + Notifications + + + + + ( + + )} + renderEndOfFeed={() => } + /> + + ) +} diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index 1639abaf0c..5dbf32c57f 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -15,7 +15,6 @@ import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' import {isIOS} from '#/platform/detection' import {useProfileShadow} from '#/state/cache/profile-shadow' -import {type Shadow} from '#/state/cache/types' import { useProfileBlockMutationQueue, useProfileFollowMutationQueue, @@ -24,6 +23,7 @@ import {useRequireAuth, useSession} from '#/state/session' import {ProfileMenu} from '#/view/com/profile/ProfileMenu' import * as Toast from '#/view/com/util/Toast' import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' +import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {MessageProfileButton} from '#/components/dms/MessageProfileButton' @@ -58,8 +58,8 @@ let ProfileHeaderStandard = ({ }: Props): React.ReactNode => { const t = useTheme() const {gtMobile} = useBreakpoints() - const profile: Shadow = - useProfileShadow(profileUnshadowed) + const profile = + useProfileShadow(profileUnshadowed) const {currentAccount, hasSession} = useSession() const {_} = useLingui() const moderation = useMemo( @@ -134,13 +134,26 @@ let ProfileHeaderStandard = ({ } }, [_, queueUnblock]) - const isMe = React.useMemo( + const isMe = useMemo( () => currentAccount?.did === profile.did, [currentAccount, profile], ) const {isActive: live} = useActorStatus(profile) + const subscriptionsAllowed = useMemo(() => { + switch (profile.associated?.activitySubscription?.allowSubscriptions) { + case 'followers': + case undefined: + return !!profile.viewer?.following + case 'mutuals': + return !!profile.viewer?.following && !!profile.viewer.followedBy + case 'none': + default: + return false + } + }, [profile]) + return ( + {hasSession && subscriptionsAllowed && ( + + )} {hasSession && } + + + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx index 487827d66b..ce46541fdc 100644 --- a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx +++ b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx @@ -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 ( @@ -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], }), ]}> + style={[a.flex_row, a.py_xs, a.gap_sm]}> + style={[a.flex_row, a.py_xs, a.gap_sm]}> - {/* - Activity alerts} + titleText={Activity from others} subtitleText={ } showSkeleton={!settings} /> - */} + @@ -71,6 +80,24 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) { )} + + + Allow others to be notified of your posts + } + subtitleText={ + + } + showSkeleton={isPending} + /> + @@ -111,3 +138,26 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) { ) } + +function NotificationDeclaration({ + data, + isError, +}: { + data?: { + value: AppBskyNotificationDeclaration.Record + } + isError?: boolean +}) { + if (isError) { + return Error loading preference + } + switch (data?.value?.allowSubscriptions) { + case 'mutuals': + return Only followers who I follow + case 'none': + return No one + case 'followers': + default: + return Anyone who follows me + } +} diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index e1d1970708..aaba0b4b5c 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -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 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 ( @@ -183,16 +182,14 @@ export function SettingsScreen({}: Props) { Moderation - {gate('reengagement_features') && ( - - - - Notifications - - - )} + + + + Notifications + + @@ -364,6 +361,7 @@ function DevOptions() { const onboardingDispatch = useOnboardingDispatch() const navigation = useNavigation() 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() { Unsnooze email reminder + {actyNotifNudged && ( + + + Reset activity subscription nudge + + + )} clearAllStorage()} label={_(msg`Clear all storage data`)}> diff --git a/src/screens/Settings/components/SettingsList.tsx b/src/screens/Settings/components/SettingsList.tsx index 520df41187..6d17990478 100644 --- a/src/screens/Settings/components/SettingsList.tsx +++ b/src/screens/Settings/components/SettingsList.tsx @@ -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, 'position'> & { color?: string diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts index 0d3bb1b17d..1489e65fd9 100644 --- a/src/state/cache/profile-shadow.ts +++ b/src/state/cache/profile-shadow.ts @@ -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, ) { 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( 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) } diff --git a/src/state/queries/activity-subscriptions.ts b/src/state/queries/activity-subscriptions.ts new file mode 100644 index 0000000000..a81a672265 --- /dev/null +++ b/src/state/queries/activity-subscriptions.ts @@ -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 { + const queryDatas = queryClient.getQueriesData< + InfiniteData + >({ + 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 + } + } + } + } +} diff --git a/src/state/queries/list-members.ts b/src/state/queries/list-members.ts index 82c3955187..152c7a5be8 100644 --- a/src/state/queries/list-members.ts +++ b/src/state/queries/list-members.ts @@ -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 } } } diff --git a/src/state/queries/messages/actor-declaration.ts b/src/state/queries/messages/actor-declaration.ts index 34fb10935e..a5adb39d9f 100644 --- a/src/state/queries/messages/actor-declaration.ts +++ b/src/state/queries/messages/actor-declaration.ts @@ -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', diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index fce7802bc3..6010f11b40 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -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' diff --git a/src/state/queries/notifications/types.ts b/src/state/queries/notifications/types.ts index e05715f77d..a7b837086b 100644 --- a/src/state/queries/notifications/types.ts +++ b/src/state/queries/notifications/types.ts @@ -48,6 +48,7 @@ type OtherNotificationType = | 'unverified' | 'like-via-repost' | 'repost-via-repost' + | 'subscribed-post' | 'unknown' type FeedNotificationBase = { diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index 007f65cc73..faccd80872 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -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' || diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts index a44ffa4c54..1947f857f1 100644 --- a/src/state/queries/nuxs/definitions.ts +++ b/src/state/queries/nuxs/definitions.ts @@ -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 | undefined> = { [Nux.NeueTypography]: undefined, [Nux.ExploreInterestsCard]: undefined, [Nux.InitialVerificationAnnouncement]: undefined, + [Nux.ActivitySubscriptions]: undefined, } diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 920892924e..361081e67e 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -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 { diff --git a/src/storage/hooks/activity-subscriptions-nudged.ts b/src/storage/hooks/activity-subscriptions-nudged.ts new file mode 100644 index 0000000000..0e9c1c39cb --- /dev/null +++ b/src/storage/hooks/activity-subscriptions-nudged.ts @@ -0,0 +1,8 @@ +import {device, useStorage} from '#/storage' + +export function useActivitySubscriptionsNudged() { + const [activitySubscriptionsNudged = false, setActivitySubscriptionsNudged] = + useStorage(device, ['activitySubscriptionsNudged']) + + return [activitySubscriptionsNudged, setActivitySubscriptionsNudged] as const +} diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 7430532a96..19c31834bb 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -11,6 +11,7 @@ export type Device = { trendingBetaEnabled: boolean devMode: boolean demoMode: boolean + activitySubscriptionsNudged?: boolean } export type Account = { diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index 85f67919ac..89e2d20e76 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -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 = ({ {firstAuthorLink} reposted your repost ) icon = + } 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 ? ( + + New posts from {firstAuthorLink} and{' '} + + + {' '} + + ) : ( + + New from{' '} + {firstAuthorLink} + + ) + icon = } 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' ? ( diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 30ebbf2c21..c1f804203b 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -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( diff --git a/yarn.lock b/yarn.lock index 3280cbb9b7..90db949d23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"