diff --git a/README.md b/README.md
index 69ec19cdc..44906e992 100644
--- a/README.md
+++ b/README.md
@@ -16,11 +16,11 @@ alt="Get it on Obtainium" height="54" /> -->
- 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 @@ alt="Get it on Obtainium" height="54" /> -->
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 @@ These are all available as options in a sub-page of the app's settings.
#### 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 @@ These are all available as options in a sub-page of the app's settings.
You can completely disable the visiblity of all metrics individually, including the number of:
- likes
-- reskeets
+- reposts
- quotes
- saves
- replies
@@ -68,7 +68,7 @@ You can completely disable the visiblity of all metrics individually, including
## 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 @@ Witchsky is a fork of the official Bluesky client, social-app. It encompasses a
> 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
index b8fee8c27..4c1bee617 100644
--- 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 @@ export async function dynamicActivate(locale: AppLanguage) {
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 @@ export async function dynamicActivate(locale: AppLanguage) {
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
index c123fec3a..336a2c3d4 100644
--- 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 @@ export async function dynamicActivate(locale: AppLanguage) {
}
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 @@ export async function dynamicActivate(locale: AppLanguage) {
}
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
index 0e8c3a6bc..a7a4dc84c 100644
--- 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 @@ function traverseAndReplace(value: any): any {
* @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
index d4cf5a39a..1552b4093 100644
--- 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 LibreTranslateInstanceDialog({
)
}
+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 @@ export function DeerSettingsScreen({}: Props) {
const setLibreTranslateInstanceControl = Dialog.useDialogControl()
- const postReplacement = usePostReplacement()
- const setPostReplacement = useSetPostReplacement()
+ const setPostReplacementDialogControl = Dialog.useDialogControl()
return (
@@ -584,58 +721,18 @@ export function DeerSettingsScreen({}: Props) {
-
+
-
- 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 @@ export function DeerSettingsScreen({}: Props) {
- setNoDiscoverFallback(value)}
- style={[a.w_full]}>
-
- Do not fall back to discover feed
-
-
-
+
- setRepostCarouselEnabled(value)}
- style={[a.w_full]}>
-
- Combine reposts into a horizontal carousel
-
-
-
-
setDisableRepostsMetrics(value)}
style={[a.w_full]}>
- Disable Reposts Metrics
+ Disable reposts metrics
@@ -1053,6 +1128,7 @@ export function DeerSettingsScreen({}: Props) {
+
)
}
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index d92b313fe..51ad09861 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -172,7 +172,8 @@ const schema = z.object({
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 @@ export const defaults: Schema = {
]),
},
requireAltTextEnabled: true,
- largeAltBadgeEnabled: true,
+ largeAltBadgeEnabled: false,
externalEmbeds: {},
mutedThreads: [],
invites: {
@@ -242,7 +243,7 @@ export const defaults: Schema = {
// deer
goLinksEnabled: true,
constellationEnabled: true,
- directFetchRecords: false,
+ directFetchRecords: true,
noAppLabelers: false,
noDiscoverFallback: false,
repostCarouselEnabled: false,
@@ -299,7 +300,8 @@ export const defaults: Schema = {
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
index 20e0b4f8f..b1b970e0c 100644
--- 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 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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 (