From d5bdcf84f80034ec41b19de09bdd9a73a519e89f Mon Sep 17 00:00:00 2001 From: xan.lol Date: Wed, 28 Jan 2026 07:30:17 +0000 Subject: [PATCH] MAKE CUSTOM POST PHRASE FEATURE WORK!!! And changed some defaults (deer settings fixes) --- README.md | 18 +++++++++--------- src/locale/i18n.ts | 6 +++--- src/locale/i18n.web.ts | 8 ++++---- src/locale/linguiHook.ts | 19 ++++++++++++++----- src/screens/Settings/DeerSettings.tsx | 228 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------------- src/state/persisted/schema.ts | 10 ++++++---- src/state/preferences/post-name-replacement.tsx | 27 ++++++++++++++------------- 7 file(s) changed, 202 insertion(s)(+), 114 deletion(s)(-) diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -16,11 +16,11 @@ ## Improvements on Bluesky - Cooler name (and kawaii logo) - Color scheme options and hue slider (defaults to Witchsky orange) -- Posts are called Skeets (may let you choose in the future) +- You can change - Choose between sharing witchsky.app or bsky.app links - Embed player works with [stream.place](https://stream.place/) links! -- Open skeets in PDSls and original pages of bridged posts -- You can redraft skeets +- Open posts in PDSls and original pages of bridged posts +- You can redraft posts - Better defaults (alt text required 😉 autoplay off 🫨) - More unique repost icons - Can download videos @@ -33,7 +33,7 @@ These are all available as options in a sub-page of the app's settings. - Toggle go.bsky.app link proxying for analytics -- Toggle to see skeets in quotes through blocks and detachments +- Toggle to see posts in quotes through blocks and detachments - Toggle for buttons to show original fedi posts and in PDSls - Toggle to trust your own preferred verifiers (and to operate as one yourself) - Toggle to change Constellation instance for custom features @@ -42,11 +42,11 @@ #### Tweaks - Toggle to turn non-bsky.social handles into clickable links -- Toggle to combine reskeets in horizontal carousels +- Toggle to combine reposts in horizontal carousels - Toggle the following feed fallback to the discover feed - Toggle displaying images in higher quality - Toggle to only show a single tab if only one feed is pinned -- Toggle to prevent others from getting notified when you interact with their reskeets +- Toggle to prevent others from getting notified when you interact with their reposts - Toggle similar account recommendations - Toggle to make all user avatars square (like labelers) - Toggle for more square-ish UI (still slightly rounded) @@ -58,7 +58,7 @@ You can completely disable the visiblity of all metrics individually, including the number of: - likes -- reskeets +- reposts - quotes - saves - replies @@ -68,7 +68,7 @@ - & who someone's followed by ## Upcoming or wishful features -- Better OpenGraph support for sharing profiles & skeets (including videos & fixing quotes) +- Better OpenGraph support for sharing profiles & posts (including videos & fixing quotes) - Selecting a custom AppView - Seeing past blocks in threads (the nuclear block in reply chains) - Configure the location used to determine regional labelers @@ -115,7 +115,7 @@ ## Contributions > Witchsky is a community fork, and we'd love to merge your PR! -As a rule of thumb, the best features for Witchsky are those that have a disproportionately positive impact on the user experience compared to the maintenance overhead. Unlike some open source projects, since Witchsky is a soft fork, any features (patches) we add on top of upstream social-app need to be maintained. For example, a change to the way skeets are composed may be very invasive, touching lots of code across the codebase. If upstream refactors this component, we will need to rewrite this feature to be compatible or drop it from the client. +As a rule of thumb, the best features for Witchsky are those that have a disproportionately positive impact on the user experience compared to the maintenance overhead. Unlike some open source projects, since Witchsky is a soft fork, any features (patches) we add on top of upstream social-app need to be maintained. For example, a change to the way posts are composed may be very invasive, touching lots of code across the codebase. If upstream refactors this component, we will need to rewrite this feature to be compatible or drop it from the client. For this reason, only features that require changing only a small amount of code from upstream should be considered. diff --git a/src/locale/i18n.ts b/src/locale/i18n.ts --- a/src/locale/i18n.ts +++ b/src/locale/i18n.ts @@ -13,7 +13,7 @@ import {i18n} from '@lingui/core' import {sanitizeAppLanguageSetting} from '#/locale/helpers' import {AppLanguage} from '#/locale/languages' -import {applySkeetReplacements} from '#/locale/linguiHook' +import {applyPostReplacements} from '#/locale/linguiHook' import {messages as messagesAn} from '#/locale/locales/an/messages' import {messages as messagesAst} from '#/locale/locales/ast/messages' import {messages as messagesCa} from '#/locale/locales/ca/messages' @@ -126,7 +126,7 @@ ]) break } case AppLanguage.en_GB: { - const transformedMsgs = applySkeetReplacements(messagesEn_GB, locale) + const transformedMsgs = applyPostReplacements(messagesEn_GB, locale) i18n.loadAndActivate({locale, messages: transformedMsgs}) await Promise.all([ import('@formatjs/intl-pluralrules/locale-data/en'), @@ -424,7 +424,7 @@ ]) break } default: { - const transformedMsgs = applySkeetReplacements(messagesEn, locale) + const transformedMsgs = applyPostReplacements(messagesEn, locale) i18n.loadAndActivate({locale, messages: transformedMsgs}) break } diff --git a/src/locale/i18n.web.ts b/src/locale/i18n.web.ts --- a/src/locale/i18n.web.ts +++ b/src/locale/i18n.web.ts @@ -3,7 +3,7 @@ import {i18n, type Messages} from '@lingui/core' import {sanitizeAppLanguageSetting} from '#/locale/helpers' import {AppLanguage} from '#/locale/languages' -import {applySkeetReplacements} from '#/locale/linguiHook' +import {applyPostReplacements} from '#/locale/linguiHook' import {useLanguagePrefs} from '#/state/preferences' /** @@ -43,14 +43,14 @@ break } case AppLanguage.en: { mod = await import(`./locales/en/messages`) - const transformedEnMessages = applySkeetReplacements(mod.messages, locale) + const transformedEnMessages = applyPostReplacements(mod.messages, locale) i18n.load(locale, transformedEnMessages) i18n.activate(locale) break } case AppLanguage.en_GB: { mod = await import(`./locales/en-GB/messages`) - const transformedEnGbMessages = applySkeetReplacements( + const transformedEnGbMessages = applyPostReplacements( mod.messages, locale, ) @@ -188,7 +188,7 @@ break } default: { mod = await import(`./locales/en/messages`) - const transformedDefaultMessages = applySkeetReplacements( + const transformedDefaultMessages = applyPostReplacements( mod.messages, locale, ) diff --git a/src/locale/linguiHook.ts b/src/locale/linguiHook.ts --- a/src/locale/linguiHook.ts +++ b/src/locale/linguiHook.ts @@ -4,12 +4,21 @@ import * as persisted from '#/state/persisted' // Helper to apply the replacement to a single string function replaceInString(text: string): string { - const {string: replacement, enabled} = persisted.get('postReplacement') + const {postName, postsName, enabled} = persisted.get('postReplacement') if (!enabled) return text - let repl = replacement?.length ? replacement.toLowerCase() : 'skeet' + + const singular = postName?.length ? postName : 'skeet' + const plural = postsName?.length ? postsName : 'skeets' + + // Capitalize first letter for proper noun replacements + const singularCapitalized = singular[0].toUpperCase() + singular.slice(1) + const pluralCapitalized = plural[0].toUpperCase() + plural.slice(1) + return text - .replaceAll('Post', repl[0].toUpperCase() + repl.slice(1)) - .replaceAll('post', repl) + .replaceAll('Posts', pluralCapitalized) + .replaceAll('posts', plural) + .replaceAll('Post', singularCapitalized) + .replaceAll('post', singular) } // Recursive helper to traverse and replace strings in nested structures @@ -40,7 +49,7 @@ * @param locale The current locale string. * @returns The messages object with replacements applied if the locale is English, * otherwise the original messages object. */ -export function applySkeetReplacements( +export function applyPostReplacements( messages: Messages, locale: string, ): Messages { diff --git a/src/screens/Settings/DeerSettings.tsx b/src/screens/Settings/DeerSettings.tsx --- a/src/screens/Settings/DeerSettings.tsx +++ b/src/screens/Settings/DeerSettings.tsx @@ -7,6 +7,9 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {usePalette} from '#/lib/hooks/usePalette' import {type CommonNavigatorParams} from '#/lib/routes/types' +import {dynamicActivate} from '#/locale/i18n' +import {dynamicActivate as dynamicActivateWeb} from '#/locale/i18n.web' +import {type AppLanguage} from '#/locale/languages' import * as persisted from '#/state/persisted' import {useGoLinksEnabled, useSetGoLinksEnabled} from '#/state/preferences' import { @@ -123,7 +126,6 @@ import {atoms as a, useBreakpoints} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import * as TextField from '#/components/forms/TextField' import * as Toggle from '#/components/forms/Toggle' import {Atom_Stroke2_Corner0_Rounded as DeerIcon} from '#/components/icons/Atom' import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' @@ -295,6 +297,142 @@ ) } +function PostReplacementDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const pal = usePalette('default') + const {_, i18n} = useLingui() + + const postReplacement = usePostReplacement() + const setPostReplacement = useSetPostReplacement() + + const [singular, setSingular] = useState(postReplacement.postName) + const [plural, setPlural] = useState(postReplacement.postsName) + const [pluralManuallyEdited, setPluralManuallyEdited] = useState(false) + + const submit = async () => { + setPostReplacement({ + enabled: singular.trim().toLowerCase() !== 'post', + postName: singular, + postsName: plural, + }) + + // Force reload the i18n messages to apply the replacement immediately + const locale = i18n.locale + await (IS_WEB + ? dynamicActivateWeb(locale as AppLanguage) + : dynamicActivate(locale as AppLanguage)) + + control.close() + } + + const handleSingularChange = (value: string) => { + setSingular(value) + if (!pluralManuallyEdited) { + setPlural(value + 's') + } + } + + const handlePluralChange = (value: string) => { + setPlural(value) + setPluralManuallyEdited(true) + } + + const handlePresetSelect = (singularForm: string, pluralForm: string) => { + setSingular(singularForm) + setPlural(pluralForm) + setPluralManuallyEdited(false) + } + + const shouldDisable = () => { + return !singular.trim() || !plural.trim() + } + + return ( + { + setSingular(postReplacement.postName) + setPlural(postReplacement.postsName) + setPluralManuallyEdited(false) + }}> + + + + + Custom post phrase + + + + + + + + {[ + {singular: 'post', plural: 'posts'}, + {singular: 'skeet', plural: 'skeets'}, + {singular: 'note', plural: 'notes'}, + {singular: 'woot', plural: 'woots'}, + {singular: 'toot', plural: 'toots'}, + {singular: 'silly', plural: 'sillies'}, + ].map(preset => ( + + ))} + + + + + + + + + + + + + ) +} + function TrustedVerifiersDialog({ control, }: { @@ -431,8 +569,7 @@ const setTranslationServicePreference = useSetTranslationServicePreference() const setLibreTranslateInstanceControl = Dialog.useDialogControl() - const postReplacement = usePostReplacement() - const setPostReplacement = useSetPostReplacement() + const setPostReplacementDialogControl = Dialog.useDialogControl() return ( @@ -584,58 +721,18 @@ - + - - Call posts{' '} - {postReplacement.string.length - ? postReplacement.string.toLowerCase() - : 'skeet'} - s - + {`Custom post phrase`} - - setPostReplacement({ - enabled: value, - string: postReplacement.string, - }) - } - style={[a.w_full]}> - - - Changes post to another word of your choosing. Requires a - refresh to update. - - - - + setPostReplacementDialogControl.open()} + /> + - {postReplacement.enabled && ( - - - - setPostReplacement( - (curr: {enabled: boolean; string: string}) => ({ - ...curr, - string: value, - }), - ) - } - /> - - - )} - + @@ -653,17 +750,7 @@ Combine reposts into a horizontal carousel - setNoDiscoverFallback(value)} - style={[a.w_full]}> - - Do not fall back to discover feed - - - + On non-bsky.social handles, show a link to that URL - - - - - setRepostCarouselEnabled(value)} - style={[a.w_full]}> - - Combine reposts into a horizontal carousel @@ -908,12 +983,12 @@ setDisableRepostsMetrics(value)} style={[a.w_full]}> - Disable Reposts Metrics + Disable reposts metrics @@ -1053,6 +1128,7 @@ + ) } 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 @@ -172,7 +172,8 @@ hideUnreplyablePosts: z.boolean().optional(), postReplacement: z.object({ enabled: z.boolean().optional(), - string: z.string().optional(), + postName: z.string().optional(), + postsName: z.string().optional(), }), showExternalShareButtons: z.boolean().optional(), @@ -218,7 +219,7 @@ deviceLanguageCodes[0], ]), }, requireAltTextEnabled: true, - largeAltBadgeEnabled: true, + largeAltBadgeEnabled: false, externalEmbeds: {}, mutedThreads: [], invites: { @@ -242,7 +243,7 @@ // deer goLinksEnabled: true, constellationEnabled: true, - directFetchRecords: false, + directFetchRecords: true, noAppLabelers: false, noDiscoverFallback: false, repostCarouselEnabled: false, @@ -299,7 +300,8 @@ libreTranslateInstance: 'https://libretranslate.com/', postReplacement: { enabled: false, - string: 'skeet', + postName: 'skeet', + postsName: 'skeets', }, } diff --git a/src/state/preferences/post-name-replacement.tsx b/src/state/preferences/post-name-replacement.tsx --- a/src/state/preferences/post-name-replacement.tsx +++ b/src/state/preferences/post-name-replacement.tsx @@ -4,7 +4,8 @@ import * as persisted from '#/state/persisted' interface PostReplacementState { enabled: boolean - string: string + postName: string + postsName: string } type StateContext = PostReplacementState @@ -31,8 +32,12 @@ const persistedState = persisted.get('postReplacement') return { enabled: persistedState?.enabled ?? persisted.defaults.postReplacement.enabled!, - string: - persistedState?.string ?? persisted.defaults.postReplacement.string!, + postName: + persistedState?.postName ?? + persisted.defaults.postReplacement.postName!, + postsName: + persistedState?.postsName ?? + persisted.defaults.postReplacement.postsName!, } }) @@ -53,17 +58,13 @@ ) React.useEffect(() => { return persisted.onUpdate('postReplacement', next => { - setState({string: next.string ?? 'skeet', enabled: next.enabled ?? true}) - /* - if (nextVal) { - _setState({ - enabled: - nextVal.enabled ?? persisted.defaults.postReplacement.enabled!, - string: nextVal.string ?? persisted.defaults.postReplacement.string!, - }) - }*/ + setState({ + postName: next.postName ?? 'skeet', + postsName: next.postsName ?? 'skeets', + enabled: next.enabled ?? true, + }) }) - }, []) + }, [setState]) return ( -- tangled.sh