···1617- Cooler name (and kawaii logo)
18- Color scheme options and hue slider (defaults to Witchsky orange)
19-- Posts are called Skeets (may let you choose in the future)
20- Choose between sharing witchsky.app or bsky.app links
21- Embed player works with [stream.place](https://stream.place/) links!
22-- Open skeets in PDSls and original pages of bridged posts
23-- You can redraft skeets
24- Better defaults (alt text required 😉 autoplay off 🫨)
25- More unique repost icons
26- Can download videos
···33These are all available as options in a sub-page of the app's settings.
3435- Toggle go.bsky.app link proxying for analytics
36-- Toggle to see skeets in quotes through blocks and detachments
37- Toggle for buttons to show original fedi posts and in PDSls
38- Toggle to trust your own preferred verifiers (and to operate as one yourself)
39- Toggle to change Constellation instance for custom features
···42#### Tweaks
4344- Toggle to turn non-bsky.social handles into clickable links
45-- Toggle to combine reskeets in horizontal carousels
46- Toggle the following feed fallback to the discover feed
47- Toggle displaying images in higher quality
48- Toggle to only show a single tab if only one feed is pinned
49-- Toggle to prevent others from getting notified when you interact with their reskeets
50- Toggle similar account recommendations
51- Toggle to make all user avatars square (like labelers)
52- Toggle for more square-ish UI (still slightly rounded)
···58You can completely disable the visiblity of all metrics individually, including the number of:
5960- likes
61-- reskeets
62- quotes
63- saves
64- replies
···6869## Upcoming or wishful features
7071-- Better OpenGraph support for sharing profiles & skeets (including videos & fixing quotes)
72- Selecting a custom AppView
73- Seeing past blocks in threads (the nuclear block in reply chains)
74- Configure the location used to determine regional labelers
···115116> Witchsky is a community fork, and we'd love to merge your PR!
117118-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.
119120For this reason, only features that require changing only a small amount of code from upstream should be considered.
121
···1617- Cooler name (and kawaii logo)
18- Color scheme options and hue slider (defaults to Witchsky orange)
19+- You can change
20- Choose between sharing witchsky.app or bsky.app links
21- Embed player works with [stream.place](https://stream.place/) links!
22+- Open posts in PDSls and original pages of bridged posts
23+- You can redraft posts
24- Better defaults (alt text required 😉 autoplay off 🫨)
25- More unique repost icons
26- Can download videos
···33These are all available as options in a sub-page of the app's settings.
3435- Toggle go.bsky.app link proxying for analytics
36+- Toggle to see posts in quotes through blocks and detachments
37- Toggle for buttons to show original fedi posts and in PDSls
38- Toggle to trust your own preferred verifiers (and to operate as one yourself)
39- Toggle to change Constellation instance for custom features
···42#### Tweaks
4344- Toggle to turn non-bsky.social handles into clickable links
45+- Toggle to combine reposts in horizontal carousels
46- Toggle the following feed fallback to the discover feed
47- Toggle displaying images in higher quality
48- Toggle to only show a single tab if only one feed is pinned
49+- Toggle to prevent others from getting notified when you interact with their reposts
50- Toggle similar account recommendations
51- Toggle to make all user avatars square (like labelers)
52- Toggle for more square-ish UI (still slightly rounded)
···58You can completely disable the visiblity of all metrics individually, including the number of:
5960- likes
61+- reposts
62- quotes
63- saves
64- replies
···6869## Upcoming or wishful features
7071+- Better OpenGraph support for sharing profiles & posts (including videos & fixing quotes)
72- Selecting a custom AppView
73- Seeing past blocks in threads (the nuclear block in reply chains)
74- Configure the location used to determine regional labelers
···115116> Witchsky is a community fork, and we'd love to merge your PR!
117118+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.
119120For this reason, only features that require changing only a small amount of code from upstream should be considered.
121
+3-3
src/locale/i18n.ts
···1314import {sanitizeAppLanguageSetting} from '#/locale/helpers'
15import {AppLanguage} from '#/locale/languages'
16-import {applySkeetReplacements} from '#/locale/linguiHook'
17import {messages as messagesAn} from '#/locale/locales/an/messages'
18import {messages as messagesAst} from '#/locale/locales/ast/messages'
19import {messages as messagesCa} from '#/locale/locales/ca/messages'
···126 break
127 }
128 case AppLanguage.en_GB: {
129- const transformedMsgs = applySkeetReplacements(messagesEn_GB, locale)
130 i18n.loadAndActivate({locale, messages: transformedMsgs})
131 await Promise.all([
132 import('@formatjs/intl-pluralrules/locale-data/en'),
···424 break
425 }
426 default: {
427- const transformedMsgs = applySkeetReplacements(messagesEn, locale)
428 i18n.loadAndActivate({locale, messages: transformedMsgs})
429 break
430 }
···1314import {sanitizeAppLanguageSetting} from '#/locale/helpers'
15import {AppLanguage} from '#/locale/languages'
16+import {applyPostReplacements} from '#/locale/linguiHook'
17import {messages as messagesAn} from '#/locale/locales/an/messages'
18import {messages as messagesAst} from '#/locale/locales/ast/messages'
19import {messages as messagesCa} from '#/locale/locales/ca/messages'
···126 break
127 }
128 case AppLanguage.en_GB: {
129+ const transformedMsgs = applyPostReplacements(messagesEn_GB, locale)
130 i18n.loadAndActivate({locale, messages: transformedMsgs})
131 await Promise.all([
132 import('@formatjs/intl-pluralrules/locale-data/en'),
···424 break
425 }
426 default: {
427+ const transformedMsgs = applyPostReplacements(messagesEn, locale)
428 i18n.loadAndActivate({locale, messages: transformedMsgs})
429 break
430 }
+4-4
src/locale/i18n.web.ts
···34import {sanitizeAppLanguageSetting} from '#/locale/helpers'
5import {AppLanguage} from '#/locale/languages'
6-import {applySkeetReplacements} from '#/locale/linguiHook'
7import {useLanguagePrefs} from '#/state/preferences'
89/**
···43 }
44 case AppLanguage.en: {
45 mod = await import(`./locales/en/messages`)
46- const transformedEnMessages = applySkeetReplacements(mod.messages, locale)
47 i18n.load(locale, transformedEnMessages)
48 i18n.activate(locale)
49 break
50 }
51 case AppLanguage.en_GB: {
52 mod = await import(`./locales/en-GB/messages`)
53- const transformedEnGbMessages = applySkeetReplacements(
54 mod.messages,
55 locale,
56 )
···188 }
189 default: {
190 mod = await import(`./locales/en/messages`)
191- const transformedDefaultMessages = applySkeetReplacements(
192 mod.messages,
193 locale,
194 )
···34import {sanitizeAppLanguageSetting} from '#/locale/helpers'
5import {AppLanguage} from '#/locale/languages'
6+import {applyPostReplacements} from '#/locale/linguiHook'
7import {useLanguagePrefs} from '#/state/preferences'
89/**
···43 }
44 case AppLanguage.en: {
45 mod = await import(`./locales/en/messages`)
46+ const transformedEnMessages = applyPostReplacements(mod.messages, locale)
47 i18n.load(locale, transformedEnMessages)
48 i18n.activate(locale)
49 break
50 }
51 case AppLanguage.en_GB: {
52 mod = await import(`./locales/en-GB/messages`)
53+ const transformedEnGbMessages = applyPostReplacements(
54 mod.messages,
55 locale,
56 )
···188 }
189 default: {
190 mod = await import(`./locales/en/messages`)
191+ const transformedDefaultMessages = applyPostReplacements(
192 mod.messages,
193 locale,
194 )
+14-5
src/locale/linguiHook.ts
···45// Helper to apply the replacement to a single string
6function replaceInString(text: string): string {
7- const {string: replacement, enabled} = persisted.get('postReplacement')
8 if (!enabled) return text
9- let repl = replacement?.length ? replacement.toLowerCase() : 'skeet'
000000010 return text
11- .replaceAll('Post', repl[0].toUpperCase() + repl.slice(1))
12- .replaceAll('post', repl)
0013}
1415// Recursive helper to traverse and replace strings in nested structures
···40 * @returns The messages object with replacements applied if the locale is English,
41 * otherwise the original messages object.
42 */
43-export function applySkeetReplacements(
44 messages: Messages,
45 locale: string,
46): Messages {
···45// Helper to apply the replacement to a single string
6function replaceInString(text: string): string {
7+ const {postName, postsName, enabled} = persisted.get('postReplacement')
8 if (!enabled) return text
9+10+ const singular = postName?.length ? postName : 'skeet'
11+ const plural = postsName?.length ? postsName : 'skeets'
12+13+ // Capitalize first letter for proper noun replacements
14+ const singularCapitalized = singular[0].toUpperCase() + singular.slice(1)
15+ const pluralCapitalized = plural[0].toUpperCase() + plural.slice(1)
16+17 return text
18+ .replaceAll('Posts', pluralCapitalized)
19+ .replaceAll('posts', plural)
20+ .replaceAll('Post', singularCapitalized)
21+ .replaceAll('post', singular)
22}
2324// Recursive helper to traverse and replace strings in nested structures
···49 * @returns The messages object with replacements applied if the locale is English,
50 * otherwise the original messages object.
51 */
52+export function applyPostReplacements(
53 messages: Messages,
54 locale: string,
55): Messages {
+152-76
src/screens/Settings/DeerSettings.tsx
···78import {usePalette} from '#/lib/hooks/usePalette'
9import {type CommonNavigatorParams} from '#/lib/routes/types'
00010import * as persisted from '#/state/persisted'
11import {useGoLinksEnabled, useSetGoLinksEnabled} from '#/state/preferences'
12import {
···123import {Admonition} from '#/components/Admonition'
124import {Button, ButtonText} from '#/components/Button'
125import * as Dialog from '#/components/Dialog'
126-import * as TextField from '#/components/forms/TextField'
127import * as Toggle from '#/components/forms/Toggle'
128import {Atom_Stroke2_Corner0_Rounded as DeerIcon} from '#/components/icons/Atom'
129import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
···295 )
296}
2970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000298function TrustedVerifiersDialog({
299 control,
300}: {
···431432 const setLibreTranslateInstanceControl = Dialog.useDialogControl()
433434- const postReplacement = usePostReplacement()
435- const setPostReplacement = useSetPostReplacement()
436437 return (
438 <Layout.Screen>
···584585 <SettingsList.Divider />
586587- <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
588 <SettingsList.ItemIcon icon={PencilIcon} />
589 <SettingsList.ItemText>
590- <Trans>
591- Call posts{' '}
592- {postReplacement.string.length
593- ? postReplacement.string.toLowerCase()
594- : 'skeet'}
595- s
596- </Trans>
597 </SettingsList.ItemText>
598- <Toggle.Item
599- name="call_posts_skeets"
600- label={_(
601- msg`Changes post to another word of your choosing. Requires a refresh to update.`,
602- )}
603- value={postReplacement.enabled}
604- onChange={value =>
605- setPostReplacement({
606- enabled: value,
607- string: postReplacement.string,
608- })
609- }
610- style={[a.w_full]}>
611- <Toggle.LabelText style={[a.flex_1]}>
612- <Trans>
613- Changes post to another word of your choosing. Requires a
614- refresh to update.
615- </Trans>
616- </Toggle.LabelText>
617- <Toggle.Platform />
618- </Toggle.Item>
619620- {postReplacement.enabled && (
621- <SettingsList.Item>
622- <TextField.Root>
623- <TextField.Input
624- label={_(msg`Custom post name`)}
625- value={postReplacement.string}
626- onChangeText={(value: string) =>
627- setPostReplacement(
628- (curr: {enabled: boolean; string: string}) => ({
629- ...curr,
630- string: value,
631- }),
632- )
633- }
634- />
635- </TextField.Root>
636- </SettingsList.Item>
637- )}
638- </SettingsList.Group>
639640 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
641 <SettingsList.ItemIcon icon={PaintRollerIcon} />
···653 </Toggle.LabelText>
654 <Toggle.Platform />
655 </Toggle.Item>
656- <Toggle.Item
657- name="no_discover_fallback"
658- label={_(msg`Do not fall back to discover feed`)}
659- value={noDiscoverFallback}
660- onChange={value => setNoDiscoverFallback(value)}
661- style={[a.w_full]}>
662- <Toggle.LabelText style={[a.flex_1]}>
663- <Trans>Do not fall back to discover feed</Trans>
664- </Toggle.LabelText>
665- <Toggle.Platform />
666- </Toggle.Item>
667 <Toggle.Item
668 name="show_link_in_handle"
669 label={_(
···676 <Trans>
677 On non-bsky.social handles, show a link to that URL
678 </Trans>
679- </Toggle.LabelText>
680- <Toggle.Platform />
681- </Toggle.Item>
682-683- <Toggle.Item
684- name="repost_carousel"
685- label={_(msg`Combine reposts into a horizontal carousel`)}
686- value={repostCarouselEnabled}
687- onChange={value => setRepostCarouselEnabled(value)}
688- style={[a.w_full]}>
689- <Toggle.LabelText style={[a.flex_1]}>
690- <Trans>Combine reposts into a horizontal carousel</Trans>
691 </Toggle.LabelText>
692 <Toggle.Platform />
693 </Toggle.Item>
···908909 <Toggle.Item
910 name="disable_reposts_metrics"
911- label={_(msg`Disable Reposts Metrics`)}
912 value={disableRepostsMetrics}
913 onChange={value => setDisableRepostsMetrics(value)}
914 style={[a.w_full]}>
915 <Toggle.LabelText style={[a.flex_1]}>
916- <Trans>Disable Reposts Metrics</Trans>
917 </Toggle.LabelText>
918 <Toggle.Platform />
919 </Toggle.Item>
···1053 <LibreTranslateInstanceDialog
1054 control={setLibreTranslateInstanceControl}
1055 />
01056 </Layout.Screen>
1057 )
1058}