From bdca7622ce507333dff94ca4598116ee77a6f1db Mon Sep 17 00:00:00 2001 From: "xan.lol" Date: Sat, 4 Jul 2026 04:41:43 -0700 Subject: [PATCH] custom post TID suffix setting --- src/lib/api/index.ts | 17 ++- .../Settings/RunesSettings/ExtraSettings.tsx | 106 ++++++++++++++++++ src/state/persisted/schema.ts | 2 + src/state/preferences/index.tsx | 10 +- src/state/preferences/settings-sync.tsx | 1 + src/state/preferences/tid-suffix.tsx | 52 +++++++++ src/view/com/composer/Composer.tsx | 3 + 7 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 src/state/preferences/tid-suffix.tsx diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 340afc0e2..6f2a44055 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -59,6 +59,21 @@ interface PostOpts { onStateChange?: (state: string) => void langs?: string[] omitViaField?: boolean + tidSuffix?: string +} + +const MAX_TID_SUFFIX_LEN = 6 + +function nextPostTid(prev: TID | undefined, tidSuffix?: string): TID { + if (prev) { + return TID.next(prev) + } + if (!tidSuffix) { + return TID.next(prev) + } + const suffix = tidSuffix.slice(0, MAX_TID_SUFFIX_LEN) + const next = TID.next(prev) + return TID.fromStr(next.toString().slice(0, -suffix.length) + suffix) } export async function post( @@ -118,7 +133,7 @@ export async function post( // The sorting behavior for multiple posts sharing the same createdAt time is // undefined, so what we'll do here is increment the time by 1 for every post now.setMilliseconds(now.getMilliseconds() + 1) - tid = TID.next(tid) + tid = nextPostTid(tid, opts.tidSuffix) const rkey = tid.toString() const uri = `at://${did}/app.bsky.feed.post/${rkey}` uris.push(uri) diff --git a/src/screens/Settings/RunesSettings/ExtraSettings.tsx b/src/screens/Settings/RunesSettings/ExtraSettings.tsx index 183cee1e3..60daa12e9 100644 --- a/src/screens/Settings/RunesSettings/ExtraSettings.tsx +++ b/src/screens/Settings/RunesSettings/ExtraSettings.tsx @@ -1,5 +1,8 @@ +import {useState} from 'react' +import {View} from 'react-native' import {Trans, useLingui} from '@lingui/react/macro' +import {usePalette} from '#/lib/hooks/usePalette' import { useAutoLikeOnRepost, useSetAutoLikeOnRepost, @@ -20,15 +23,21 @@ import { useOmitViaField, useSetOmitViaField, } from '#/state/preferences/omit-via-field' +import {useTidSuffix, useSetTidSuffix} from '#/state/preferences/tid-suffix' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a} from '#/alf' import {Admonition} from '#/components/Admonition' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' import * as Toggle from '#/components/forms/Toggle' import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging' import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets' import {Explosion_Stroke2_Corner0_Rounded as ExplosionIcon} from '#/components/icons/Explosion' import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' import {LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon} from '#/components/icons/Heart2' +import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons/Hashtag' +import {Text} from '#/components/Typography' +import {IS_WEB} from '#/env' import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' import {useDevMode} from '#/storage/hooks/dev-mode' import {RunesScreenLayout} from './components/RunesScreenLayout' @@ -50,6 +59,8 @@ export function RunesExtraSettingsScreen() { const omitViaField = useOmitViaField() const setOmitViaField = useSetOmitViaField() + const tidSuffix = useTidSuffix() + const setTidSuffixControl = Dialog.useDialogControl() const [devMode, setDevMode] = useDevMode() return ( @@ -127,6 +138,25 @@ export function RunesExtraSettingsScreen() { + + + + Custom TID suffix for posts + + setTidSuffixControl.open()} + /> + + + + + Replace the last characters of the first post in a thread's record + key with a custom suffix (up to 6 characters). Current:  + {tidSuffix || l`none`} + + + Feature gates + ) } + +const MAX_TID_SUFFIX_LEN = 6 + +function TidSuffixDialog({control}: {control: Dialog.DialogControlProps}) { + const pal = usePalette('default') + const {t: l} = useLingui() + + const tidSuffix = useTidSuffix() + const [suffix, setSuffix] = useState(tidSuffix ?? '') + const setTidSuffix = useSetTidSuffix() + const isReset = suffix.trim().length === 0 + + const submit = () => { + const trimmedSuffix = suffix.trim().slice(0, MAX_TID_SUFFIX_LEN) + control.close(() => { + setTidSuffix(trimmedSuffix || undefined) + }) + } + + return ( + setSuffix(tidSuffix ?? '')}> + + + + + Custom TID suffix + + + + + setSuffix(text.slice(0, MAX_TID_SUFFIX_LEN))} + placeholder={l`e.g. abc`} + placeholderTextColor={pal.colors.textLight} + onSubmitEditing={submit} + accessibilityHint={l`Input up to 6 characters to use as a TID suffix`} + defaultValue={tidSuffix} + maxLength={MAX_TID_SUFFIX_LEN} + /> + + + + + + + + + + ) +} + +const styles = { + textInput: { + borderWidth: 1, + borderRadius: 8, + paddingHorizontal: 14, + paddingVertical: 10, + fontSize: 16, + }, +} diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 13f3c5a6a..c012afdd6 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -281,6 +281,7 @@ const schema = z.object({ autoLikeOnRepost: z.boolean().optional(), omitViaField: z.boolean().optional(), + tidSuffix: z.string().optional(), // settings sync settingsSyncEnabled: z.boolean().optional(), @@ -417,6 +418,7 @@ export const defaults: Schema = { autoLikeOnRepost: false, omitViaField: false, + tidSuffix: undefined, settingsSyncEnabled: false, settingsSyncDraftId: undefined, diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index 5ae805ea4..e2a3ff6fa 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -42,6 +42,7 @@ import {Provider as LoadSmallPNGsProvider} from './load-small-pngs' import {MetricsDisplayPreferencesProvider} from './metrics-display-preference' import {Provider as NoDiscoverProvider} from './no-discover-fallback' import {Provider as OmitViaFieldProvider} from './omit-via-field' +import {Provider as TidSuffixProvider} from './tid-suffix' import {Provider as OpenRouterProvider} from './openrouter' import {Provider as PdsLabelProvider} from './pds-label' import {Provider as PlcDirectoryProvider} from './plc-directory' @@ -111,6 +112,7 @@ export {useImageCdnHost, useSetImageCdnHost} from './image-cdn-host' export {useLabelDefinitions} from './label-defs' export {useLanguagePrefs, useLanguagePrefsApi} from './languages' export {useOmitViaField, useSetOmitViaField} from './omit-via-field' +export {useTidSuffix, useSetTidSuffix} from './tid-suffix' export { useOpenRouterApiKey, useOpenRouterConfigured, @@ -203,9 +205,11 @@ export function Provider({children}: PropsWithChildren<{}>) { - { - children - } + + { + children + } + diff --git a/src/state/preferences/settings-sync.tsx b/src/state/preferences/settings-sync.tsx index 6e31713e8..e12183780 100644 --- a/src/state/preferences/settings-sync.tsx +++ b/src/state/preferences/settings-sync.tsx @@ -96,6 +96,7 @@ export const SYNCED_PREFS_KEYS = [ 'trendingVideoDisabled', 'autoLikeOnRepost', 'omitViaField', + 'tidSuffix', 'syncOpenRouterApiKey', ] as const satisfies readonly (keyof Schema)[] diff --git a/src/state/preferences/tid-suffix.tsx b/src/state/preferences/tid-suffix.tsx new file mode 100644 index 000000000..d7ce7a84f --- /dev/null +++ b/src/state/preferences/tid-suffix.tsx @@ -0,0 +1,52 @@ +import { + createContext, + type PropsWithChildren, + useCallback, + useContext, + useEffect, + useState, +} from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['tidSuffix'] +type SetContext = (v: persisted.Schema['tidSuffix']) => void + +const stateContext = createContext(persisted.defaults.tidSuffix) +const setContext = createContext( + (_: persisted.Schema['tidSuffix']) => {}, +) + +export function Provider({children}: PropsWithChildren<{}>) { + const [state, setState] = useState(persisted.get('tidSuffix')) + + const setStateWrapped = useCallback( + (value: persisted.Schema['tidSuffix']) => { + setState(value) + persisted.write('tidSuffix', value) + }, + [setState], + ) + + useEffect(() => { + return persisted.onUpdate('tidSuffix', next => { + setState(next) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useTidSuffix() { + return useContext(stateContext) +} + +export function useSetTidSuffix() { + return useContext(setContext) +} diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 790a7bb08..e39374d28 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -103,6 +103,7 @@ import { useLanguagePrefsApi, } from '#/state/preferences/languages' import {useOmitViaField} from '#/state/preferences/omit-via-field' +import {useTidSuffix} from '#/state/preferences/tid-suffix' import { useOpenRouterApiKey, useOpenRouterConfigured, @@ -310,6 +311,7 @@ export const ComposePost = ({ const {t: l, i18n} = useLingui() const requireAltTextEnabled = useRequireAltTextEnabled() const omitViaField = useOmitViaField() + const tidSuffix = useTidSuffix() const langPrefs = useLanguagePrefs() const setLangPrefs = useLanguagePrefsApi() const textInputRef = useRef(null) @@ -1173,6 +1175,7 @@ export const ComposePost = ({ onStateChange: setPublishingStage, langs: currentLanguages, omitViaField, + tidSuffix, }) ).uris[0] -- 2.51.2