From 7916c73b093e9faca1ecd5dfae7f81d9ae38120f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 23 Sep 2025 20:04:22 +0300 Subject: [PATCH 01/29] Better screen transitions for auth flow (#7803) --- .../Wizard => }/ScreenTransition.tsx | 15 +- src/screens/Login/ScreenTransition.tsx | 17 -- src/screens/Login/ScreenTransition.web.tsx | 1 - src/screens/Login/index.tsx | 66 ++++-- src/screens/Signup/StepCaptcha/index.tsx | 5 +- src/screens/Signup/StepHandle/index.tsx | 5 +- src/screens/Signup/StepInfo/index.tsx | 5 +- src/screens/Signup/index.tsx | 212 +++++++++--------- src/screens/Signup/state.ts | 6 +- .../StarterPack/Wizard/StepDetails.tsx | 4 +- src/screens/StarterPack/Wizard/StepFeeds.tsx | 7 +- .../StarterPack/Wizard/StepProfiles.tsx | 7 +- src/view/com/auth/SplashScreen.tsx | 131 ++++++----- 13 files changed, 265 insertions(+), 216 deletions(-) rename src/components/{StarterPack/Wizard => }/ScreenTransition.tsx (51%) delete mode 100644 src/screens/Login/ScreenTransition.tsx delete mode 100644 src/screens/Login/ScreenTransition.web.tsx diff --git a/src/components/StarterPack/Wizard/ScreenTransition.tsx b/src/components/ScreenTransition.tsx similarity index 51% rename from src/components/StarterPack/Wizard/ScreenTransition.tsx rename to src/components/ScreenTransition.tsx index c02888e1d1..8c4e7e01f5 100644 --- a/src/components/StarterPack/Wizard/ScreenTransition.tsx +++ b/src/components/ScreenTransition.tsx @@ -1,5 +1,6 @@ import {type StyleProp, type ViewStyle} from 'react-native' import Animated, { + Easing, FadeIn, FadeOut, SlideInLeft, @@ -13,17 +14,25 @@ export function ScreenTransition({ direction, style, children, + enabledWeb, }: { direction: 'Backward' | 'Forward' style?: StyleProp children: React.ReactNode + enabledWeb?: boolean }) { - const entering = direction === 'Forward' ? SlideInRight : SlideInLeft + const entering = + direction === 'Forward' + ? SlideInRight.easing(Easing.out(Easing.exp)) + : SlideInLeft.easing(Easing.out(Easing.exp)) + const webEntering = enabledWeb ? FadeIn.duration(90) : undefined + const exiting = FadeOut.duration(90) // Totally vibes based + const webExiting = enabledWeb ? FadeOut.duration(90) : undefined return ( {children} diff --git a/src/screens/Login/ScreenTransition.tsx b/src/screens/Login/ScreenTransition.tsx deleted file mode 100644 index b9e4b2d556..0000000000 --- a/src/screens/Login/ScreenTransition.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import {type StyleProp, type ViewStyle} from 'react-native' -import Animated, {FadeInRight, FadeOutLeft} from 'react-native-reanimated' -import type React from 'react' - -export function ScreenTransition({ - style, - children, -}: { - style?: StyleProp - children: React.ReactNode -}) { - return ( - - {children} - - ) -} diff --git a/src/screens/Login/ScreenTransition.web.tsx b/src/screens/Login/ScreenTransition.web.tsx deleted file mode 100644 index 4583720aa8..0000000000 --- a/src/screens/Login/ScreenTransition.web.tsx +++ /dev/null @@ -1 +0,0 @@ -export {Fragment as ScreenTransition} from 'react' diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 601d766061..9cbbd51216 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -1,6 +1,6 @@ -import React, {useRef} from 'react' +import {useEffect, useRef, useState} from 'react' import {KeyboardAvoidingView} from 'react-native' -import {LayoutAnimationConfig} from 'react-native-reanimated' +import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -15,9 +15,9 @@ import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm' import {LoginForm} from '#/screens/Login/LoginForm' import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm' import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm' -import {atoms as a} from '#/alf' +import {atoms as a, native} from '#/alf' +import {ScreenTransition} from '#/components/ScreenTransition' import {ChooseAccountForm} from './ChooseAccountForm' -import {ScreenTransition} from './ScreenTransition' enum Forms { Login, @@ -27,6 +27,14 @@ enum Forms { PasswordUpdated, } +const OrderedForms = [ + Forms.ChooseAccount, + Forms.Login, + Forms.ForgotPassword, + Forms.SetNewPassword, + Forms.PasswordUpdated, +] as const + export const Login = ({onPressBack}: {onPressBack: () => void}) => { const {_} = useLingui() const failedAttemptCountRef = useRef(0) @@ -38,20 +46,23 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { acc => acc.did === requestedAccountSwitchTo, ) - const [error, setError] = React.useState('') - const [serviceUrl, setServiceUrl] = React.useState( + const [error, setError] = useState('') + const [serviceUrl, setServiceUrl] = useState( requestedAccount?.service || DEFAULT_SERVICE, ) - const [initialHandle, setInitialHandle] = React.useState( + const [initialHandle, setInitialHandle] = useState( requestedAccount?.handle || '', ) - const [currentForm, setCurrentForm] = React.useState( + const [currentForm, setCurrentForm] = useState( requestedAccount ? Forms.Login : accounts.length ? Forms.ChooseAccount : Forms.Login, ) + const [screenTransitionDirection, setScreenTransitionDirection] = useState< + 'Forward' | 'Backward' + >('Forward') const { data: serviceDescription, @@ -64,15 +75,18 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { setServiceUrl(account.service) } setInitialHandle(account?.handle || '') - setCurrentForm(Forms.Login) + gotoForm(Forms.Login) } const gotoForm = (form: Forms) => { setError('') + const index = OrderedForms.indexOf(currentForm) + const nextIndex = OrderedForms.indexOf(form) + setScreenTransitionDirection(index < nextIndex ? 'Forward' : 'Backward') setCurrentForm(form) } - React.useEffect(() => { + useEffect(() => { if (serviceError) { setError( _( @@ -89,12 +103,13 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { }, [serviceError, serviceUrl, _]) const onPressForgotPassword = () => { - setCurrentForm(Forms.ForgotPassword) + gotoForm(Forms.ForgotPassword) logEvent('signin:forgotPasswordPressed', {}) } const handlePressBack = () => { onPressBack() + setScreenTransitionDirection('Backward') logEvent('signin:backPressed', { failedAttemptsCount: failedAttemptCountRef.current, }) @@ -106,7 +121,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { timeTakenSeconds: Math.round((Date.now() - startTimeRef.current) / 1000), failedAttemptsCount: failedAttemptCountRef.current, }) - setCurrentForm(Forms.Login) } const onAttemptFailed = () => { @@ -187,16 +201,22 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { } return ( - - - - {content} - - - + + + + + + {content} + + + + + ) } diff --git a/src/screens/Signup/StepCaptcha/index.tsx b/src/screens/Signup/StepCaptcha/index.tsx index 8ea893c4aa..e1d7afd01b 100644 --- a/src/screens/Signup/StepCaptcha/index.tsx +++ b/src/screens/Signup/StepCaptcha/index.tsx @@ -8,7 +8,6 @@ import {nanoid} from 'nanoid/non-secure' import {createFullHandle} from '#/lib/strings/handles' import {logger} from '#/logger' import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' -import {ScreenTransition} from '#/screens/Login/ScreenTransition' import {useSignupContext} from '#/screens/Signup/state' import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView' import {atoms as a, useTheme} from '#/alf' @@ -143,7 +142,7 @@ function StepCaptchaInner({ }, [dispatch, state.handle]) return ( - + <> - + ) } diff --git a/src/screens/Signup/StepHandle/index.tsx b/src/screens/Signup/StepHandle/index.tsx index 64333933cc..696c4d468b 100644 --- a/src/screens/Signup/StepHandle/index.tsx +++ b/src/screens/Signup/StepHandle/index.tsx @@ -19,7 +19,6 @@ import { checkHandleAvailability, useHandleAvailabilityQuery, } from '#/state/queries/handle-availability' -import {ScreenTransition} from '#/screens/Login/ScreenTransition' import {useSignupContext} from '#/screens/Signup/state' import {atoms as a, native, useTheme} from '#/alf' import * as TextField from '#/components/forms/TextField' @@ -141,7 +140,7 @@ export function StepHandle() { !validCheck.totalLength return ( - + <> @@ -252,7 +251,7 @@ export function StepHandle() { onNextPress={onNextPress} /> - + ) } diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx index 842ddadc52..e3664c0194 100644 --- a/src/screens/Signup/StepInfo/index.tsx +++ b/src/screens/Signup/StepInfo/index.tsx @@ -7,7 +7,6 @@ import type tldts from 'tldts' import {isEmailMaybeInvalid} from '#/lib/strings/email' import {logger} from '#/logger' -import {ScreenTransition} from '#/screens/Login/ScreenTransition' import {is13, is18, useSignupContext} from '#/screens/Signup/state' import {Policies} from '#/screens/Signup/StepInfo/Policies' import {atoms as a, native} from '#/alf' @@ -147,7 +146,7 @@ export function StepInfo({ } return ( - + <> - + ) } diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx index 807bbff4f9..21f9547d0c 100644 --- a/src/screens/Signup/index.tsx +++ b/src/screens/Signup/index.tsx @@ -23,11 +23,12 @@ import { import {StepCaptcha} from '#/screens/Signup/StepCaptcha' import {StepHandle} from '#/screens/Signup/StepHandle' import {StepInfo} from '#/screens/Signup/StepInfo' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {atoms as a, native, useBreakpoints, useTheme} from '#/alf' import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' import {Divider} from '#/components/Divider' import {LinearGradientBackground} from '#/components/LinearGradientBackground' import {InlineLinkText} from '#/components/Link' +import {ScreenTransition} from '#/components/ScreenTransition' import {Text} from '#/components/Typography' import {GCP_PROJECT_ID} from '#/env' import * as bsky from '#/types/bsky' @@ -116,109 +117,120 @@ export function Signup({onPressBack}: {onPressBack: () => void}) { }, []) return ( - - - - {showStarterPackCard && - bsky.dangerousIsType( - starterPack.record, - AppBskyGraphStarterpack.isRecord, - ) ? ( - - - - {starterPack.record.name} - - - {starterPack.feeds?.length ? ( - - You'll follow the suggested users and feeds once you - finish creating your account! - + + + + + {showStarterPackCard && + bsky.dangerousIsType( + starterPack.record, + AppBskyGraphStarterpack.isRecord, + ) ? ( + + + + {starterPack.record.name} + + + {starterPack.feeds?.length ? ( + + You'll follow the suggested users and feeds once you + finish creating your account! + + ) : ( + + You'll follow the suggested users once you finish + creating your account! + + )} + + + + ) : null} + + + + + + + Step {state.activeStep + 1} of{' '} + {state.serviceDescription && + !state.serviceDescription.phoneVerificationRequired + ? '2' + : '3'} + + + + {state.activeStep === SignupStep.INFO ? ( + Your account + ) : state.activeStep === SignupStep.HANDLE ? ( + Choose your username + ) : ( + Complete the challenge + )} + + + + {state.activeStep === SignupStep.INFO ? ( + + ) : state.activeStep === SignupStep.HANDLE ? ( + ) : ( - - You'll follow the suggested users once you finish creating - your account! - + )} - - - - ) : null} - - - - - Step {state.activeStep + 1} of{' '} - {state.serviceDescription && - !state.serviceDescription.phoneVerificationRequired - ? '2' - : '3'} - - - - {state.activeStep === SignupStep.INFO ? ( - Your account - ) : state.activeStep === SignupStep.HANDLE ? ( - Choose your username - ) : ( - Complete the challenge - )} - - - - {state.activeStep === SignupStep.INFO ? ( - - ) : state.activeStep === SignupStep.HANDLE ? ( - - ) : ( - - )} + + + + + + Having trouble?{' '} + + Contact support + + + + + - - - - - - - Having trouble?{' '} - - Contact support - - - - - - + + + ) } diff --git a/src/screens/Signup/state.ts b/src/screens/Signup/state.ts index 29b27e7a24..5bf0466d75 100644 --- a/src/screens/Signup/state.ts +++ b/src/screens/Signup/state.ts @@ -41,6 +41,7 @@ type ErrorField = export type SignupState = { hasPrev: boolean activeStep: SignupStep + screenTransitionDirection: 'Forward' | 'Backward' serviceUrl: string serviceDescription?: ServiceDescription @@ -84,6 +85,7 @@ export type SignupAction = export const initialState: SignupState = { hasPrev: false, activeStep: SignupStep.INFO, + screenTransitionDirection: 'Forward', serviceUrl: DEFAULT_SERVICE, serviceDescription: undefined, @@ -126,7 +128,7 @@ export function reducer(s: SignupState, a: SignupAction): SignupState { switch (a.type) { case 'prev': { if (s.activeStep !== SignupStep.INFO) { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + next.screenTransitionDirection = 'Backward' next.activeStep-- next.error = '' next.errorField = undefined @@ -135,7 +137,7 @@ export function reducer(s: SignupState, a: SignupAction): SignupState { } case 'next': { if (s.activeStep !== SignupStep.CAPTCHA) { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + next.screenTransitionDirection = 'Forward' next.activeStep++ next.error = '' next.errorField = undefined diff --git a/src/screens/StarterPack/Wizard/StepDetails.tsx b/src/screens/StarterPack/Wizard/StepDetails.tsx index ba18a4b023..00fff95c4e 100644 --- a/src/screens/StarterPack/Wizard/StepDetails.tsx +++ b/src/screens/StarterPack/Wizard/StepDetails.tsx @@ -8,7 +8,7 @@ import {useWizardState} from '#/screens/StarterPack/Wizard/State' import {atoms as a, useTheme} from '#/alf' import * as TextField from '#/components/forms/TextField' import {StarterPack} from '#/components/icons/StarterPack' -import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition' +import {ScreenTransition} from '#/components/ScreenTransition' import {Text} from '#/components/Typography' export function StepDetails() { @@ -23,7 +23,7 @@ export function StepDetails() { }) return ( - + diff --git a/src/screens/StarterPack/Wizard/StepFeeds.tsx b/src/screens/StarterPack/Wizard/StepFeeds.tsx index c87408e23d..0f16874642 100644 --- a/src/screens/StarterPack/Wizard/StepFeeds.tsx +++ b/src/screens/StarterPack/Wizard/StepFeeds.tsx @@ -17,7 +17,7 @@ import {atoms as a, useTheme} from '#/alf' import {SearchInput} from '#/components/forms/SearchInput' import {useThrottledValue} from '#/components/hooks/useThrottledValue' import {Loader} from '#/components/Loader' -import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition' +import {ScreenTransition} from '#/components/ScreenTransition' import {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardListCard' import {Text} from '#/components/Typography' @@ -79,7 +79,10 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) { } return ( - + + - - - + + + + - - + + + + + + What's up? + - - What's up? - - - - - - - - - + + + - - - + + + + + + + + ) } From 3bc906b9110428dc54defe74927b8eb13c6041d1 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 23 Sep 2025 20:06:11 +0300 Subject: [PATCH 02/29] fix profile feed liked by tablet offset (#9066) --- src/view/screens/ProfileFeedLikedBy.tsx | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/view/screens/ProfileFeedLikedBy.tsx b/src/view/screens/ProfileFeedLikedBy.tsx index 0a2ffc0976..d26ff61725 100644 --- a/src/view/screens/ProfileFeedLikedBy.tsx +++ b/src/view/screens/ProfileFeedLikedBy.tsx @@ -1,5 +1,5 @@ -import React from 'react' -import {msg} from '@lingui/macro' +import {useCallback} from 'react' +import {Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' @@ -10,8 +10,6 @@ import { import {makeRecordUri} from '#/lib/strings/url-helpers' import {useSetMinimalShellMode} from '#/state/shell' import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy' -import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView} from '#/view/com/util/Views' import * as Layout from '#/components/Layout' type Props = NativeStackScreenProps @@ -22,17 +20,23 @@ export const ProfileFeedLikedByScreen = ({route}: Props) => { const {_} = useLingui() useFocusEffect( - React.useCallback(() => { + useCallback(() => { setMinimalShellMode(false) }, [setMinimalShellMode]), ) return ( - - - - + + + + + Liked By + + + + + ) } From e90cfdc84959a19892b3db8f34adbe37caca1a78 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 23 Sep 2025 20:11:21 +0300 Subject: [PATCH 03/29] Modernise list create/edit dialog (#8223) --- src/components/Dialog/index.tsx | 5 +- .../dialogs/lists/CreateOrEditListDialog.tsx | 454 ++++++++++++++++++ src/lib/strings/helpers.ts | 11 +- .../Profile/Header/EditProfileDialog.tsx | 12 +- .../components/MoreOptionsMenu.tsx | 15 +- src/state/modals/index.tsx | 9 - .../composer/photos/EditImageDialog.web.tsx | 2 +- src/view/com/modals/Modal.tsx | 6 +- src/view/com/modals/Modal.web.tsx | 5 +- src/view/screens/Lists.tsx | 47 +- src/view/screens/ModerationModlists.tsx | 47 +- 11 files changed, 536 insertions(+), 77 deletions(-) create mode 100644 src/components/dialogs/lists/CreateOrEditListDialog.tsx diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index de8287a53c..ee2c0f76c7 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -267,7 +267,10 @@ export const ScrollableInner = React.forwardRef( scrollEventThrottle={50} onScroll={isAndroid ? onScroll : undefined} keyboardShouldPersistTaps="handled" - stickyHeaderIndices={header ? [0] : undefined}> + // TODO: figure out why this positions the header absolutely (rather than stickily) + // on Android. fine to disable for now, because we don't have any + // dialogs that use this that actually scroll -sfn + stickyHeaderIndices={ios(header ? [0] : undefined)}> {header} {children} diff --git a/src/components/dialogs/lists/CreateOrEditListDialog.tsx b/src/components/dialogs/lists/CreateOrEditListDialog.tsx new file mode 100644 index 0000000000..5045853f6e --- /dev/null +++ b/src/components/dialogs/lists/CreateOrEditListDialog.tsx @@ -0,0 +1,454 @@ +import {useCallback, useEffect, useMemo, useState} from 'react' +import {useWindowDimensions, View} from 'react-native' +import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' +import {msg, Plural, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {cleanError} from '#/lib/strings/errors' +import {useWarnMaxGraphemeCount} from '#/lib/strings/helpers' +import {richTextToString} from '#/lib/strings/rich-text-helpers' +import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' +import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' +import {type ImageMeta} from '#/state/gallery' +import { + useListCreateMutation, + useListMetadataMutation, +} from '#/state/queries/list' +import {useAgent} from '#/state/session' +import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' +import * as Toast from '#/view/com/util/Toast' +import {EditableUserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useTheme, web} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as TextField from '#/components/forms/TextField' +import {Loader} from '#/components/Loader' +import * as Prompt from '#/components/Prompt' +import {Text} from '#/components/Typography' + +const DISPLAY_NAME_MAX_GRAPHEMES = 64 +const DESCRIPTION_MAX_GRAPHEMES = 300 + +export function CreateOrEditListDialog({ + control, + list, + purpose, + onSave, +}: { + control: Dialog.DialogControlProps + list?: AppBskyGraphDefs.ListView + purpose?: AppBskyGraphDefs.ListPurpose + onSave?: (uri: string) => void +}) { + const {_} = useLingui() + const cancelControl = Dialog.useDialogControl() + const [dirty, setDirty] = useState(false) + const {height} = useWindowDimensions() + + // 'You might lose unsaved changes' warning + useEffect(() => { + if (isWeb && dirty) { + const abortController = new AbortController() + const {signal} = abortController + window.addEventListener('beforeunload', evt => evt.preventDefault(), { + signal, + }) + return () => { + abortController.abort() + } + } + }, [dirty]) + + const onPressCancel = useCallback(() => { + if (dirty) { + cancelControl.open() + } else { + control.close() + } + }, [dirty, control, cancelControl]) + + return ( + + + + control.close()} + confirmButtonCta={_(msg`Discard`)} + confirmButtonColor="negative" + /> + + ) +} + +function DialogInner({ + list, + purpose, + onSave, + setDirty, + onPressCancel, +}: { + list?: AppBskyGraphDefs.ListView + purpose?: AppBskyGraphDefs.ListPurpose + onSave?: (uri: string) => void + setDirty: (dirty: boolean) => void + onPressCancel: () => void +}) { + const activePurpose = useMemo(() => { + if (list?.purpose) { + return list.purpose + } + if (purpose) { + return purpose + } + return 'app.bsky.graph.defs#curatelist' + }, [list, purpose]) + const isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist' + + const {_} = useLingui() + const t = useTheme() + const agent = useAgent() + const control = Dialog.useDialogContext() + const { + mutateAsync: createListMutation, + error: createListError, + isError: isCreateListError, + isPending: isCreatingList, + } = useListCreateMutation() + const { + mutateAsync: updateListMutation, + error: updateListError, + isError: isUpdateListError, + isPending: isUpdatingList, + } = useListMetadataMutation() + const [imageError, setImageError] = useState('') + const [displayNameTooShort, setDisplayNameTooShort] = useState(false) + const initialDisplayName = list?.name || '' + const [displayName, setDisplayName] = useState(initialDisplayName) + const initialDescription = list?.description || '' + const [descriptionRt, setDescriptionRt] = useState(() => { + const text = list?.description + const facets = list?.descriptionFacets + + if (!text || !facets) { + return new RichTextAPI({text: text || ''}) + } + + // We want to be working with a blank state here, so let's get the + // serialized version and turn it back into a RichText + const serialized = richTextToString(new RichTextAPI({text, facets}), false) + + const richText = new RichTextAPI({text: serialized}) + richText.detectFacetsWithoutResolution() + + return richText + }) + + const [listAvatar, setListAvatar] = useState( + list?.avatar, + ) + const [newListAvatar, setNewListAvatar] = useState< + ImageMeta | undefined | null + >() + + const dirty = + displayName !== initialDisplayName || + descriptionRt.text !== initialDescription || + listAvatar !== list?.avatar + + useEffect(() => { + setDirty(dirty) + }, [dirty, setDirty]) + + const onSelectNewAvatar = useCallback( + (img: ImageMeta | null) => { + setImageError('') + if (img === null) { + setNewListAvatar(null) + setListAvatar(null) + return + } + try { + setNewListAvatar(img) + setListAvatar(img.path) + } catch (e: any) { + setImageError(cleanError(e)) + } + }, + [setNewListAvatar, setListAvatar, setImageError], + ) + + const onPressSave = useCallback(async () => { + setImageError('') + setDisplayNameTooShort(false) + try { + if (displayName.length === 0) { + setDisplayNameTooShort(true) + return + } + + let richText = new RichTextAPI( + {text: descriptionRt.text.trimEnd()}, + {cleanNewlines: true}, + ) + + await richText.detectFacets(agent) + richText = shortenLinks(richText) + richText = stripInvalidMentions(richText) + + if (list) { + await updateListMutation({ + uri: list.uri, + name: displayName, + description: richText.text, + descriptionFacets: richText.facets, + avatar: newListAvatar, + }) + Toast.show( + isCurateList + ? _(msg({message: 'User list updated', context: 'toast'})) + : _(msg({message: 'Moderation list updated', context: 'toast'})), + ) + control.close(() => onSave?.(list.uri)) + } else { + const {uri} = await createListMutation({ + purpose: activePurpose, + name: displayName, + description: richText.text, + descriptionFacets: richText.facets, + avatar: newListAvatar, + }) + Toast.show( + isCurateList + ? _(msg({message: 'User list created', context: 'toast'})) + : _(msg({message: 'Moderation list created', context: 'toast'})), + ) + control.close(() => onSave?.(uri)) + } + } catch (e: any) { + logger.error('Failed to create/edit list', {message: String(e)}) + } + }, [ + list, + createListMutation, + updateListMutation, + onSave, + control, + displayName, + descriptionRt, + newListAvatar, + setImageError, + activePurpose, + isCurateList, + agent, + _, + ]) + + const displayNameTooLong = useWarnMaxGraphemeCount({ + text: displayName, + maxCount: DISPLAY_NAME_MAX_GRAPHEMES, + }) + const descriptionTooLong = useWarnMaxGraphemeCount({ + text: descriptionRt, + maxCount: DESCRIPTION_MAX_GRAPHEMES, + }) + + const cancelButton = useCallback( + () => ( + + ), + [onPressCancel, _], + ) + + const saveButton = useCallback( + () => ( + + ), + [ + _, + t, + dirty, + onPressSave, + isCreatingList, + isUpdatingList, + displayNameTooLong, + descriptionTooLong, + ], + ) + + const onChangeDisplayName = useCallback( + (text: string) => { + setDisplayName(text) + if (text.length > 0 && displayNameTooShort) { + setDisplayNameTooShort(false) + } + }, + [displayNameTooShort], + ) + + const onChangeDescription = useCallback( + (newText: string) => { + const richText = new RichTextAPI({text: newText}) + richText.detectFacetsWithoutResolution() + + setDescriptionRt(richText) + }, + [setDescriptionRt], + ) + + const title = list + ? isCurateList + ? _(msg`Edit user list`) + : _(msg`Edit moderation list`) + : isCurateList + ? _(msg`Create user list`) + : _(msg`Create moderation list`) + + return ( + + {title} + + }> + {isUpdateListError && ( + + )} + {isCreateListError && ( + + )} + {imageError !== '' && } + + + + List avatar + + + + + + + + List name + + + + + {(displayNameTooLong || displayNameTooShort) && ( + + {displayNameTooLong ? ( + + List name is too long.{' '} + + + ) : displayNameTooShort ? ( + List must have a name. + ) : null} + + )} + + + + + List description + + + + + {descriptionTooLong && ( + + + List description is too long.{' '} + + + + )} + + + + ) +} diff --git a/src/lib/strings/helpers.ts b/src/lib/strings/helpers.ts index 61ad4e85ba..3f7c0d4782 100644 --- a/src/lib/strings/helpers.ts +++ b/src/lib/strings/helpers.ts @@ -1,6 +1,9 @@ import {useCallback, useMemo} from 'react' +import {type RichText} from '@atproto/api' import Graphemer from 'graphemer' +import {shortenLinks} from './rich-text-manip' + export function enforceLen( str: string, len: number, @@ -45,13 +48,17 @@ export function useWarnMaxGraphemeCount({ text, maxCount, }: { - text: string + text: string | RichText maxCount: number }) { const splitter = useMemo(() => new Graphemer(), []) return useMemo(() => { - return splitter.countGraphemes(text) > maxCount + if (typeof text === 'string') { + return splitter.countGraphemes(text) > maxCount + } else { + return shortenLinks(text).graphemeLength > maxCount + } }, [splitter, maxCount, text]) } diff --git a/src/screens/Profile/Header/EditProfileDialog.tsx b/src/screens/Profile/Header/EditProfileDialog.tsx index eb9e9179df..b1c52d67d7 100644 --- a/src/screens/Profile/Header/EditProfileDialog.tsx +++ b/src/screens/Profile/Header/EditProfileDialog.tsx @@ -1,5 +1,5 @@ import {useCallback, useEffect, useState} from 'react' -import {Dimensions, View} from 'react-native' +import {useWindowDimensions, View} from 'react-native' import {type AppBskyActorDefs} from '@atproto/api' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -28,8 +28,6 @@ import {useSimpleVerificationState} from '#/components/verification' const DISPLAY_NAME_MAX_GRAPHEMES = 64 const DESCRIPTION_MAX_GRAPHEMES = 256 -const SCREEN_HEIGHT = Dimensions.get('window').height - export function EditProfileDialog({ profile, control, @@ -42,6 +40,7 @@ export function EditProfileDialog({ const {_} = useLingui() const cancelControl = Dialog.useDialogControl() const [dirty, setDirty] = useState(false) + const {height} = useWindowDimensions() const onPressCancel = useCallback(() => { if (dirty) { @@ -56,7 +55,7 @@ export function EditProfileDialog({ control={control} nativeOptions={{ preventDismiss: dirty, - minHeight: SCREEN_HEIGHT, + minHeight: height, }} webOptions={{ onBackgroundPress: () => { @@ -186,8 +185,7 @@ function DialogInner({ newUserAvatar, newUserBanner, }) - onUpdate?.() - control.close() + control.close(() => onUpdate?.()) Toast.show(_(msg({message: 'Profile updated', context: 'toast'}))) } catch (e: any) { logger.error('Failed to update user profile', {message: String(e)}) @@ -369,7 +367,7 @@ function DialogInner({ defaultValue={description} onChangeText={setDescription} multiline - label={_(msg`Display name`)} + label={_(msg`Description`)} placeholder={_(msg`Tell us a bit about yourself`)} testID="editProfileDescriptionInput" /> diff --git a/src/screens/ProfileList/components/MoreOptionsMenu.tsx b/src/screens/ProfileList/components/MoreOptionsMenu.tsx index 17ca43a823..a275854ff7 100644 --- a/src/screens/ProfileList/components/MoreOptionsMenu.tsx +++ b/src/screens/ProfileList/components/MoreOptionsMenu.tsx @@ -8,7 +8,6 @@ import {shareUrl} from '#/lib/sharing' import {toShareUrl} from '#/lib/strings/url-helpers' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' -import {useModalControls} from '#/state/modals' import { useListBlockMutation, useListDeleteMutation, @@ -18,6 +17,7 @@ import {useRemoveFeedMutation} from '#/state/queries/preferences' import {useSession} from '#/state/session' import {Button, ButtonIcon} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' +import {CreateOrEditListDialog} from '#/components/dialogs/lists/CreateOrEditListDialog' import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox' import {ChainLink_Stroke2_Corner0_Rounded as ChainLink} from '#/components/icons/ChainLink' import {DotGrid_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid' @@ -44,7 +44,7 @@ export function MoreOptionsMenu({ }) { const {_} = useLingui() const {currentAccount} = useSession() - const {openModal} = useModalControls() + const editListDialogControl = useDialogControl() const deleteListPromptControl = useDialogControl() const reportDialogControl = useReportDialogControl() const navigation = useNavigation() @@ -80,13 +80,6 @@ export function MoreOptionsMenu({ } } - const onPressEdit = () => { - openModal({ - name: 'create-or-edit-list', - list, - }) - } - const onPressDelete = async () => { await deleteList({uri: list.uri}) @@ -201,7 +194,7 @@ export function MoreOptionsMenu({ + onPress={editListDialogControl.open}> Edit list details @@ -275,6 +268,8 @@ export function MoreOptionsMenu({ + + void -} - export interface UserAddRemoveListsModal { name: 'user-add-remove-lists' subject: string @@ -46,7 +38,6 @@ export type Modal = | ContentLanguagesSettingsModal // Lists - | CreateOrEditListModal | UserAddRemoveListsModal // Bluesky access diff --git a/src/view/com/composer/photos/EditImageDialog.web.tsx b/src/view/com/composer/photos/EditImageDialog.web.tsx index cda4e9ecfa..b448fad3a7 100644 --- a/src/view/com/composer/photos/EditImageDialog.web.tsx +++ b/src/view/com/composer/photos/EditImageDialog.web.tsx @@ -19,7 +19,7 @@ import {type EditImageDialogProps} from './EditImageDialog' export function EditImageDialog(props: EditImageDialogProps) { return ( - + diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 79971e660a..d44f79a740 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -7,7 +7,6 @@ import {usePalette} from '#/lib/hooks/usePalette' import {useModalControls, useModals} from '#/state/modals' import {FullWindowOverlay} from '#/components/FullWindowOverlay' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' -import * as CreateOrEditListModal from './CreateOrEditList' import * as DeleteAccountModal from './DeleteAccount' import * as InviteCodesModal from './InviteCodes' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' @@ -44,10 +43,7 @@ export function ModalsContainer() { let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS let element - if (activeModal?.name === 'create-or-edit-list') { - snapPoints = CreateOrEditListModal.snapPoints - element = - } else if (activeModal?.name === 'user-add-remove-lists') { + if (activeModal?.name === 'user-add-remove-lists') { snapPoints = UserAddRemoveListsModal.snapPoints element = } else if (activeModal?.name === 'delete-account') { diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index d0799a3901..555b184b7a 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -6,7 +6,6 @@ import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {type Modal as ModalIface} from '#/state/modals' import {useModalControls, useModals} from '#/state/modals' -import * as CreateOrEditListModal from './CreateOrEditList' import * as DeleteAccountModal from './DeleteAccount' import * as InviteCodesModal from './InviteCodes' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' @@ -48,9 +47,7 @@ function Modal({modal}: {modal: ModalIface}) { } let element - if (modal.name === 'create-or-edit-list') { - element = - } else if (modal.name === 'user-add-remove-lists') { + if (modal.name === 'user-add-remove-lists') { element = } else if (modal.name === 'delete-account') { element = diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx index bcda97dc5a..b165979c2c 100644 --- a/src/view/screens/Lists.tsx +++ b/src/view/screens/Lists.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {useCallback} from 'react' import {AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -10,11 +10,12 @@ import { type NativeStackScreenProps, } from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types' -import {useModalControls} from '#/state/modals' import {useSetMinimalShellMode} from '#/state/shell' import {MyLists} from '#/view/com/lists/MyLists' import {atoms as a} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {CreateOrEditListDialog} from '#/components/dialogs/lists/CreateOrEditListDialog' import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' import * as Layout from '#/components/Layout' @@ -23,30 +24,18 @@ export function ListsScreen({}: Props) { const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() const navigation = useNavigation() - const {openModal} = useModalControls() const requireEmailVerification = useRequireEmailVerification() + const createListDialogControl = useDialogControl() useFocusEffect( - React.useCallback(() => { + useCallback(() => { setMinimalShellMode(false) }, [setMinimalShellMode]), ) - const onPressNewList = React.useCallback(() => { - openModal({ - name: 'create-or-edit-list', - purpose: 'app.bsky.graph.defs#curatelist', - onSave: (uri: string) => { - try { - const urip = new AtUri(uri) - navigation.navigate('ProfileList', { - name: urip.hostname, - rkey: urip.rkey, - }) - } catch {} - }, - }) - }, [openModal, navigation]) + const onPressNewList = useCallback(() => { + createListDialogControl.open() + }, [createListDialogControl]) const wrappedOnPressNewList = requireEmailVerification(onPressNewList, { instructions: [ @@ -56,6 +45,19 @@ export function ListsScreen({}: Props) { ], }) + const onCreateList = useCallback( + (uri: string) => { + try { + const urip = new AtUri(uri) + navigation.navigate('ProfileList', { + name: urip.hostname, + rkey: urip.rkey, + }) + } catch {} + }, + [navigation], + ) + return ( @@ -78,7 +80,14 @@ export function ListsScreen({}: Props) { + + + ) } diff --git a/src/view/screens/ModerationModlists.tsx b/src/view/screens/ModerationModlists.tsx index 23ed492f64..1f786d88bc 100644 --- a/src/view/screens/ModerationModlists.tsx +++ b/src/view/screens/ModerationModlists.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {useCallback} from 'react' import {AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -10,11 +10,12 @@ import { type NativeStackScreenProps, } from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types' -import {useModalControls} from '#/state/modals' import {useSetMinimalShellMode} from '#/state/shell' import {MyLists} from '#/view/com/lists/MyLists' import {atoms as a} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {CreateOrEditListDialog} from '#/components/dialogs/lists/CreateOrEditListDialog' import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' import * as Layout from '#/components/Layout' @@ -23,30 +24,18 @@ export function ModerationModlistsScreen({}: Props) { const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() const navigation = useNavigation() - const {openModal} = useModalControls() const requireEmailVerification = useRequireEmailVerification() + const createListDialogControl = useDialogControl() useFocusEffect( - React.useCallback(() => { + useCallback(() => { setMinimalShellMode(false) }, [setMinimalShellMode]), ) - const onPressNewList = React.useCallback(() => { - openModal({ - name: 'create-or-edit-list', - purpose: 'app.bsky.graph.defs#modlist', - onSave: (uri: string) => { - try { - const urip = new AtUri(uri) - navigation.navigate('ProfileList', { - name: urip.hostname, - rkey: urip.rkey, - }) - } catch {} - }, - }) - }, [openModal, navigation]) + const onPressNewList = useCallback(() => { + createListDialogControl.open() + }, [createListDialogControl]) const wrappedOnPressNewList = requireEmailVerification(onPressNewList, { instructions: [ @@ -56,6 +45,19 @@ export function ModerationModlistsScreen({}: Props) { ], }) + const onCreateList = useCallback( + (uri: string) => { + try { + const urip = new AtUri(uri) + navigation.navigate('ProfileList', { + name: urip.hostname, + rkey: urip.rkey, + }) + } catch {} + }, + [navigation], + ) + return ( @@ -78,7 +80,14 @@ export function ModerationModlistsScreen({}: Props) { + + + ) } From 46a97fadcb71d1044be9796c814cfdc82e348605 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 23 Sep 2025 21:45:51 +0300 Subject: [PATCH 04/29] Delete unused modals and invite code stuff (#8244) * delete waitlist modal, which doesn't exist * delete invite code modal * rip out copied invites persisted state * delete invite query * delete unused modal implementations --- src/App.native.tsx | 35 +- src/App.web.tsx | 25 +- src/state/invites.tsx | 59 ---- src/state/modals/index.tsx | 12 - src/state/queries/invites.ts | 65 ---- src/view/com/modals/CreateOrEditList.tsx | 403 ----------------------- src/view/com/modals/CropImage.web.tsx | 145 -------- src/view/com/modals/InviteCodes.tsx | 287 ---------------- src/view/com/modals/Modal.tsx | 4 - src/view/com/modals/Modal.web.tsx | 3 - src/view/com/testing/TestCtrls.e2e.tsx | 8 - 11 files changed, 27 insertions(+), 1019 deletions(-) delete mode 100644 src/state/invites.tsx delete mode 100644 src/state/queries/invites.ts delete mode 100644 src/view/com/modals/CreateOrEditList.tsx delete mode 100644 src/view/com/modals/CropImage.web.tsx delete mode 100644 src/view/com/modals/InviteCodes.tsx diff --git a/src/App.native.tsx b/src/App.native.tsx index 036ecff60e..104a7ecaec 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -38,7 +38,6 @@ import { } from '#/state/geolocation' import {GlobalGestureEventsProvider} from '#/state/global-gesture-events' import {Provider as HomeBadgeProvider} from '#/state/home-badge' -import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as LightboxStateProvider} from '#/state/lightbox' import {MessagesProvider} from '#/state/messages' import {Provider as ModalStateProvider} from '#/state/modals' @@ -225,24 +224,22 @@ function App() { - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index c86960172a..569c9be799 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -26,7 +26,6 @@ import { Provider as GeolocationProvider, } from '#/state/geolocation' import {Provider as HomeBadgeProvider} from '#/state/home-badge' -import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as LightboxStateProvider} from '#/state/lightbox' import {MessagesProvider} from '#/state/messages' import {Provider as ModalStateProvider} from '#/state/modals' @@ -199,19 +198,17 @@ function App() { - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/state/invites.tsx b/src/state/invites.tsx deleted file mode 100644 index 4f12cb12f4..0000000000 --- a/src/state/invites.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react' - -import * as persisted from '#/state/persisted' - -type StateContext = persisted.Schema['invites'] -type ApiContext = { - setInviteCopied: (code: string) => void -} - -const stateContext = React.createContext( - persisted.defaults.invites, -) -stateContext.displayName = 'InvitesStateContext' -const apiContext = React.createContext({ - setInviteCopied(_: string) {}, -}) -apiContext.displayName = 'InvitesApiContext' - -export function Provider({children}: React.PropsWithChildren<{}>) { - const [state, setState] = React.useState(persisted.get('invites')) - - const api = React.useMemo( - () => ({ - setInviteCopied(code: string) { - setState(state => { - state = { - ...state, - copiedInvites: state.copiedInvites.includes(code) - ? state.copiedInvites - : state.copiedInvites.concat([code]), - } - persisted.write('invites', state) - return state - }) - }, - }), - [setState], - ) - - React.useEffect(() => { - return persisted.onUpdate('invites', nextInvites => { - setState(nextInvites) - }) - }, [setState]) - - return ( - - {children} - - ) -} - -export function useInvitesState() { - return React.useContext(stateContext) -} - -export function useInvitesAPI() { - return React.useContext(apiContext) -} diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 661ab3a15a..dab90c0af3 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -15,14 +15,6 @@ export interface DeleteAccountModal { name: 'delete-account' } -export interface WaitlistModal { - name: 'waitlist' -} - -export interface InviteCodesModal { - name: 'invite-codes' -} - export interface ContentLanguagesSettingsModal { name: 'content-languages-settings' } @@ -40,10 +32,6 @@ export type Modal = // Lists | UserAddRemoveListsModal - // Bluesky access - | WaitlistModal - | InviteCodesModal - const ModalContext = React.createContext<{ isModalActive: boolean activeModals: Modal[] diff --git a/src/state/queries/invites.ts b/src/state/queries/invites.ts deleted file mode 100644 index ed7fc534f9..0000000000 --- a/src/state/queries/invites.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {type ComAtprotoServerDefs} from '@atproto/api' -import {useQuery} from '@tanstack/react-query' - -import {cleanError} from '#/lib/strings/errors' -import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' - -function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean { - return invite.available - invite.uses.length > 0 && !invite.disabled -} - -const inviteCodesQueryKeyRoot = 'inviteCodes' - -export type InviteCodesQueryResponse = Exclude< - ReturnType['data'], - undefined -> -export function useInviteCodesQuery() { - const agent = useAgent() - return useQuery({ - staleTime: STALE.MINUTES.FIVE, - queryKey: [inviteCodesQueryKeyRoot], - queryFn: async () => { - const res = await agent.com.atproto.server - .getAccountInviteCodes({}) - .catch(e => { - if (cleanError(e) === 'Bad token scope') { - return null - } else { - throw e - } - }) - - if (res === null) { - return { - disabled: true, - all: [], - available: [], - used: [], - } - } - - if (!res.data?.codes) { - throw new Error(`useInviteCodesQuery: no codes returned`) - } - - const available = res.data.codes.filter(isInviteAvailable) - const used = res.data.codes - .filter(code => !isInviteAvailable(code)) - .sort((a, b) => { - return ( - new Date(b.uses[0].usedAt).getTime() - - new Date(a.uses[0].usedAt).getTime() - ) - }) - - return { - disabled: false, - all: [...available, ...used], - available, - used, - } - }, - }) -} diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx deleted file mode 100644 index 3687dce901..0000000000 --- a/src/view/com/modals/CreateOrEditList.tsx +++ /dev/null @@ -1,403 +0,0 @@ -import {useCallback, useMemo, useState} from 'react' -import { - ActivityIndicator, - KeyboardAvoidingView, - ScrollView, - StyleSheet, - TextInput, - TouchableOpacity, - View, -} from 'react-native' -import {LinearGradient} from 'expo-linear-gradient' -import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {cleanError, isNetworkError} from '#/lib/strings/errors' -import {enforceLen} from '#/lib/strings/helpers' -import {richTextToString} from '#/lib/strings/rich-text-helpers' -import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' -import {colors, gradients, s} from '#/lib/styles' -import {useTheme} from '#/lib/ThemeContext' -import {type ImageMeta} from '#/state/gallery' -import {useModalControls} from '#/state/modals' -import { - useListCreateMutation, - useListMetadataMutation, -} from '#/state/queries/list' -import {useAgent} from '#/state/session' -import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import {Text} from '#/view/com/util/text/Text' -import * as Toast from '#/view/com/util/Toast' -import {EditableUserAvatar} from '#/view/com/util/UserAvatar' - -const MAX_NAME = 64 // todo -const MAX_DESCRIPTION = 300 // todo - -export const snapPoints = ['fullscreen'] - -export function Component({ - purpose, - onSave, - list, -}: { - purpose?: string - onSave?: (uri: string) => void - list?: AppBskyGraphDefs.ListView -}) { - const {closeModal} = useModalControls() - const {isMobile} = useWebMediaQueries() - const [error, setError] = useState('') - const pal = usePalette('default') - const theme = useTheme() - const {_} = useLingui() - const listCreateMutation = useListCreateMutation() - const listMetadataMutation = useListMetadataMutation() - const agent = useAgent() - - const activePurpose = useMemo(() => { - if (list?.purpose) { - return list.purpose - } - if (purpose) { - return purpose - } - return 'app.bsky.graph.defs#curatelist' - }, [list, purpose]) - const isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist' - - const [isProcessing, setProcessing] = useState(false) - const [name, setName] = useState(list?.name || '') - - const [descriptionRt, setDescriptionRt] = useState(() => { - const text = list?.description - const facets = list?.descriptionFacets - - if (!text || !facets) { - return new RichTextAPI({text: text || ''}) - } - - // We want to be working with a blank state here, so let's get the - // serialized version and turn it back into a RichText - const serialized = richTextToString(new RichTextAPI({text, facets}), false) - - const richText = new RichTextAPI({text: serialized}) - richText.detectFacetsWithoutResolution() - - return richText - }) - const graphemeLength = useMemo(() => { - return shortenLinks(descriptionRt).graphemeLength - }, [descriptionRt]) - const isDescriptionOver = graphemeLength > MAX_DESCRIPTION - - const [avatar, setAvatar] = useState(list?.avatar) - const [newAvatar, setNewAvatar] = useState() - - const onDescriptionChange = useCallback( - (newText: string) => { - const richText = new RichTextAPI({text: newText}) - richText.detectFacetsWithoutResolution() - - setDescriptionRt(richText) - }, - [setDescriptionRt], - ) - - const onPressCancel = useCallback(() => { - closeModal() - }, [closeModal]) - - const onSelectNewAvatar = useCallback( - (img: ImageMeta | null) => { - if (!img) { - setNewAvatar(null) - setAvatar(undefined) - return - } - try { - setNewAvatar(img) - setAvatar(img.path) - } catch (e: any) { - setError(cleanError(e)) - } - }, - [setNewAvatar, setAvatar, setError], - ) - - const onPressSave = useCallback(async () => { - const nameTrimmed = name.trim() - if (!nameTrimmed) { - setError(_(msg`Name is required`)) - return - } - setProcessing(true) - if (error) { - setError('') - } - try { - let richText = new RichTextAPI( - {text: descriptionRt.text.trimEnd()}, - {cleanNewlines: true}, - ) - - await richText.detectFacets(agent) - richText = shortenLinks(richText) - richText = stripInvalidMentions(richText) - - if (list) { - await listMetadataMutation.mutateAsync({ - uri: list.uri, - name: nameTrimmed, - description: richText.text, - descriptionFacets: richText.facets, - avatar: newAvatar, - }) - Toast.show( - isCurateList - ? _(msg({message: 'User list updated', context: 'toast'})) - : _(msg({message: 'Moderation list updated', context: 'toast'})), - ) - onSave?.(list.uri) - } else { - const res = await listCreateMutation.mutateAsync({ - purpose: activePurpose, - name, - description: richText.text, - descriptionFacets: richText.facets, - avatar: newAvatar, - }) - Toast.show( - isCurateList - ? _(msg({message: 'User list created', context: 'toast'})) - : _(msg({message: 'Moderation list created', context: 'toast'})), - ) - onSave?.(res.uri) - } - closeModal() - } catch (e: any) { - if (isNetworkError(e)) { - setError( - _( - msg`Failed to create the list. Check your internet connection and try again.`, - ), - ) - } else { - setError(cleanError(e)) - } - } - setProcessing(false) - }, [ - setProcessing, - setError, - error, - onSave, - closeModal, - activePurpose, - isCurateList, - name, - descriptionRt, - newAvatar, - list, - listMetadataMutation, - listCreateMutation, - _, - agent, - ]) - - return ( - - - - {isCurateList ? ( - list ? ( - Edit User List - ) : ( - New User List - ) - ) : list ? ( - Edit Moderation List - ) : ( - New Moderation List - )} - - {error !== '' && ( - - - - )} - - List Avatar - - - - - - - - - List Name - - - setName(enforceLen(v, MAX_NAME))} - accessible={true} - accessibilityLabel={_(msg`Name`)} - accessibilityHint="" - accessibilityLabelledBy="list-name" - /> - - - - - Description - - - {graphemeLength}/{MAX_DESCRIPTION} - - - - - {isProcessing ? ( - - - - ) : ( - - - - Save - - - - )} - - - - Cancel - - - - - - - ) -} - -const styles = StyleSheet.create({ - title: { - textAlign: 'center', - fontWeight: '600', - fontSize: 24, - marginBottom: 18, - }, - labelWrapper: { - flexDirection: 'row', - gap: 8, - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 4, - paddingBottom: 4, - marginTop: 20, - }, - label: { - fontWeight: '600', - }, - form: { - paddingHorizontal: 6, - }, - textInput: { - borderWidth: 1, - borderRadius: 6, - paddingHorizontal: 14, - paddingVertical: 10, - fontSize: 16, - }, - textArea: { - borderWidth: 1, - borderRadius: 6, - paddingHorizontal: 12, - paddingTop: 10, - fontSize: 16, - height: 100, - textAlignVertical: 'top', - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - borderRadius: 32, - padding: 10, - marginBottom: 10, - }, - avi: { - width: 84, - height: 84, - borderWidth: 2, - borderRadius: 42, - marginTop: 4, - }, - errorContainer: {marginTop: 20}, -}) diff --git a/src/view/com/modals/CropImage.web.tsx b/src/view/com/modals/CropImage.web.tsx deleted file mode 100644 index 78c0466f0b..0000000000 --- a/src/view/com/modals/CropImage.web.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' -import {LinearGradient} from 'expo-linear-gradient' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import ReactCrop, {type PercentCrop} from 'react-image-crop' - -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {type PickerImage} from '#/lib/media/picker.shared' -import {getDataUriSize} from '#/lib/media/util' -import {gradients, s} from '#/lib/styles' -import {useModalControls} from '#/state/modals' -import {Text} from '#/view/com/util/text/Text' - -export const snapPoints = ['0%'] - -export function Component({ - uri, - aspect, - circular, - onSelect, -}: { - uri: string - aspect?: number - circular?: boolean - onSelect: (img?: PickerImage) => void -}) { - const pal = usePalette('default') - const {_} = useLingui() - - const {closeModal} = useModalControls() - const {isMobile} = useWebMediaQueries() - - const imageRef = React.useRef(null) - const [crop, setCrop] = React.useState() - - const isEmpty = !crop || (crop.width || crop.height) === 0 - - const onPressCancel = () => { - onSelect(undefined) - closeModal() - } - const onPressDone = async () => { - const img = imageRef.current! - - const result = await manipulateAsync( - uri, - isEmpty - ? [] - : [ - { - crop: { - originX: (crop.x * img.naturalWidth) / 100, - originY: (crop.y * img.naturalHeight) / 100, - width: (crop.width * img.naturalWidth) / 100, - height: (crop.height * img.naturalHeight) / 100, - }, - }, - ], - { - base64: true, - format: SaveFormat.JPEG, - }, - ) - - onSelect({ - path: result.uri, - mime: 'image/jpeg', - size: result.base64 !== undefined ? getDataUriSize(result.base64) : 0, - width: result.width, - height: result.height, - }) - - closeModal() - } - - return ( - - - setCrop(percentCrop)} - circularCrop={circular}> - - - - - - - Cancel - - - - - - - Done - - - - - - ) -} - -const styles = StyleSheet.create({ - cropper: { - marginLeft: 'auto', - marginRight: 'auto', - borderWidth: 1, - borderRadius: 4, - overflow: 'hidden', - alignItems: 'center', - }, - ctrls: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 10, - }, - btns: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 10, - }, - btn: { - borderRadius: 4, - paddingVertical: 8, - paddingHorizontal: 24, - }, -}) diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx deleted file mode 100644 index 93f7490625..0000000000 --- a/src/view/com/modals/InviteCodes.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import React from 'react' -import { - ActivityIndicator, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' -import {setStringAsync} from 'expo-clipboard' -import {type ComAtprotoServerDefs} from '@atproto/api' -import { - FontAwesomeIcon, - type FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {makeProfileLink} from '#/lib/routes/links' -import {cleanError} from '#/lib/strings/errors' -import {isWeb} from '#/platform/detection' -import {useInvitesAPI, useInvitesState} from '#/state/invites' -import {useModalControls} from '#/state/modals' -import { - type InviteCodesQueryResponse, - useInviteCodesQuery, -} from '#/state/queries/invites' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {Button} from '../util/forms/Button' -import {Link} from '../util/Link' -import {Text} from '../util/text/Text' -import * as Toast from '../util/Toast' -import {UserInfoText} from '../util/UserInfoText' -import {ScrollView} from './util' - -export const snapPoints = ['70%'] - -export function Component() { - const {isLoading, data: invites, error} = useInviteCodesQuery() - - return error ? ( - - ) : isLoading || !invites ? ( - - - - ) : ( - - ) -} - -export function Inner({invites}: {invites: InviteCodesQueryResponse}) { - const pal = usePalette('default') - const {_} = useLingui() - const {closeModal} = useModalControls() - const {isTabletOrDesktop} = useWebMediaQueries() - - const onClose = React.useCallback(() => { - closeModal() - }, [closeModal]) - - if (invites.all.length === 0) { - return ( - - - - - You don't have any invite codes yet! We'll send you some when - you've been on Bluesky for a little longer. - - - - - - - {copy.title} + {copy.title} Your report will be sent to the Bluesky Moderation Service @@ -213,10 +213,11 @@ function SubmitStep({ )} - + Reason: {' '} - {reportOption.title} + {reportOption.title} @@ -346,7 +347,7 @@ function DoneStep({ return ( - + Report submitted diff --git a/src/components/forms/FormError.tsx b/src/components/forms/FormError.tsx index d51243d505..d28c89c8d4 100644 --- a/src/components/forms/FormError.tsx +++ b/src/components/forms/FormError.tsx @@ -20,7 +20,8 @@ export function FormError({error}: {error?: string}) { ]}> - + {error} diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 85fb7c481a..48f71e73a0 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -15,7 +15,6 @@ import { android, applyFonts, atoms as a, - ios, platform, type TextStyleProp, tokens, @@ -202,17 +201,23 @@ export function createInput(Component: typeof TextInput) { a.px_xs, { // paddingVertical doesn't work w/multiline - esb - lineHeight: a.text_md.fontSize * 1.1875, + lineHeight: a.text_md.fontSize * 1.2, textAlignVertical: rest.multiline ? 'top' : undefined, minHeight: rest.multiline ? 80 : undefined, minWidth: 0, + paddingTop: 13, + paddingBottom: 13, }, - ios({paddingTop: 12, paddingBottom: 13}), - // Needs to be sm on Paper, md on Fabric for some godforsaken reason -sfn - android(a.py_sm), - // fix for autofill styles covering border + android({ + paddingTop: 8, + paddingBottom: 9, + }), + /* + * Margins are needed here to avoid autofill background overlapping the + * top and bottom borders - esb + */ web({ - paddingTop: 10, + paddingTop: 11, paddingBottom: 11, marginTop: 2, marginBottom: 2, @@ -262,7 +267,7 @@ export function createInput(Component: typeof TextInput) { a.absolute, a.inset_0, a.rounded_sm, - t.atoms.bg_contrast_25, + t.atoms.bg_contrast_50, {borderColor: 'transparent', borderWidth: 2}, ctx.hovered ? chromeHover : {}, ctx.focused ? chromeFocus : {}, @@ -287,7 +292,12 @@ export function LabelText({ return ( + style={[ + a.text_sm, + a.font_semi_bold, + t.atoms.text_contrast_medium, + a.mb_sm, + ]}> {children} ) diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index bb9fde2e11..d6a968ecf7 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -249,7 +249,7 @@ export function LabelText({ return ( diff --git a/src/components/intents/VerifyEmailIntentDialog.tsx b/src/components/intents/VerifyEmailIntentDialog.tsx index ab628eeef9..3aca1b6d82 100644 --- a/src/components/intents/VerifyEmailIntentDialog.tsx +++ b/src/components/intents/VerifyEmailIntentDialog.tsx @@ -75,7 +75,7 @@ function Inner({}: {control: DialogControlProps}) { ) : status === 'success' ? ( - + Email Verified @@ -87,7 +87,7 @@ function Inner({}: {control: DialogControlProps}) { ) : status === 'failure' ? ( - + Invalid Verification Code @@ -100,13 +100,13 @@ function Inner({}: {control: DialogControlProps}) { ) : ( - + Email Resent We have sent another verification email to{' '} - + {currentAccount?.email} . diff --git a/src/components/interstitials/Trending.tsx b/src/components/interstitials/Trending.tsx index 5561be18e0..2580ef28f3 100644 --- a/src/components/interstitials/Trending.tsx +++ b/src/components/interstitials/Trending.tsx @@ -82,7 +82,7 @@ export function Inner() { style={[ t.atoms.text_contrast_medium, a.text_sm, - a.font_bold, + a.font_semi_bold, ]}> {' '} @@ -101,7 +101,7 @@ export function Inner() { style={[ t.atoms.text, a.text_sm, - a.font_bold, + a.font_semi_bold, {opacity: 0.7}, // NOTE: we use opacity 0.7 instead of a color to match the color of the home pager tab bar ]}> {topic.topic} diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx index 6be64335a2..175f92fd57 100644 --- a/src/components/interstitials/TrendingVideos.tsx +++ b/src/components/interstitials/TrendingVideos.tsx @@ -82,7 +82,7 @@ export function TrendingVideos() { a.align_center, a.justify_between, ]}> - + Trending Videos + ) +} + +function StackedButtonInnerText({ + children, + icon: Icon, +}: Pick) { + const textStyles = useSharedButtonTextStyles() + return ( + <> + + {children} + + ) +} diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx index eebb6f8924..f6a2c36a0b 100644 --- a/src/view/screens/Storybook/Buttons.tsx +++ b/src/view/screens/Storybook/Buttons.tsx @@ -8,6 +8,7 @@ import { ButtonIcon, type ButtonSize, ButtonText, + StackedButton, } from '#/components/Button' import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' @@ -18,6 +19,30 @@ export function Buttons() { Buttons + + + Bop it + + + Twist it + + + Pull it + + + {[ 'primary', 'secondary', From 144d61ef7655002f917be3ec624016b302100b80 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:40:10 +0000 Subject: [PATCH 27/29] Nightly source-language update --- src/locale/locales/en/messages.po | 174 +++++++++++++++--------------- 1 file changed, 90 insertions(+), 84 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 36b0c3160f..97a49b20aa 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -604,12 +604,12 @@ msgctxt "toast" msgid "Account muted" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:103 +#: src/components/moderation/ModerationDetailsDialog.tsx:105 #: src/lib/moderation/useModerationCauseDescription.ts:98 msgid "Account Muted" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:89 +#: src/components/moderation/ModerationDetailsDialog.tsx:91 msgid "Account Muted by List" msgstr "" @@ -735,8 +735,8 @@ msgstr "" msgid "Add media to post" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:403 -#: src/components/moderation/ReportDialog/index.tsx:407 +#: src/components/moderation/ReportDialog/index.tsx:406 +#: src/components/moderation/ReportDialog/index.tsx:410 msgid "Add more details (optional)" msgstr "" @@ -813,7 +813,7 @@ msgstr "" msgid "Additional details (limit 1000 characters)" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:421 +#: src/components/moderation/ReportDialog/index.tsx:424 msgid "Additional details (limit 300 characters)" msgstr "" @@ -903,7 +903,7 @@ msgid "Allow new messages from" msgstr "" #: src/screens/Settings/ActivityPrivacySettings.tsx:52 -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:92 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:91 msgid "Allow others to be notified of your posts" msgstr "" @@ -1051,7 +1051,7 @@ msgstr "" msgid "an unknown error occurred" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:134 +#: src/components/moderation/ModerationDetailsDialog.tsx:136 #: src/lib/moderation/useModerationCauseDescription.ts:144 msgid "an unknown labeler" msgstr "" @@ -1089,7 +1089,7 @@ msgstr "" #: src/screens/Settings/ActivityPrivacySettings.tsx:111 #: src/screens/Settings/ActivityPrivacySettings.tsx:116 -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:163 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:162 msgid "Anyone who follows me" msgstr "" @@ -1125,8 +1125,8 @@ msgstr "" msgid "App password names must be at least 4 characters long" msgstr "" -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:72 -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:75 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:71 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:74 msgid "App passwords" msgstr "" @@ -1135,16 +1135,16 @@ msgstr "" msgid "App Passwords" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:150 -#: src/components/moderation/LabelsOnMeDialog.tsx:153 +#: src/components/moderation/LabelsOnMeDialog.tsx:152 +#: src/components/moderation/LabelsOnMeDialog.tsx:155 msgid "Appeal" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:268 +#: src/components/moderation/LabelsOnMeDialog.tsx:270 msgid "Appeal \"{0}\" label" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:258 +#: src/components/moderation/LabelsOnMeDialog.tsx:260 #: src/screens/Messages/components/ChatDisabled.tsx:103 msgctxt "toast" msgid "Appeal submitted" @@ -1274,8 +1274,8 @@ msgstr "" msgid "Available" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:315 -#: src/components/moderation/LabelsOnMeDialog.tsx:316 +#: src/components/moderation/LabelsOnMeDialog.tsx:317 +#: src/components/moderation/LabelsOnMeDialog.tsx:318 #: src/screens/Login/ChooseAccountForm.tsx:90 #: src/screens/Login/ChooseAccountForm.tsx:95 #: src/screens/Login/ForgotPasswordForm.tsx:123 @@ -1558,7 +1558,7 @@ msgid "Business" msgstr "" #: src/components/LabelingServiceCard/index.tsx:62 -#: src/components/moderation/ReportDialog/index.tsx:683 +#: src/components/moderation/ReportDialog/index.tsx:686 #: src/screens/Search/components/StarterPackCard.tsx:106 #: src/screens/Search/Explore.tsx:930 msgid "By {0}" @@ -1695,7 +1695,7 @@ msgstr "" msgid "Change Handle" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:325 +#: src/components/moderation/ReportDialog/index.tsx:328 msgid "Change moderation service" msgstr "" @@ -1712,7 +1712,7 @@ msgstr "" msgid "Change post language to {suggestedLanguageName}" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:244 +#: src/components/moderation/ReportDialog/index.tsx:247 msgid "Change report reason" msgstr "" @@ -2186,12 +2186,12 @@ msgstr "" msgid "Content Languages" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:82 +#: src/components/moderation/ModerationDetailsDialog.tsx:84 #: src/lib/moderation/useModerationCauseDescription.ts:82 msgid "Content Not Available" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:50 +#: src/components/moderation/ModerationDetailsDialog.tsx:52 #: src/components/moderation/ScreenHider.tsx:99 #: src/lib/moderation/useGlobalLabelStrings.ts:22 #: src/lib/moderation/useModerationCauseDescription.ts:45 @@ -2478,7 +2478,7 @@ msgstr "" msgid "Create new account" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:585 +#: src/components/moderation/ReportDialog/index.tsx:588 #: src/components/ReportDialog/SelectReportOptionView.tsx:102 msgid "Create report for {0}" msgstr "" @@ -3055,7 +3055,7 @@ msgctxt "toast" msgid "Email 2FA disabled" msgstr "" -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:63 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:62 msgid "Email 2FA enabled" msgstr "" @@ -3126,6 +3126,7 @@ msgid "Enable media players for" msgstr "" #: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:139 +#: src/view/screens/Storybook/Admonitions.tsx:75 msgid "Enable notifications for an account by visiting their profile and pressing the <0>bell icon <1/>." msgstr "" @@ -3231,7 +3232,7 @@ msgstr "" msgid "Error loading post" msgstr "" -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:154 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:153 msgid "Error loading preference" msgstr "" @@ -3345,8 +3346,8 @@ msgstr "" msgid "Expires {0}" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:199 -#: src/components/moderation/ModerationDetailsDialog.tsx:208 +#: src/components/moderation/LabelsOnMeDialog.tsx:201 +#: src/components/moderation/ModerationDetailsDialog.tsx:210 msgid "Expires in {0}" msgstr "" @@ -3554,7 +3555,7 @@ msgstr "" msgid "Failed to send email, please try again." msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:254 +#: src/components/moderation/LabelsOnMeDialog.tsx:256 #: src/screens/Messages/components/ChatDisabled.tsx:99 msgid "Failed to submit appeal, please try again." msgstr "" @@ -4418,6 +4419,7 @@ msgid "If you want to change your password, we will send you a code to verify th msgstr "" #: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:157 +#: src/view/screens/Storybook/Admonitions.tsx:89 msgid "If you want to restrict who can receive notifications for your account's activity, you can change this in <0>Settings → Privacy and Security." msgstr "" @@ -4660,11 +4662,11 @@ msgstr "" msgid "Labels are annotations on users and content. They can be used to hide, warn, and categorize the network." msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:72 +#: src/components/moderation/LabelsOnMeDialog.tsx:74 msgid "Labels on your account" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:74 +#: src/components/moderation/LabelsOnMeDialog.tsx:76 msgid "Labels on your content" msgstr "" @@ -4746,8 +4748,8 @@ msgctxt "english-only-resource" msgid "Learn more about verification on Bluesky" msgstr "" -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:128 -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:131 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:127 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:130 msgid "Learn more about what is public on Bluesky." msgstr "" @@ -5031,7 +5033,7 @@ msgstr "" msgid "Log" msgstr "" -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:107 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:106 msgid "Logged-out visibility" msgstr "" @@ -5205,7 +5207,7 @@ msgstr "" msgid "Moderation" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:138 +#: src/components/moderation/ModerationDetailsDialog.tsx:140 msgid "Moderation details" msgstr "" @@ -5254,7 +5256,7 @@ msgstr "" msgid "Moderation tools" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:52 +#: src/components/moderation/ModerationDetailsDialog.tsx:54 #: src/lib/moderation/useModerationCauseDescription.ts:47 msgid "Moderator has chosen to set a general warning on the content." msgstr "" @@ -5437,8 +5439,8 @@ msgstr "" msgid "Navigates to your profile" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:271 -#: src/components/moderation/ReportDialog/index.tsx:288 +#: src/components/moderation/ReportDialog/index.tsx:274 +#: src/components/moderation/ReportDialog/index.tsx:291 msgid "Need to report a copyright violation, legal request, or regulatory compliance issue?" msgstr "" @@ -5649,7 +5651,7 @@ msgstr "" #: src/screens/Messages/Settings.tsx:109 #: src/screens/Settings/ActivityPrivacySettings.tsx:129 #: src/screens/Settings/ActivityPrivacySettings.tsx:134 -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:160 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:159 msgid "No one" msgstr "" @@ -5760,7 +5762,7 @@ msgstr "" msgid "Note about sharing" msgstr "" -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:117 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:116 msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites." msgstr "" @@ -5908,7 +5910,7 @@ msgstr "" #: src/screens/Settings/ActivityPrivacySettings.tsx:120 #: src/screens/Settings/ActivityPrivacySettings.tsx:125 -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:158 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:157 msgid "Only followers who I follow" msgstr "" @@ -6411,7 +6413,7 @@ msgstr "" msgid "Please enter your username" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:290 +#: src/components/moderation/LabelsOnMeDialog.tsx:292 msgid "Please explain why you think this label was incorrectly applied by {0}" msgstr "" @@ -6463,7 +6465,7 @@ msgstr "" msgid "Porn" msgstr "" -#: src/screens/PostThread/index.tsx:502 +#: src/screens/PostThread/index.tsx:503 msgctxt "description" msgid "Post" msgstr "" @@ -6505,12 +6507,12 @@ msgstr "" msgid "Post has been deleted" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:107 +#: src/components/moderation/ModerationDetailsDialog.tsx:109 #: src/lib/moderation/useModerationCauseDescription.ts:106 msgid "Post Hidden by Muted Word" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:110 +#: src/components/moderation/ModerationDetailsDialog.tsx:112 #: src/lib/moderation/useModerationCauseDescription.ts:115 msgid "Post Hidden by You" msgstr "" @@ -6618,11 +6620,12 @@ msgstr "" #: src/Navigation.tsx:407 #: src/Navigation.tsx:415 #: src/screens/Settings/ActivityPrivacySettings.tsx:40 -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:45 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:44 msgid "Privacy and Security" msgstr "" #: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:161 +#: src/view/screens/Storybook/Admonitions.tsx:93 msgid "Privacy and Security settings" msgstr "" @@ -7091,12 +7094,12 @@ msgstr "" msgid "Reply ({0, plural, one {# reply} other {# replies}})" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:116 +#: src/components/moderation/ModerationDetailsDialog.tsx:118 #: src/lib/moderation/useModerationCauseDescription.ts:125 msgid "Reply Hidden by Thread Author" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:115 +#: src/components/moderation/ModerationDetailsDialog.tsx:117 #: src/lib/moderation/useModerationCauseDescription.ts:124 msgid "Reply Hidden by You" msgstr "" @@ -7345,7 +7348,7 @@ msgstr "" #: src/components/dms/MessageItem.tsx:322 #: src/components/Error.tsx:65 #: src/components/Lists.tsx:110 -#: src/components/moderation/ReportDialog/index.tsx:229 +#: src/components/moderation/ReportDialog/index.tsx:232 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:55 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:58 #: src/components/StarterPack/ProfileStarterPacks.tsx:342 @@ -7363,10 +7366,12 @@ msgstr "" #: src/screens/Signup/BackNextButtons.tsx:53 #: src/view/com/util/error/ErrorMessage.tsx:60 #: src/view/com/util/error/ErrorScreen.tsx:97 +#: src/view/screens/Storybook/Admonitions.tsx:63 msgid "Retry" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:226 +#: src/components/moderation/ReportDialog/index.tsx:229 +#: src/view/screens/Storybook/Admonitions.tsx:60 msgid "Retry loading report options" msgstr "" @@ -7709,7 +7714,7 @@ msgstr "" msgid "Select languages" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:310 +#: src/components/moderation/ReportDialog/index.tsx:313 msgid "Select moderation service" msgstr "" @@ -7819,7 +7824,7 @@ msgstr "" msgid "Send report to {0}" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:649 +#: src/components/moderation/ReportDialog/index.tsx:652 msgid "Send report to {title}" msgstr "" @@ -7875,7 +7880,7 @@ msgstr "" msgid "Settings for activity from others" msgstr "" -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:85 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:84 msgid "Settings for allowing others to be notified of your posts" msgstr "" @@ -8270,9 +8275,10 @@ msgid "Something went wrong" msgstr "" #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:138 -#: src/components/moderation/ReportDialog/index.tsx:223 +#: src/components/moderation/ReportDialog/index.tsx:224 #: src/screens/Deactivated.tsx:94 #: src/screens/Settings/components/DeactivateAccountDialog.tsx:59 +#: src/view/screens/Storybook/Admonitions.tsx:55 msgid "Something went wrong, please try again" msgstr "" @@ -8315,8 +8321,8 @@ msgstr "" msgid "Sort replies to the same post by:" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:178 -#: src/components/moderation/ModerationDetailsDialog.tsx:186 +#: src/components/moderation/LabelsOnMeDialog.tsx:180 +#: src/components/moderation/ModerationDetailsDialog.tsx:188 msgid "Source: <0>{sourceName}" msgstr "" @@ -8412,8 +8418,8 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:117 #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:123 -#: src/components/moderation/LabelsOnMeDialog.tsx:324 -#: src/components/moderation/LabelsOnMeDialog.tsx:325 +#: src/components/moderation/LabelsOnMeDialog.tsx:326 +#: src/components/moderation/LabelsOnMeDialog.tsx:327 #: src/screens/Messages/components/ChatDisabled.tsx:154 #: src/screens/Messages/components/ChatDisabled.tsx:155 msgid "Submit" @@ -8427,9 +8433,9 @@ msgstr "" msgid "Submit Appeal" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:387 -#: src/components/moderation/ReportDialog/index.tsx:444 -#: src/components/moderation/ReportDialog/index.tsx:451 +#: src/components/moderation/ReportDialog/index.tsx:390 +#: src/components/moderation/ReportDialog/index.tsx:447 +#: src/components/moderation/ReportDialog/index.tsx:454 msgid "Submit report" msgstr "" @@ -8630,7 +8636,7 @@ msgstr "" msgid "Text field" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:288 +#: src/components/moderation/LabelsOnMeDialog.tsx:290 #: src/screens/Messages/components/ChatDisabled.tsx:120 msgid "Text input field" msgstr "" @@ -8687,7 +8693,7 @@ msgstr "" msgid "The app will be restarted" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:119 +#: src/components/moderation/ModerationDetailsDialog.tsx:121 #: src/lib/moderation/useModerationCauseDescription.ts:128 msgid "The author of this thread has hidden this reply." msgstr "" @@ -8724,11 +8730,11 @@ msgstr "" msgid "The feed has been replaced with Discover." msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:59 +#: src/components/moderation/LabelsOnMeDialog.tsx:61 msgid "The following labels were applied to your account." msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:60 +#: src/components/moderation/LabelsOnMeDialog.tsx:62 msgid "The following labels were applied to your content." msgstr "" @@ -8915,7 +8921,7 @@ msgstr "" msgid "This action can be undone at any time." msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:271 +#: src/components/moderation/LabelsOnMeDialog.tsx:273 msgid "This appeal will be sent to <0>{sourceName}." msgstr "" @@ -8943,7 +8949,7 @@ msgstr "" msgid "This content is hosted by {0}. Do you want to enable external media?" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:84 +#: src/components/moderation/ModerationDetailsDialog.tsx:86 #: src/lib/moderation/useModerationCauseDescription.ts:84 msgid "This content is not available because one of the users involved has blocked the other." msgstr "" @@ -9007,11 +9013,11 @@ msgstr "" msgid "This is not a valid link" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:168 +#: src/components/moderation/ModerationDetailsDialog.tsx:170 msgid "This label was applied by the author." msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:165 +#: src/components/moderation/LabelsOnMeDialog.tsx:167 msgid "This label was applied by you." msgstr "" @@ -9095,7 +9101,7 @@ msgstr "" msgid "This user has blocked you" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:79 +#: src/components/moderation/ModerationDetailsDialog.tsx:81 #: src/lib/moderation/useModerationCauseDescription.ts:75 msgid "This user has blocked you. You cannot view their content." msgstr "" @@ -9104,11 +9110,11 @@ msgstr "" msgid "This user has requested that their content only be shown to signed-in users." msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:59 +#: src/components/moderation/ModerationDetailsDialog.tsx:61 msgid "This user is included in the <0>{0} list which you have blocked." msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:91 +#: src/components/moderation/ModerationDetailsDialog.tsx:93 msgid "This user is included in the <0>{0} list which you have muted." msgstr "" @@ -9240,7 +9246,7 @@ msgstr "" msgid "TV" msgstr "" -#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:65 +#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:64 msgid "Two-factor authentication (2FA)" msgstr "" @@ -9365,7 +9371,7 @@ msgstr "" msgid "Unfortunately, Bluesky is unavailable in Mississippi right now." msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:372 +#: src/components/moderation/ReportDialog/index.tsx:375 msgid "Unfortunately, none of your subscribed labelers supports this report type." msgstr "" @@ -9617,7 +9623,7 @@ msgctxt "toast" msgid "User blocked" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:71 +#: src/components/moderation/ModerationDetailsDialog.tsx:73 #: src/lib/moderation/useModerationCauseDescription.ts:63 msgid "User Blocked" msgstr "" @@ -9630,7 +9636,7 @@ msgstr "" msgid "User blocked by list" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:57 +#: src/components/moderation/ModerationDetailsDialog.tsx:59 msgid "User Blocked by List" msgstr "" @@ -9638,7 +9644,7 @@ msgstr "" msgid "User Blocking You" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:77 +#: src/components/moderation/ModerationDetailsDialog.tsx:79 msgid "User Blocks You" msgstr "" @@ -9852,7 +9858,7 @@ msgctxt "Action to view the post the user just created" msgid "View" msgstr "" -#: src/screens/Profile/Header/Shell.tsx:229 +#: src/screens/Profile/Header/Shell.tsx:241 msgid "View {0}'s avatar" msgstr "" @@ -10456,7 +10462,7 @@ msgstr "" msgid "You have blocked this user" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:73 +#: src/components/moderation/ModerationDetailsDialog.tsx:75 #: src/lib/moderation/useModerationCauseDescription.ts:57 #: src/lib/moderation/useModerationCauseDescription.ts:65 msgid "You have blocked this user. You cannot view their content." @@ -10476,11 +10482,11 @@ msgstr "" msgid "You have hidden this post" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:111 +#: src/components/moderation/ModerationDetailsDialog.tsx:113 msgid "You have hidden this post." msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:104 +#: src/components/moderation/ModerationDetailsDialog.tsx:106 #: src/lib/moderation/useModerationCauseDescription.ts:99 msgid "You have muted this account." msgstr "" @@ -10534,7 +10540,7 @@ msgstr "" msgid "You haven't muted any words or tags yet" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:118 +#: src/components/moderation/ModerationDetailsDialog.tsx:120 #: src/lib/moderation/useModerationCauseDescription.ts:127 msgid "You hid this reply." msgstr "" @@ -10551,11 +10557,11 @@ msgstr "" msgid "You joined Bluesky using a starter pack {timeAgoString} ago" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:79 +#: src/components/moderation/LabelsOnMeDialog.tsx:81 msgid "You may appeal non-self labels if you feel they were placed in error." msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:84 +#: src/components/moderation/LabelsOnMeDialog.tsx:86 msgid "You may appeal these labels if you feel they were placed in error." msgstr "" @@ -10694,7 +10700,7 @@ msgstr "" msgid "You're signed in with an App Password. Please sign in with your main password to continue deactivating your account." msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:108 +#: src/components/moderation/ModerationDetailsDialog.tsx:110 #: src/lib/moderation/useModerationCauseDescription.ts:108 msgid "You've chosen to hide a word or tag within this post." msgstr "" @@ -10881,7 +10887,7 @@ msgstr "" msgid "Your reply was sent" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:394 +#: src/components/moderation/ReportDialog/index.tsx:397 msgid "Your report will be sent to <0>{0}." msgstr "" From 1e6a44f2e87a5bdaafff9d2a086dd98db6034135 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 1 Oct 2025 16:09:12 +0300 Subject: [PATCH 28/29] Fix feedfeedback metrics not distinguishing which feed it's from (#9099) * fix feedfeedback metrics being sent for all feeds * remove `discover:` metrics --- src/logger/metrics.ts | 15 ++++++++++----- src/state/feed-feedback.tsx | 38 +++++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 1ba1cd3b50..f2c56d9638 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -175,19 +175,24 @@ export type MetricEvents = { 'feed:suggestion:press': { feedUrl: string } - 'discover:showMore': { + 'feed:showMore': { + feed: string feedContext: string } - 'discover:showLess': { + 'feed:showLess': { + feed: string feedContext: string } - 'discover:clickthrough': { + 'feed:clickthrough': { + feed: string count: number } - 'discover:engaged': { + 'feed:engaged': { + feed: string count: number } - 'discover:seen': { + 'feed:seen': { + feed: string count: number } diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index e56bdc2bd8..67ac77ba37 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -12,7 +12,6 @@ import throttle from 'lodash.throttle' import {PROD_FEEDS, STAGING_FEEDS} from '#/lib/constants' import {isNetworkError} from '#/lib/hooks/useCleanError' -import {logEvent} from '#/lib/statsig/statsig' import {Logger} from '#/logger' import { type FeedSourceFeedInfo, @@ -90,11 +89,19 @@ export function useFeedFeedback( const aggregatedStats = useRef(null) const throttledFlushAggregatedStats = useMemo( () => - throttle(() => flushToStatsig(aggregatedStats.current), 45e3, { - leading: true, // The outer call is already throttled somewhat. - trailing: true, - }), - [], + throttle( + () => + flushToStatsig( + aggregatedStats.current, + feed?.feedDescriptor ?? 'unknown', + ), + 45e3, + { + leading: true, // The outer call is already throttled somewhat. + trailing: true, + }, + ), + [feed?.feedDescriptor], ) const sendToFeedNoDelay = useCallback(() => { @@ -135,6 +142,7 @@ export function useFeedFeedback( sendOrAggregateInteractionsForStats( aggregatedStats.current, interactionsToSend, + feed?.feedDescriptor ?? 'unknown', ) throttledFlushAggregatedStats() logger.debug('flushed') @@ -271,19 +279,22 @@ function createAggregatedStats(): AggregatedStats { function sendOrAggregateInteractionsForStats( stats: AggregatedStats, interactions: AppBskyFeedDefs.Interaction[], + feed: string, ) { for (let interaction of interactions) { switch (interaction.event) { // Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them. // This lets us send the feed context together with them. case 'app.bsky.feed.defs#requestLess': { - logEvent('discover:showLess', { + logger.metric('feed:showLess', { + feed, feedContext: interaction.feedContext ?? '', }) break } case 'app.bsky.feed.defs#requestMore': { - logEvent('discover:showMore', { + logger.metric('feed:showMore', { + feed, feedContext: interaction.feedContext ?? '', }) break @@ -313,28 +324,31 @@ function sendOrAggregateInteractionsForStats( } } -function flushToStatsig(stats: AggregatedStats | null) { +function flushToStatsig(stats: AggregatedStats | null, feedDescriptor: string) { if (stats === null) { return } if (stats.clickthroughCount > 0) { - logEvent('discover:clickthrough', { + logger.metric('feed:clickthrough', { count: stats.clickthroughCount, + feed: feedDescriptor, }) stats.clickthroughCount = 0 } if (stats.engagedCount > 0) { - logEvent('discover:engaged', { + logger.metric('feed:engaged', { count: stats.engagedCount, + feed: feedDescriptor, }) stats.engagedCount = 0 } if (stats.seenCount > 0) { - logEvent('discover:seen', { + logger.metric('feed:seen', { count: stats.seenCount, + feed: feedDescriptor, }) stats.seenCount = 0 } From 5fd52b3d300fa2a7892a3235c48be69ec75a0324 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 1 Oct 2025 13:49:06 -0500 Subject: [PATCH 29/29] add patent pledge link to readme (#9118) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index eed6ac4fec..ae4511cc3b 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ Bluesky is an open social network built on the AT Protocol, a flexible technolog See [./LICENSE](./LICENSE) for the full license. +Bluesky Social PBC has committed to a software patent non-aggression pledge. For details see [the original announcement](https://bsky.social/about/blog/10-01-2025-patent-pledge). + ## P.S. We ❤️ you and all of the ways you support us. Thank you for making Bluesky a great place!