diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -59,6 +59,21 @@
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 @@
// 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/state/persisted/schema.ts b/src/state/persisted/schema.ts
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -281,6 +281,7 @@
autoLikeOnRepost: z.boolean().optional(),
omitViaField: z.boolean().optional(),
+ tidSuffix: z.string().optional(),
// settings sync
settingsSyncEnabled: z.boolean().optional(),
@@ -417,6 +418,7 @@
autoLikeOnRepost: false,
omitViaField: false,
+ tidSuffix: undefined,
settingsSyncEnabled: false,
settingsSyncDraftId: undefined,
diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx
--- a/src/state/preferences/index.tsx
+++ b/src/state/preferences/index.tsx
@@ -42,6 +42,7 @@
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 {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 @@
- {
- children
- }
+
+ {
+ children
+ }
+
diff --git a/src/state/preferences/settings-sync.tsx b/src/state/preferences/settings-sync.tsx
--- a/src/state/preferences/settings-sync.tsx
+++ b/src/state/preferences/settings-sync.tsx
@@ -96,6 +96,7 @@
'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
--- /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/screens/Settings/RunesSettings/ExtraSettings.tsx b/src/screens/Settings/RunesSettings/ExtraSettings.tsx
--- 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 @@
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 @@
const omitViaField = useOmitViaField()
const setOmitViaField = useSetOmitViaField()
+ const tidSuffix = useTidSuffix()
+ const setTidSuffixControl = Dialog.useDialogControl()
const [devMode, setDevMode] = useDevMode()
return (
@@ -127,6 +138,25 @@
+
+
+
+ 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/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -103,6 +103,7 @@
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 @@
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 @@
onStateChange: setPublishingStage,
langs: currentLanguages,
omitViaField,
+ tidSuffix,
})
).uris[0]