@oxy.so/services 12.4.0 → 12.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/client.js.map +1 -1
- package/lib/commonjs/ui/components/FollowButton.js +27 -14
- package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
- package/lib/commonjs/ui/components/FollowTargetButton.js +39 -51
- package/lib/commonjs/ui/components/FollowTargetButton.js.map +1 -1
- package/lib/commonjs/ui/components/OxyConsentScreen.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/types.js +14 -8
- package/lib/commonjs/ui/components/authChooser/types.js.map +1 -1
- package/lib/commonjs/ui/components/followButtonAccessibility.js +24 -7
- package/lib/commonjs/ui/components/followButtonAccessibility.js.map +1 -1
- package/lib/commonjs/ui/components/followLabels.js +38 -0
- package/lib/commonjs/ui/components/followLabels.js.map +1 -0
- package/lib/commonjs/ui/components/followRules.js +29 -13
- package/lib/commonjs/ui/components/followRules.js.map +1 -1
- package/lib/commonjs/ui/hooks/useI18n.js +2 -0
- package/lib/commonjs/ui/hooks/useI18n.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +3 -12
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/CreateAccountScreen.js +3 -12
- package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +7 -3
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/commonjs/ui/utils/deviceSessionTitle.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/client.js.map +1 -1
- package/lib/module/ui/components/FollowButton.js +28 -15
- package/lib/module/ui/components/FollowButton.js.map +1 -1
- package/lib/module/ui/components/FollowTargetButton.js +39 -51
- package/lib/module/ui/components/FollowTargetButton.js.map +1 -1
- package/lib/module/ui/components/OxyConsentScreen.js.map +1 -1
- package/lib/module/ui/components/authChooser/types.js +15 -8
- package/lib/module/ui/components/authChooser/types.js.map +1 -1
- package/lib/module/ui/components/followButtonAccessibility.js +24 -7
- package/lib/module/ui/components/followButtonAccessibility.js.map +1 -1
- package/lib/module/ui/components/followLabels.js +35 -0
- package/lib/module/ui/components/followLabels.js.map +1 -0
- package/lib/module/ui/components/followRules.js +30 -13
- package/lib/module/ui/components/followRules.js.map +1 -1
- package/lib/module/ui/hooks/useI18n.js +2 -0
- package/lib/module/ui/hooks/useI18n.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +4 -13
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/CreateAccountScreen.js +3 -13
- package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/module/ui/screens/EditProfileFieldScreen.js +8 -4
- package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/module/ui/utils/deviceSessionTitle.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/client.d.ts +3 -1
- package/lib/typescript/commonjs/ui/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/FollowButton.d.ts +13 -0
- package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/FollowTargetButton.d.ts +6 -18
- package/lib/typescript/commonjs/ui/components/FollowTargetButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyConsentScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts +2 -5
- package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/followButtonAccessibility.d.ts +12 -2
- package/lib/typescript/commonjs/ui/components/followButtonAccessibility.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/followLabels.d.ts +28 -0
- package/lib/typescript/commonjs/ui/components/followLabels.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/followRules.d.ts +20 -9
- package/lib/typescript/commonjs/ui/components/followRules.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +3 -2
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useI18n.d.ts +3 -1
- package/lib/typescript/commonjs/ui/hooks/useI18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/deviceSessionTitle.d.ts +1 -2
- package/lib/typescript/commonjs/ui/utils/deviceSessionTitle.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/client.d.ts +3 -1
- package/lib/typescript/module/ui/client.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FollowButton.d.ts +13 -0
- package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FollowTargetButton.d.ts +6 -18
- package/lib/typescript/module/ui/components/FollowTargetButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyConsentScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/types.d.ts +2 -5
- package/lib/typescript/module/ui/components/authChooser/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/followButtonAccessibility.d.ts +12 -2
- package/lib/typescript/module/ui/components/followButtonAccessibility.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/followLabels.d.ts +28 -0
- package/lib/typescript/module/ui/components/followLabels.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/followRules.d.ts +20 -9
- package/lib/typescript/module/ui/components/followRules.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +3 -2
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useI18n.d.ts +3 -1
- package/lib/typescript/module/ui/hooks/useI18n.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/deviceSessionTitle.d.ts +1 -2
- package/lib/typescript/module/ui/utils/deviceSessionTitle.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/index.ts +2 -5
- package/src/ui/client.ts +7 -5
- package/src/ui/components/FollowButton.tsx +53 -23
- package/src/ui/components/FollowTargetButton.tsx +39 -52
- package/src/ui/components/OxyConsentScreen.tsx +1 -3
- package/src/ui/components/__tests__/followButtonAccessibility.test.tsx +32 -6
- package/src/ui/components/__tests__/followTargetButton.test.ts +7 -1
- package/src/ui/components/authChooser/__tests__/profileColors.test.ts +13 -0
- package/src/ui/components/authChooser/types.ts +17 -17
- package/src/ui/components/followButtonAccessibility.ts +38 -25
- package/src/ui/components/followLabels.ts +48 -0
- package/src/ui/components/followRules.ts +29 -13
- package/src/ui/hooks/useI18n.ts +5 -3
- package/src/ui/screens/AccountSettingsScreen.tsx +10 -29
- package/src/ui/screens/CreateAccountScreen.tsx +9 -27
- package/src/ui/screens/EditProfileFieldScreen.tsx +10 -8
- package/src/ui/utils/deviceSessionTitle.ts +1 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { APP_COLOR_NAMES } from '@oxy.so/bloom/color-presets';
|
|
1
2
|
/**
|
|
2
3
|
* Shared types + per-account color resolution for the auth chooser's views.
|
|
3
4
|
*
|
|
@@ -8,17 +9,11 @@
|
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import type { AccountStorageUsageResponse } from '@oxy.so/core';
|
|
11
|
-
import {
|
|
12
|
-
APP_COLOR_NAMES,
|
|
13
|
-
APP_COLOR_PRESETS,
|
|
14
|
-
type AppColorName,
|
|
15
|
-
type useTheme,
|
|
16
|
-
} from '@oxy.so/bloom/theme';
|
|
12
|
+
import { APP_COLOR_PRESETS, type useTheme } from '@oxy.so/bloom/theme';
|
|
17
13
|
import type { AccountDialogMenuSection } from '../../navigation/accountDialogManager';
|
|
18
|
-
import type { useI18n } from '../../hooks/useI18n';
|
|
19
14
|
|
|
20
15
|
export type Theme = ReturnType<typeof useTheme>;
|
|
21
|
-
export type Translate
|
|
16
|
+
export type { Translate } from '../../hooks/useI18n';
|
|
22
17
|
|
|
23
18
|
/**
|
|
24
19
|
* One SUBORDINATE action on a sign-in surface — an alternative revealed behind
|
|
@@ -132,14 +127,19 @@ export interface SignInAlternatives {
|
|
|
132
127
|
* renders in EVERY consumer regardless of NativeWind availability.
|
|
133
128
|
*/
|
|
134
129
|
export function resolveAccentHex(color: string | null, fallback: string): string {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
const normalized = color?.trim().toLowerCase();
|
|
131
|
+
if (!normalized) return fallback;
|
|
132
|
+
|
|
133
|
+
if (/^#[0-9a-f]{3}$/i.test(normalized)) {
|
|
134
|
+
return `#${normalized[1]}${normalized[1]}${normalized[2]}${normalized[2]}${normalized[3]}${normalized[3]}`;
|
|
135
|
+
}
|
|
136
|
+
if (/^#[0-9a-f]{6}$/i.test(normalized)) return normalized;
|
|
138
137
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
138
|
+
// `amber` was the retired name for `yellow`; existing user records are
|
|
139
|
+
// migrated, but inboxes can still render cached account data during rollout.
|
|
140
|
+
const preset = normalized === 'amber' ? 'yellow' : normalized;
|
|
141
|
+
if (APP_COLOR_NAMES.includes(preset as (typeof APP_COLOR_NAMES)[number])) {
|
|
142
|
+
return APP_COLOR_PRESETS[preset as (typeof APP_COLOR_NAMES)[number]].hex;
|
|
143
|
+
}
|
|
144
|
+
return fallback;
|
|
145
145
|
}
|
|
@@ -8,10 +8,16 @@
|
|
|
8
8
|
* (OxyHQ/oxy#1375 item 22). The name now says the state, and who it is for
|
|
9
9
|
* when the caller knows the handle.
|
|
10
10
|
*
|
|
11
|
+
* The words follow the button's verb ("Joined @fediverse", hinted "Leaves
|
|
12
|
+
* @fediverse") and its language: every string is `@oxy.so/core`'s `follow.*`.
|
|
13
|
+
*
|
|
11
14
|
* Pure, and kept apart from the components, so every state is pinned by a
|
|
12
15
|
* table test rather than by a render that can only see one state at a time.
|
|
13
16
|
*/
|
|
14
17
|
|
|
18
|
+
import type { Translate } from '../hooks/useI18n';
|
|
19
|
+
import type { FollowLabels, FollowVerb } from './followLabels';
|
|
20
|
+
|
|
15
21
|
export interface FollowButtonAccessibility {
|
|
16
22
|
accessibilityLabel: string;
|
|
17
23
|
accessibilityHint?: string;
|
|
@@ -33,36 +39,40 @@ export function formatHandle(username: string | undefined): string | undefined {
|
|
|
33
39
|
* because a press does nothing until the write settles.
|
|
34
40
|
* - Following: "Following @nate", hinted "Unfollows @nate", because pressing
|
|
35
41
|
* it unfollows, which the visible "Following" does not say.
|
|
42
|
+
*
|
|
43
|
+
* The name is the visible label (the caller's, when it passed `labels`) beside
|
|
44
|
+
* the handle; the checking line and the hint are the verb's.
|
|
36
45
|
*/
|
|
37
|
-
export function describeFollowButton(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
export function describeFollowButton(
|
|
47
|
+
input: {
|
|
48
|
+
isKnown: boolean;
|
|
49
|
+
isFollowing: boolean;
|
|
50
|
+
isPending: boolean;
|
|
51
|
+
username?: string;
|
|
52
|
+
verb: FollowVerb;
|
|
53
|
+
labels: Pick<FollowLabels, 'idle' | 'active'>;
|
|
54
|
+
},
|
|
55
|
+
t: Translate,
|
|
56
|
+
): FollowButtonAccessibility {
|
|
43
57
|
const handle = formatHandle(input.username);
|
|
58
|
+
const words = `follow.verbs.${input.verb}`;
|
|
44
59
|
|
|
45
60
|
if (!input.isKnown) {
|
|
46
61
|
return {
|
|
47
62
|
accessibilityLabel: handle
|
|
48
|
-
?
|
|
49
|
-
:
|
|
63
|
+
? t(`${words}.checkingNamed`, { name: handle })
|
|
64
|
+
: t(`${words}.checking`),
|
|
50
65
|
};
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
? `Following ${handle}`
|
|
56
|
-
: 'Following'
|
|
57
|
-
: handle
|
|
58
|
-
? `Follow ${handle}`
|
|
59
|
-
: 'Follow';
|
|
68
|
+
const label = input.isFollowing ? input.labels.active : input.labels.idle;
|
|
69
|
+
const accessibilityLabel = handle ? t('follow.named', { label, name: handle }) : label;
|
|
60
70
|
|
|
61
71
|
if (input.isPending || !input.isFollowing) return { accessibilityLabel };
|
|
62
72
|
|
|
63
73
|
return {
|
|
64
74
|
accessibilityLabel,
|
|
65
|
-
accessibilityHint: handle ?
|
|
75
|
+
accessibilityHint: handle ? t(`${words}.undoNamed`, { name: handle }) : t(`${words}.undo`),
|
|
66
76
|
};
|
|
67
77
|
}
|
|
68
78
|
|
|
@@ -70,18 +80,21 @@ export function describeFollowButton(input: {
|
|
|
70
80
|
* The "Follow all" button. The labels are the caller's, since a starter pack
|
|
71
81
|
* and a feed's member list word it differently; the hint carries the count.
|
|
72
82
|
*/
|
|
73
|
-
export function describeFollowAllButton(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
export function describeFollowAllButton(
|
|
84
|
+
input: {
|
|
85
|
+
allFollowing: boolean;
|
|
86
|
+
isPending: boolean;
|
|
87
|
+
count: number;
|
|
88
|
+
followAllLabel: string;
|
|
89
|
+
followedAllLabel: string;
|
|
90
|
+
},
|
|
91
|
+
t: Translate,
|
|
92
|
+
): FollowButtonAccessibility {
|
|
80
93
|
const accessibilityLabel = input.allFollowing ? input.followedAllLabel : input.followAllLabel;
|
|
81
94
|
if (input.isPending) return { accessibilityLabel };
|
|
82
|
-
const
|
|
95
|
+
const hint = input.allFollowing ? 'follow.all.undoHint' : 'follow.all.hint';
|
|
83
96
|
return {
|
|
84
97
|
accessibilityLabel,
|
|
85
|
-
accessibilityHint: input.
|
|
98
|
+
accessibilityHint: t(input.count === 1 ? `${hint}One` : hint, { count: input.count }),
|
|
86
99
|
};
|
|
87
100
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words on a follow button, for every verb the ecosystem uses.
|
|
3
|
+
*
|
|
4
|
+
* "Follow" is wrong for half the things an Oxy user can follow: you subscribe
|
|
5
|
+
* to a channel, you join a community, you watch a listing. Both follow buttons
|
|
6
|
+
* read their words here, translated through `@oxy.so/core`'s `follow.*`
|
|
7
|
+
* strings, so a verb reads the same — and in the same language — wherever it is
|
|
8
|
+
* drawn.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useMemo } from 'react';
|
|
12
|
+
import { useI18n } from '../hooks/useI18n';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The small vocabulary of verbs the ecosystem actually uses. An application
|
|
16
|
+
* whose verb is not here passes `labels` instead — the escape hatch exists so
|
|
17
|
+
* this union never has to grow to accommodate one product's wording.
|
|
18
|
+
*/
|
|
19
|
+
export type FollowVerb = 'follow' | 'subscribe' | 'join' | 'watch';
|
|
20
|
+
|
|
21
|
+
export interface FollowLabels {
|
|
22
|
+
/** Not following yet. */
|
|
23
|
+
idle: string;
|
|
24
|
+
/** Following, and this application acts on it. */
|
|
25
|
+
active: string;
|
|
26
|
+
/** Asked, waiting for the other side to accept. */
|
|
27
|
+
pending: string;
|
|
28
|
+
/** Following globally, but switched off in this application. */
|
|
29
|
+
disabled: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The labels for `verb` in the current language, with the caller's `overrides` on top. */
|
|
33
|
+
export function useFollowLabels(
|
|
34
|
+
verb: FollowVerb = 'follow',
|
|
35
|
+
overrides?: Partial<FollowLabels>,
|
|
36
|
+
): FollowLabels {
|
|
37
|
+
const { t } = useI18n();
|
|
38
|
+
return useMemo(
|
|
39
|
+
() => ({
|
|
40
|
+
idle: t(`follow.verbs.${verb}.idle`),
|
|
41
|
+
active: t(`follow.verbs.${verb}.active`),
|
|
42
|
+
pending: t('follow.pending'),
|
|
43
|
+
disabled: t('follow.disabled'),
|
|
44
|
+
...overrides,
|
|
45
|
+
}),
|
|
46
|
+
[t, verb, overrides],
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -5,16 +5,26 @@
|
|
|
5
5
|
* application drawing its own affordance (a chip grid, a compact row) imports
|
|
6
6
|
* them from here — through `@oxy.so/services/ui/client` — without paying for the
|
|
7
7
|
* button's dropdown menu.
|
|
8
|
+
*
|
|
9
|
+
* The menu's words are `@oxy.so/core`'s `follow.*` strings, read through the
|
|
10
|
+
* caller's `t`; without one they are English.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import type { FollowApplicationMode } from '@oxy.so/contracts';
|
|
14
|
+
import { translate } from '@oxy.so/core';
|
|
15
|
+
import type { Translate } from '../hooks/useI18n';
|
|
16
|
+
import type { FollowVerb } from './followLabels';
|
|
11
17
|
|
|
12
18
|
/** A timed-follow choice offered in the menu. */
|
|
13
19
|
export interface FollowDuration {
|
|
20
|
+
/** The duration as it reads inside a sentence: "24 hours", "a week". */
|
|
14
21
|
label: string;
|
|
15
22
|
seconds: number;
|
|
16
23
|
}
|
|
17
24
|
|
|
25
|
+
/** The fallback language for a caller that passes no translator. */
|
|
26
|
+
const english: Translate = (key, vars) => translate(undefined, key, vars);
|
|
27
|
+
|
|
18
28
|
/**
|
|
19
29
|
* Whether an action leaves the target ACTIVE in this application.
|
|
20
30
|
*
|
|
@@ -50,15 +60,6 @@ export interface FollowMenuItem {
|
|
|
50
60
|
| { type: 'unfollow' };
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
/**
|
|
54
|
-
* Which choices exist, given the state.
|
|
55
|
-
*
|
|
56
|
-
* Pure and exported because this is the product decision, not a rendering
|
|
57
|
-
* detail: a timed follow is only offered before following, turning it off here
|
|
58
|
-
* is only offered while following, and NEITHER is offered before the server has
|
|
59
|
-
* answered — every one of them addresses a relationship that does not exist
|
|
60
|
-
* yet, so offering them mid-write would mean sending a guessed id.
|
|
61
|
-
*/
|
|
62
63
|
/**
|
|
63
64
|
* What the main button should do for the current state.
|
|
64
65
|
*
|
|
@@ -75,15 +76,30 @@ export function resolveFollowPrimaryAction(input: {
|
|
|
75
76
|
return 'unfollow';
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Which choices exist, given the state.
|
|
81
|
+
*
|
|
82
|
+
* Pure and exported because this is the product decision, not a rendering
|
|
83
|
+
* detail: a timed follow is only offered before following, turning it off here
|
|
84
|
+
* is only offered while following, and NEITHER is offered before the server has
|
|
85
|
+
* answered — every one of them addresses a relationship that does not exist
|
|
86
|
+
* yet, so offering them mid-write would mean sending a guessed id.
|
|
87
|
+
*/
|
|
78
88
|
export function buildFollowMenuItems(input: {
|
|
79
89
|
following: boolean;
|
|
80
90
|
applicationMode: FollowApplicationMode;
|
|
81
91
|
hasRelationship: boolean;
|
|
82
92
|
isPending: boolean;
|
|
83
93
|
durations: FollowDuration[] | false;
|
|
94
|
+
/** The button's idle label, which a timed choice starts with: "Follow for 24 hours". */
|
|
84
95
|
idleVerb: string;
|
|
85
96
|
applicationName: string;
|
|
97
|
+
/** Words the "everywhere" line with this verb: "Leave everywhere". Default `follow`. */
|
|
98
|
+
verb?: FollowVerb;
|
|
99
|
+
/** `useI18n().t`, for the menu in the app's language. Default English. */
|
|
100
|
+
t?: Translate;
|
|
86
101
|
}): FollowMenuItem[] {
|
|
102
|
+
const t = input.t ?? english;
|
|
87
103
|
const items: FollowMenuItem[] = [];
|
|
88
104
|
|
|
89
105
|
if (!input.following) {
|
|
@@ -91,7 +107,7 @@ export function buildFollowMenuItems(input: {
|
|
|
91
107
|
for (const d of input.durations) {
|
|
92
108
|
items.push({
|
|
93
109
|
key: `for-${d.seconds}`,
|
|
94
|
-
label:
|
|
110
|
+
label: t('follow.menu.forDuration', { label: input.idleVerb, duration: d.label }),
|
|
95
111
|
action: { type: 'follow-timed', seconds: d.seconds, durationLabel: d.label },
|
|
96
112
|
});
|
|
97
113
|
}
|
|
@@ -104,12 +120,12 @@ export function buildFollowMenuItems(input: {
|
|
|
104
120
|
input.applicationMode === 'disabled'
|
|
105
121
|
? {
|
|
106
122
|
key: 'enable-here',
|
|
107
|
-
label:
|
|
123
|
+
label: t('follow.menu.showIn', { app: input.applicationName }),
|
|
108
124
|
action: { type: 'enable-here' },
|
|
109
125
|
}
|
|
110
126
|
: {
|
|
111
127
|
key: 'disable-here',
|
|
112
|
-
label:
|
|
128
|
+
label: t('follow.menu.hideIn', { app: input.applicationName }),
|
|
113
129
|
action: { type: 'disable-here' },
|
|
114
130
|
},
|
|
115
131
|
);
|
|
@@ -117,7 +133,7 @@ export function buildFollowMenuItems(input: {
|
|
|
117
133
|
// Named for what it does. "Unfollow" beside "don't show here" would read as
|
|
118
134
|
// the same action twice, and the user would pick the wrong one.
|
|
119
135
|
key: 'unfollow-everywhere',
|
|
120
|
-
label: '
|
|
136
|
+
label: t(`follow.verbs.${input.verb ?? 'follow'}.everywhere`),
|
|
121
137
|
action: { type: 'unfollow' },
|
|
122
138
|
});
|
|
123
139
|
|
package/src/ui/hooks/useI18n.ts
CHANGED
|
@@ -9,6 +9,9 @@ import { getLocalesVersion, subscribeLocales, translate } from '@oxy.so/core';
|
|
|
9
9
|
*/
|
|
10
10
|
const FALLBACK_LOCALE = 'en-US';
|
|
11
11
|
|
|
12
|
+
/** `useI18n().t`: a key and its `{{placeholders}}` in, the current language's text out. */
|
|
13
|
+
export type Translate = (key: string, vars?: Record<string, string | number>) => string;
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Display copy for SDK surfaces.
|
|
14
17
|
*
|
|
@@ -32,9 +35,8 @@ export function useI18n() {
|
|
|
32
35
|
getLocalesVersion,
|
|
33
36
|
);
|
|
34
37
|
// biome-ignore lint/correctness/useExhaustiveDependencies: `localesVersion` is the re-translate signal — a new `t` makes consumers re-render once a dictionary lands.
|
|
35
|
-
const t = useMemo(() => {
|
|
36
|
-
return (key
|
|
37
|
-
translate(currentLanguage, key, vars);
|
|
38
|
+
const t = useMemo<Translate>(() => {
|
|
39
|
+
return (key, vars) => translate(currentLanguage, key, vars);
|
|
38
40
|
}, [currentLanguage, localesVersion]);
|
|
39
41
|
return { t, locale: currentLanguage };
|
|
40
42
|
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
ORGANIZATION_NAME_MAX_LENGTH,
|
|
4
|
-
organizationNameCheck,
|
|
5
|
-
checkContract,
|
|
6
|
-
} from '@oxy.so/contracts/client';
|
|
7
2
|
import { OrganizationSettings } from '../components/organizations/OrganizationSettings';
|
|
8
3
|
import { useCallback, useMemo, useState } from 'react';
|
|
9
4
|
import { View } from 'react-native';
|
|
@@ -16,10 +11,11 @@ import { Button } from '@oxy.so/bloom/button';
|
|
|
16
11
|
import { TextField, TextFieldInput } from '@oxy.so/bloom/text-field';
|
|
17
12
|
import { GroupedSection, GroupedSectionItem } from '@oxy.so/bloom/grouped-section';
|
|
18
13
|
import {
|
|
19
|
-
|
|
14
|
+
accountNameInvalidKey,
|
|
15
|
+
accountNameInvalidMessage,
|
|
16
|
+
accountNameMaxLength,
|
|
20
17
|
getNormalizedUserHandle,
|
|
21
|
-
|
|
22
|
-
MAX_DISPLAY_NAME_LENGTH,
|
|
18
|
+
isValidAccountName,
|
|
23
19
|
type UpdateAccountInput,
|
|
24
20
|
} from '@oxy.so/core';
|
|
25
21
|
import type { BaseScreenProps } from '../types/navigation';
|
|
@@ -29,7 +25,6 @@ import { useI18n } from '../hooks/useI18n';
|
|
|
29
25
|
import { useSurfaceHeader } from '../hooks/useSurfaceHeader';
|
|
30
26
|
import { clearedFieldsFromAccountUpdate, upsertCachedUser } from '../hooks/queries/userCache';
|
|
31
27
|
|
|
32
|
-
const DISPLAY_NAME_MAX = MAX_DISPLAY_NAME_LENGTH;
|
|
33
28
|
const BIO_MAX = 160;
|
|
34
29
|
|
|
35
30
|
const errorMessage = (error: unknown, fallback: string): string =>
|
|
@@ -158,22 +153,12 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
158
153
|
setDisplayNameError('');
|
|
159
154
|
return;
|
|
160
155
|
}
|
|
161
|
-
|
|
162
|
-
setDisplayNameError(
|
|
163
|
-
checkContract(organizationNameCheck, trimmed) !== null
|
|
164
|
-
? ''
|
|
165
|
-
: t('organizationDirectory.invalidName'),
|
|
166
|
-
);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const nameParts = trimmed.split(/\s+/).filter(Boolean);
|
|
170
|
-
const first = nameParts[0] || '';
|
|
171
|
-
const last = nameParts.length > 1 ? nameParts.slice(1).join(' ') : '';
|
|
172
|
-
const invalidPart = [first, last].find((part) => part && !isValidDisplayName(part));
|
|
156
|
+
const kind = node?.account.kind;
|
|
173
157
|
setDisplayNameError(
|
|
174
|
-
|
|
175
|
-
?
|
|
176
|
-
:
|
|
158
|
+
isValidAccountName(trimmed, kind)
|
|
159
|
+
? ''
|
|
160
|
+
: t(`accounts.settings.displayName.${accountNameInvalidKey(kind)}`) ||
|
|
161
|
+
accountNameInvalidMessage(kind),
|
|
177
162
|
);
|
|
178
163
|
},
|
|
179
164
|
[node?.account.kind, t],
|
|
@@ -235,11 +220,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
235
220
|
value={displayName}
|
|
236
221
|
onChangeText={handleDisplayNameChange}
|
|
237
222
|
isInvalid={Boolean(displayNameError)}
|
|
238
|
-
maxLength={
|
|
239
|
-
node?.account.kind === 'organization'
|
|
240
|
-
? ORGANIZATION_NAME_MAX_LENGTH
|
|
241
|
-
: DISPLAY_NAME_MAX
|
|
242
|
-
}
|
|
223
|
+
maxLength={accountNameMaxLength(node?.account.kind)}
|
|
243
224
|
/>
|
|
244
225
|
</TextField>
|
|
245
226
|
{displayNameError ? (
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
import type { OrganizationType } from '@oxy.so/contracts';
|
|
3
|
-
import {
|
|
4
|
-
ORGANIZATION_NAME_MAX_LENGTH,
|
|
5
|
-
organizationNameCheck,
|
|
6
|
-
checkContract,
|
|
7
|
-
} from '@oxy.so/contracts/client';
|
|
8
3
|
import { OrganizationDirectorySearch } from '../components/organizations/OrganizationDirectorySearch';
|
|
9
4
|
import { OrganizationTypePicker } from '../components/organizations/OrganizationTypePicker';
|
|
10
5
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
@@ -13,10 +8,11 @@ import Ionicons from '../icons/Ionicons';
|
|
|
13
8
|
import type { AccountCategoryId, AccountKind, CreateAccountInput } from '@oxy.so/core';
|
|
14
9
|
import {
|
|
15
10
|
accountCategoryLabel,
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
accountNameInvalidKey,
|
|
12
|
+
accountNameInvalidMessage,
|
|
13
|
+
accountNameMaxLength,
|
|
14
|
+
isValidAccountName,
|
|
18
15
|
MAX_ACCOUNT_CATEGORIES,
|
|
19
|
-
MAX_DISPLAY_NAME_LENGTH,
|
|
20
16
|
SELECTABLE_ACCOUNT_CATEGORY_IDS,
|
|
21
17
|
} from '@oxy.so/core';
|
|
22
18
|
import {
|
|
@@ -61,7 +57,6 @@ type UsernameStatus = 'idle' | 'checking' | 'available' | 'taken' | 'invalid';
|
|
|
61
57
|
type CreatableAccountKind = Extract<AccountKind, 'organization' | 'project' | 'bot'>;
|
|
62
58
|
|
|
63
59
|
const DEBOUNCE_MS = 400;
|
|
64
|
-
const DISPLAY_NAME_MAX = MAX_DISPLAY_NAME_LENGTH;
|
|
65
60
|
const BIO_MAX = 160;
|
|
66
61
|
|
|
67
62
|
interface KindOption {
|
|
@@ -286,22 +281,11 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
|
|
|
286
281
|
setDisplayNameError('');
|
|
287
282
|
return;
|
|
288
283
|
}
|
|
289
|
-
if (kind === 'organization') {
|
|
290
|
-
setDisplayNameError(
|
|
291
|
-
checkContract(organizationNameCheck, trimmed) !== null
|
|
292
|
-
? ''
|
|
293
|
-
: t('organizationDirectory.invalidName'),
|
|
294
|
-
);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
const nameParts = trimmed.split(/\s+/);
|
|
298
|
-
const firstName = nameParts[0] || '';
|
|
299
|
-
const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : '';
|
|
300
|
-
const invalidPart = [firstName, lastName].find((part) => part && !isValidDisplayName(part));
|
|
301
284
|
setDisplayNameError(
|
|
302
|
-
|
|
303
|
-
?
|
|
304
|
-
:
|
|
285
|
+
isValidAccountName(trimmed, kind)
|
|
286
|
+
? ''
|
|
287
|
+
: t(`accounts.create.displayName.${accountNameInvalidKey(kind)}`) ||
|
|
288
|
+
accountNameInvalidMessage(kind),
|
|
305
289
|
);
|
|
306
290
|
},
|
|
307
291
|
[kind, t],
|
|
@@ -548,9 +532,7 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
|
|
|
548
532
|
value={displayName}
|
|
549
533
|
onChangeText={handleDisplayNameChange}
|
|
550
534
|
isInvalid={Boolean(displayNameError)}
|
|
551
|
-
maxLength={
|
|
552
|
-
kind === 'organization' ? ORGANIZATION_NAME_MAX_LENGTH : DISPLAY_NAME_MAX
|
|
553
|
-
}
|
|
535
|
+
maxLength={accountNameMaxLength(kind)}
|
|
554
536
|
/>
|
|
555
537
|
</TextField>
|
|
556
538
|
{displayNameError ? (
|
|
@@ -22,7 +22,7 @@ import { useSurfaceHeader } from '../hooks/useSurfaceHeader';
|
|
|
22
22
|
import { useOxy } from '../context/oxyRuntimeContext';
|
|
23
23
|
import { useProfileEditing } from '../hooks/useProfileEditing';
|
|
24
24
|
import { toast } from '@oxy.so/bloom/toast';
|
|
25
|
-
import {
|
|
25
|
+
import { accountNameInvalidKey, accountNameInvalidMessage, isValidAccountName } from '@oxy.so/core';
|
|
26
26
|
import { getLinkTitle, getLinkDescription, linksToListItems } from './linkFormat';
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -217,6 +217,12 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
|
|
|
217
217
|
fieldType === 'username',
|
|
218
218
|
);
|
|
219
219
|
|
|
220
|
+
// The name policy is the account's kind's: an organization a person switched
|
|
221
|
+
// into is named by the title policy, not held to a person's.
|
|
222
|
+
const kind = user?.kind;
|
|
223
|
+
const invalidNameMessage =
|
|
224
|
+
t(`editProfile.items.displayName.${accountNameInvalidKey(kind)}`) ||
|
|
225
|
+
accountNameInvalidMessage(kind);
|
|
220
226
|
// Get field configuration based on fieldType
|
|
221
227
|
const fieldConfig = useMemo((): FieldConfig => {
|
|
222
228
|
switch (fieldType) {
|
|
@@ -233,9 +239,7 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
|
|
|
233
239
|
placeholder:
|
|
234
240
|
t('editProfile.items.displayName.firstNamePlaceholder') || 'Enter first name',
|
|
235
241
|
validation: (value) =>
|
|
236
|
-
|
|
237
|
-
? undefined
|
|
238
|
-
: t('editProfile.items.displayName.invalidChars') || DISPLAY_NAME_INVALID_MESSAGE,
|
|
242
|
+
isValidAccountName(value, kind) ? undefined : invalidNameMessage,
|
|
239
243
|
},
|
|
240
244
|
{
|
|
241
245
|
key: 'lastName',
|
|
@@ -244,9 +248,7 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
|
|
|
244
248
|
t('editProfile.items.displayName.lastNamePlaceholder') ||
|
|
245
249
|
'Enter last name (optional)',
|
|
246
250
|
validation: (value) =>
|
|
247
|
-
|
|
248
|
-
? undefined
|
|
249
|
-
: t('editProfile.items.displayName.invalidChars') || DISPLAY_NAME_INVALID_MESSAGE,
|
|
251
|
+
isValidAccountName(value, kind) ? undefined : invalidNameMessage,
|
|
250
252
|
},
|
|
251
253
|
],
|
|
252
254
|
};
|
|
@@ -360,7 +362,7 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
|
|
|
360
362
|
fields: [],
|
|
361
363
|
};
|
|
362
364
|
}
|
|
363
|
-
}, [fieldType, t]);
|
|
365
|
+
}, [fieldType, t, kind, invalidNameMessage]);
|
|
364
366
|
|
|
365
367
|
// Field change handler
|
|
366
368
|
const handleFieldChange = useCallback(
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { getNormalizedUserHandle, type DeviceLinkedSession } from '@oxy.so/core';
|
|
2
|
-
|
|
3
|
-
type Translate = (key: string, vars?: Record<string, string | number>) => string;
|
|
2
|
+
import type { Translate } from '../hooks/useI18n';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* The title of one row in "Sessions & devices".
|