Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

implement custom post name feature with replace

replaces "Post" with "Skeet" when language files are loaded

finish up the feature

+299 -47
+5 -3
src/locale/i18n.ts
··· 13 13 14 14 import {sanitizeAppLanguageSetting} from '#/locale/helpers' 15 15 import {AppLanguage} from '#/locale/languages' 16 + import {applySkeetReplacements} from '#/locale/linguiHook' 16 17 import {messages as messagesAn} from '#/locale/locales/an/messages' 17 18 import {messages as messagesAst} from '#/locale/locales/ast/messages' 18 19 import {messages as messagesCa} from '#/locale/locales/ca/messages' ··· 125 126 break 126 127 } 127 128 case AppLanguage.en_GB: { 128 - i18n.loadAndActivate({locale, messages: messagesEn_GB}) 129 + const transformedMsgs = applySkeetReplacements(messagesEn_GB, locale) 130 + i18n.loadAndActivate({locale, messages: transformedMsgs}) 129 131 await Promise.all([ 130 132 import('@formatjs/intl-pluralrules/locale-data/en'), 131 133 import('@formatjs/intl-numberformat/locale-data/en-GB'), ··· 418 420 await Promise.all([ 419 421 import('@formatjs/intl-pluralrules/locale-data/zh'), 420 422 import('@formatjs/intl-numberformat/locale-data/zh'), 421 - import('@formatjs/intl-displaynames/locale-data/zh'), 422 423 ]) 423 424 break 424 425 } 425 426 default: { 426 - i18n.loadAndActivate({locale, messages: messagesEn}) 427 + const transformedMsgs = applySkeetReplacements(messagesEn, locale) 428 + i18n.loadAndActivate({locale, messages: transformedMsgs}) 427 429 break 428 430 } 429 431 }
+22 -5
src/locale/i18n.web.ts
··· 1 1 import {useEffect} from 'react' 2 - import {i18n} from '@lingui/core' 2 + import {i18n, type Messages} from '@lingui/core' 3 3 4 4 import {sanitizeAppLanguageSetting} from '#/locale/helpers' 5 5 import {AppLanguage} from '#/locale/languages' 6 + import {applySkeetReplacements} from '#/locale/linguiHook' 6 7 import {useLanguagePrefs} from '#/state/preferences' 7 8 8 9 /** 9 10 * We do a dynamic import of just the catalog that we need 10 11 */ 11 12 export async function dynamicActivate(locale: AppLanguage) { 12 - let mod: any 13 + let mod: {messages: Messages} 13 14 14 15 switch (locale) { 15 16 case AppLanguage.an: { ··· 40 41 mod = await import(`./locales/el/messages`) 41 42 break 42 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 + } 43 51 case AppLanguage.en_GB: { 44 52 mod = await import(`./locales/en-GB/messages`) 53 + const transformedEnGbMessages = applySkeetReplacements( 54 + mod.messages, 55 + locale, 56 + ) 57 + i18n.load(locale, transformedEnGbMessages) 58 + i18n.activate(locale) 45 59 break 46 60 } 47 61 case AppLanguage.eo: { ··· 174 188 } 175 189 default: { 176 190 mod = await import(`./locales/en/messages`) 191 + const transformedDefaultMessages = applySkeetReplacements( 192 + mod.messages, 193 + locale, 194 + ) 195 + i18n.load(locale, transformedDefaultMessages) 196 + i18n.activate(locale) 177 197 break 178 198 } 179 199 } 180 - 181 - i18n.load(locale, mod.messages) 182 - i18n.activate(locale) 183 200 } 184 201 185 202 export function useLocaleLanguage() {
+61
src/locale/linguiHook.ts
··· 1 + import {type Messages} from '@lingui/core' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + // Helper to apply the replacement to a single string 6 + function 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' 10 + return text 11 + .replaceAll('Post', repl[0].toUpperCase() + repl.slice(1)) 12 + .replaceAll('post', repl) 13 + } 14 + 15 + // Recursive helper to traverse and replace strings in nested structures 16 + function traverseAndReplace(value: any): any { 17 + if (typeof value === 'string') { 18 + return replaceInString(value) 19 + } 20 + if (Array.isArray(value)) { 21 + return value.map(item => traverseAndReplace(item)) 22 + } 23 + if (typeof value === 'object' && value !== null) { 24 + const newObject: Record<string, any> = {} 25 + for (const key in value) { 26 + if (Object.prototype.hasOwnProperty.call(value, key)) { 27 + newObject[key] = traverseAndReplace(value[key]) 28 + } 29 + } 30 + return newObject 31 + } 32 + return value 33 + } 34 + 35 + /** 36 + * Applies "Post" to "Skeet" replacements (case-insensitive) to messages 37 + * for English locales. 38 + * @param messages The raw messages object loaded from Lingui. 39 + * @param locale The current locale string. 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 { 47 + const {enabled} = persisted.get('postReplacement') 48 + console.log('replacements enabled:', enabled) 49 + if (!enabled || !locale.startsWith('en')) { 50 + return messages 51 + } 52 + 53 + // Traverse the entire messages catalog and apply replacements 54 + const transformedCatalog: Messages = {} 55 + for (const key in messages) { 56 + if (Object.prototype.hasOwnProperty.call(messages, key)) { 57 + transformedCatalog[key] = traverseAndReplace(messages[key]) 58 + } 59 + } 60 + return transformedCatalog 61 + }
+47 -30
src/screens/Profile/Header/Metrics.tsx
··· 37 37 const disablePostsMetrics = useDisablePostsMetrics() 38 38 39 39 return ( 40 - <View 41 - style={[a.flex_row, a.gap_sm, a.align_center]} 42 - pointerEvents="box-none"> 43 - <InlineLinkText 44 - testID="profileHeaderFollowersButton" 45 - style={[a.flex_row, t.atoms.text]} 46 - to={makeProfileLink(profile, 'followers')} 47 - label={`${profile.followersCount || 0} ${pluralizedFollowers}`}> 48 - <Text style={[a.font_semi_bold, a.text_md]}>{followers} </Text> 49 - <Text style={[t.atoms.text_contrast_medium, a.text_md]}> 50 - {pluralizedFollowers} 51 - </Text> 52 - </InlineLinkText> 53 - <InlineLinkText 54 - testID="profileHeaderFollowsButton" 55 - style={[a.flex_row, t.atoms.text]} 56 - to={makeProfileLink(profile, 'follows')} 57 - label={_(msg`${profile.followsCount || 0} following`)}> 58 - <Text style={[a.font_semi_bold, a.text_md]}>{following} </Text> 59 - <Text style={[t.atoms.text_contrast_medium, a.text_md]}> 60 - {pluralizedFollowings} 61 - </Text> 62 - </InlineLinkText> 63 - <Text style={[a.font_semi_bold, t.atoms.text, a.text_md]}> 64 - {formatCount(i18n, profile.postsCount || 0)}{' '} 65 - <Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}> 66 - {plural(profile.postsCount || 0, {one: 'post', other: 'posts'})} 67 - </Text> 68 - </Text> 69 - </View> 40 + <> 41 + {disableFollowersMetrics && 42 + disableFollowingMetrics && 43 + disablePostsMetrics ? null : ( 44 + <View 45 + style={[a.flex_row, a.gap_sm, a.align_center]} 46 + pointerEvents="box-none"> 47 + {!disableFollowersMetrics ? ( 48 + <InlineLinkText 49 + testID="profileHeaderFollowersButton" 50 + style={[a.flex_row, t.atoms.text]} 51 + to={makeProfileLink(profile, 'followers')} 52 + label={`${profile.followersCount || 0} ${pluralizedFollowers}`}> 53 + <Text style={[a.font_semi_bold, a.text_md]}>{followers} </Text> 54 + <Text style={[t.atoms.text_contrast_medium, a.text_md]}> 55 + {pluralizedFollowers} 56 + </Text> 57 + </InlineLinkText> 58 + ) : null} 59 + {!disableFollowingMetrics ? ( 60 + <InlineLinkText 61 + testID="profileHeaderFollowsButton" 62 + style={[a.flex_row, t.atoms.text]} 63 + to={makeProfileLink(profile, 'follows')} 64 + label={_(msg`${profile.followsCount || 0} following`)}> 65 + <Text style={[a.font_semi_bold, a.text_md]}>{following} </Text> 66 + <Text style={[t.atoms.text_contrast_medium, a.text_md]}> 67 + {pluralizedFollowings} 68 + </Text> 69 + </InlineLinkText> 70 + ) : null} 71 + {!disablePostsMetrics ? ( 72 + <Text style={[a.font_semi_bold, t.atoms.text, a.text_md]}> 73 + {formatCount(i18n, profile.postsCount || 0)}{' '} 74 + <Text 75 + style={[ 76 + t.atoms.text_contrast_medium, 77 + a.font_normal, 78 + a.text_md, 79 + ]}> 80 + {plural(profile.postsCount || 0, {one: 'post', other: 'posts'})} 81 + </Text> 82 + </Text> 83 + ) : null} 84 + </View> 85 + )} 86 + </> 70 87 ) 71 88 }
+61
src/screens/Settings/DeerSettings.tsx
··· 100 100 useSetNoDiscoverFallback, 101 101 } from '#/state/preferences/no-discover-fallback' 102 102 import { 103 + usePostReplacement, 104 + useSetPostReplacement, 105 + } from '#/state/preferences/post-name-replacement' 106 + import { 103 107 useRepostCarouselEnabled, 104 108 useSetRepostCarouselEnabled, 105 109 } from '#/state/preferences/repost-carousel-enabled' ··· 119 123 import {Admonition} from '#/components/Admonition' 120 124 import {Button, ButtonText} from '#/components/Button' 121 125 import * as Dialog from '#/components/Dialog' 126 + import * as TextField from '#/components/forms/TextField' 122 127 import * as Toggle from '#/components/forms/Toggle' 123 128 import {Atom_Stroke2_Corner0_Rounded as DeerIcon} from '#/components/icons/Atom' 124 129 import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' ··· 425 430 426 431 const setLibreTranslateInstanceControl = Dialog.useDialogControl() 427 432 433 + const postReplacement = usePostReplacement() 434 + const setPostReplacement = useSetPostReplacement() 435 + 428 436 return ( 429 437 <Layout.Screen> 430 438 <Layout.Header.Outer> ··· 574 582 </SettingsList.Item> 575 583 576 584 <SettingsList.Divider /> 585 + 586 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 587 + <SettingsList.ItemIcon icon={VerifiedIcon} /> 588 + <SettingsList.ItemText> 589 + <Trans> 590 + Call posts{' '} 591 + {postReplacement.string.length 592 + ? postReplacement.string.toLowerCase() 593 + : 'skeet'} 594 + s 595 + </Trans> 596 + </SettingsList.ItemText> 597 + <Toggle.Item 598 + name="call_posts_skeets" 599 + label={_( 600 + msg`Changes post to another word of your choosing. Requires a refresh to update.`, 601 + )} 602 + value={postReplacement.enabled} 603 + onChange={value => 604 + setPostReplacement({ 605 + enabled: value, 606 + string: postReplacement.string, 607 + }) 608 + } 609 + style={[a.w_full]}> 610 + <Toggle.LabelText style={[a.flex_1]}> 611 + <Trans> 612 + Changes post to another word of your choosing. Requires a 613 + refresh to update. 614 + </Trans> 615 + </Toggle.LabelText> 616 + <Toggle.Platform /> 617 + </Toggle.Item> 618 + 619 + {postReplacement.enabled && ( 620 + <SettingsList.Item> 621 + <TextField.Root> 622 + <TextField.Input 623 + label={_(msg`Custom post name`)} 624 + value={postReplacement.string} 625 + onChangeText={(value: string) => 626 + setPostReplacement( 627 + (curr: {enabled: boolean; string: string}) => ({ 628 + ...curr, 629 + string: value, 630 + }), 631 + ) 632 + } 633 + /> 634 + </TextField.Root> 635 + </SettingsList.Item> 636 + )} 637 + </SettingsList.Group> 577 638 578 639 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 579 640 <SettingsList.ItemIcon icon={PaintRollerIcon} />
+10
src/state/persisted/schema.ts
··· 170 170 highQualityImages: z.boolean().optional(), 171 171 hideUnreplyablePosts: z.boolean().optional(), 172 172 173 + postReplacement: z.object({ 174 + enabled: z.boolean().optional(), 175 + string: z.string().optional(), 176 + }), 177 + 173 178 showExternalShareButtons: z.boolean().optional(), 174 179 175 180 translationServicePreference: z.enum([ ··· 291 296 showExternalShareButtons: false, 292 297 translationServicePreference: 'google', 293 298 libreTranslateInstance: 'https://libretranslate.com/', 299 + 300 + postReplacement: { 301 + enabled: false, 302 + string: 'skeet', 303 + }, 294 304 } 295 305 296 306 export function tryParse(rawData: string): Schema | undefined {
+12 -9
src/state/preferences/index.tsx
··· 35 35 import {Provider as LargeAltBadgeProvider} from './large-alt-badge' 36 36 import {Provider as NoAppLabelersProvider} from './no-app-labelers' 37 37 import {Provider as NoDiscoverProvider} from './no-discover-fallback' 38 + import {Provider as PostNameReplacementProvider} from './post-name-replacement.tsx' 38 39 import {Provider as RepostCarouselProvider} from './repost-carousel-enabled' 39 40 import {Provider as ShowLinkInHandleProvider} from './show-link-in-handle' 40 41 import {Provider as SubtitlesProvider} from './subtitles' ··· 110 111 <HideUnreplyablePostsProvider> 111 112 <EnableSquareAvatarsProvider> 112 113 <EnableSquareButtonsProvider> 113 - <DisableVerifyEmailReminderProvider> 114 - <TranslationServicePreferenceProvider> 115 - <DisableComposerPromptProvider> 116 - { 117 - children 118 - } 119 - </DisableComposerPromptProvider> 120 - </TranslationServicePreferenceProvider> 121 - </DisableVerifyEmailReminderProvider> 114 + <PostNameReplacementProvider> 115 + <DisableVerifyEmailReminderProvider> 116 + <TranslationServicePreferenceProvider> 117 + <DisableComposerPromptProvider> 118 + { 119 + children 120 + } 121 + </DisableComposerPromptProvider> 122 + </TranslationServicePreferenceProvider> 123 + </DisableVerifyEmailReminderProvider> 124 + </PostNameReplacementProvider> 122 125 </EnableSquareButtonsProvider> 123 126 </EnableSquareAvatarsProvider> 124 127 </HideUnreplyablePostsProvider>
+81
src/state/preferences/post-name-replacement.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + interface PostReplacementState { 6 + enabled: boolean 7 + string: string 8 + } 9 + 10 + type StateContext = PostReplacementState 11 + type SetContext = ( 12 + v: 13 + | PostReplacementState 14 + | ((curr: PostReplacementState) => PostReplacementState), 15 + ) => void 16 + 17 + const stateContext = React.createContext<StateContext>( 18 + persisted.defaults.postReplacement as PostReplacementState, 19 + ) 20 + const setContext = React.createContext<SetContext>( 21 + ( 22 + _: 23 + | PostReplacementState 24 + | ((curr: PostReplacementState) => PostReplacementState), 25 + ) => {}, 26 + ) 27 + 28 + export function Provider({children}: React.PropsWithChildren<{}>) { 29 + const [state, _setState] = React.useState<PostReplacementState>(() => { 30 + const persistedState = persisted.get('postReplacement') 31 + return { 32 + enabled: 33 + persistedState?.enabled ?? persisted.defaults.postReplacement.enabled!, 34 + string: 35 + persistedState?.string ?? persisted.defaults.postReplacement.string!, 36 + } 37 + }) 38 + 39 + const setState = React.useCallback( 40 + ( 41 + val: 42 + | PostReplacementState 43 + | ((curr: PostReplacementState) => PostReplacementState), 44 + ) => { 45 + _setState(curr => { 46 + const next = typeof val === 'function' ? val(curr) : val 47 + persisted.write('postReplacement', next) 48 + return next 49 + }) 50 + }, 51 + [], 52 + ) 53 + 54 + React.useEffect(() => { 55 + return persisted.onUpdate('postReplacement', next => { 56 + setState({string: next.string ?? 'skeet', enabled: next.enabled ?? true}) 57 + /* 58 + if (nextVal) { 59 + _setState({ 60 + enabled: 61 + nextVal.enabled ?? persisted.defaults.postReplacement.enabled!, 62 + string: nextVal.string ?? persisted.defaults.postReplacement.string!, 63 + }) 64 + }*/ 65 + }) 66 + }, []) 67 + 68 + return ( 69 + <stateContext.Provider value={state}> 70 + <setContext.Provider value={setState}>{children}</setContext.Provider> 71 + </stateContext.Provider> 72 + ) 73 + } 74 + 75 + export function usePostReplacement() { 76 + return React.useContext(stateContext) 77 + } 78 + 79 + export function useSetPostReplacement() { 80 + return React.useContext(setContext) 81 + }