@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.
Files changed (118) hide show
  1. package/lib/commonjs/index.js.map +1 -1
  2. package/lib/commonjs/ui/client.js.map +1 -1
  3. package/lib/commonjs/ui/components/FollowButton.js +27 -14
  4. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  5. package/lib/commonjs/ui/components/FollowTargetButton.js +39 -51
  6. package/lib/commonjs/ui/components/FollowTargetButton.js.map +1 -1
  7. package/lib/commonjs/ui/components/OxyConsentScreen.js.map +1 -1
  8. package/lib/commonjs/ui/components/authChooser/types.js +14 -8
  9. package/lib/commonjs/ui/components/authChooser/types.js.map +1 -1
  10. package/lib/commonjs/ui/components/followButtonAccessibility.js +24 -7
  11. package/lib/commonjs/ui/components/followButtonAccessibility.js.map +1 -1
  12. package/lib/commonjs/ui/components/followLabels.js +38 -0
  13. package/lib/commonjs/ui/components/followLabels.js.map +1 -0
  14. package/lib/commonjs/ui/components/followRules.js +29 -13
  15. package/lib/commonjs/ui/components/followRules.js.map +1 -1
  16. package/lib/commonjs/ui/hooks/useI18n.js +2 -0
  17. package/lib/commonjs/ui/hooks/useI18n.js.map +1 -1
  18. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +3 -12
  19. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  20. package/lib/commonjs/ui/screens/CreateAccountScreen.js +3 -12
  21. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
  22. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +7 -3
  23. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
  24. package/lib/commonjs/ui/utils/deviceSessionTitle.js.map +1 -1
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/ui/client.js.map +1 -1
  27. package/lib/module/ui/components/FollowButton.js +28 -15
  28. package/lib/module/ui/components/FollowButton.js.map +1 -1
  29. package/lib/module/ui/components/FollowTargetButton.js +39 -51
  30. package/lib/module/ui/components/FollowTargetButton.js.map +1 -1
  31. package/lib/module/ui/components/OxyConsentScreen.js.map +1 -1
  32. package/lib/module/ui/components/authChooser/types.js +15 -8
  33. package/lib/module/ui/components/authChooser/types.js.map +1 -1
  34. package/lib/module/ui/components/followButtonAccessibility.js +24 -7
  35. package/lib/module/ui/components/followButtonAccessibility.js.map +1 -1
  36. package/lib/module/ui/components/followLabels.js +35 -0
  37. package/lib/module/ui/components/followLabels.js.map +1 -0
  38. package/lib/module/ui/components/followRules.js +30 -13
  39. package/lib/module/ui/components/followRules.js.map +1 -1
  40. package/lib/module/ui/hooks/useI18n.js +2 -0
  41. package/lib/module/ui/hooks/useI18n.js.map +1 -1
  42. package/lib/module/ui/screens/AccountSettingsScreen.js +4 -13
  43. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  44. package/lib/module/ui/screens/CreateAccountScreen.js +3 -13
  45. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
  46. package/lib/module/ui/screens/EditProfileFieldScreen.js +8 -4
  47. package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
  48. package/lib/module/ui/utils/deviceSessionTitle.js.map +1 -1
  49. package/lib/typescript/commonjs/index.d.ts +2 -1
  50. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/ui/client.d.ts +3 -1
  52. package/lib/typescript/commonjs/ui/client.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/ui/components/FollowButton.d.ts +13 -0
  54. package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/ui/components/FollowTargetButton.d.ts +6 -18
  56. package/lib/typescript/commonjs/ui/components/FollowTargetButton.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/ui/components/OxyConsentScreen.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts +2 -5
  59. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/ui/components/followButtonAccessibility.d.ts +12 -2
  61. package/lib/typescript/commonjs/ui/components/followButtonAccessibility.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/ui/components/followLabels.d.ts +28 -0
  63. package/lib/typescript/commonjs/ui/components/followLabels.d.ts.map +1 -0
  64. package/lib/typescript/commonjs/ui/components/followRules.d.ts +20 -9
  65. package/lib/typescript/commonjs/ui/components/followRules.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +3 -2
  67. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/ui/hooks/useI18n.d.ts +3 -1
  69. package/lib/typescript/commonjs/ui/hooks/useI18n.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/ui/utils/deviceSessionTitle.d.ts +1 -2
  74. package/lib/typescript/commonjs/ui/utils/deviceSessionTitle.d.ts.map +1 -1
  75. package/lib/typescript/module/index.d.ts +2 -1
  76. package/lib/typescript/module/index.d.ts.map +1 -1
  77. package/lib/typescript/module/ui/client.d.ts +3 -1
  78. package/lib/typescript/module/ui/client.d.ts.map +1 -1
  79. package/lib/typescript/module/ui/components/FollowButton.d.ts +13 -0
  80. package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
  81. package/lib/typescript/module/ui/components/FollowTargetButton.d.ts +6 -18
  82. package/lib/typescript/module/ui/components/FollowTargetButton.d.ts.map +1 -1
  83. package/lib/typescript/module/ui/components/OxyConsentScreen.d.ts.map +1 -1
  84. package/lib/typescript/module/ui/components/authChooser/types.d.ts +2 -5
  85. package/lib/typescript/module/ui/components/authChooser/types.d.ts.map +1 -1
  86. package/lib/typescript/module/ui/components/followButtonAccessibility.d.ts +12 -2
  87. package/lib/typescript/module/ui/components/followButtonAccessibility.d.ts.map +1 -1
  88. package/lib/typescript/module/ui/components/followLabels.d.ts +28 -0
  89. package/lib/typescript/module/ui/components/followLabels.d.ts.map +1 -0
  90. package/lib/typescript/module/ui/components/followRules.d.ts +20 -9
  91. package/lib/typescript/module/ui/components/followRules.d.ts.map +1 -1
  92. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +3 -2
  93. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  94. package/lib/typescript/module/ui/hooks/useI18n.d.ts +3 -1
  95. package/lib/typescript/module/ui/hooks/useI18n.d.ts.map +1 -1
  96. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  97. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  98. package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  99. package/lib/typescript/module/ui/utils/deviceSessionTitle.d.ts +1 -2
  100. package/lib/typescript/module/ui/utils/deviceSessionTitle.d.ts.map +1 -1
  101. package/package.json +6 -6
  102. package/src/index.ts +2 -5
  103. package/src/ui/client.ts +7 -5
  104. package/src/ui/components/FollowButton.tsx +53 -23
  105. package/src/ui/components/FollowTargetButton.tsx +39 -52
  106. package/src/ui/components/OxyConsentScreen.tsx +1 -3
  107. package/src/ui/components/__tests__/followButtonAccessibility.test.tsx +32 -6
  108. package/src/ui/components/__tests__/followTargetButton.test.ts +7 -1
  109. package/src/ui/components/authChooser/__tests__/profileColors.test.ts +13 -0
  110. package/src/ui/components/authChooser/types.ts +17 -17
  111. package/src/ui/components/followButtonAccessibility.ts +38 -25
  112. package/src/ui/components/followLabels.ts +48 -0
  113. package/src/ui/components/followRules.ts +29 -13
  114. package/src/ui/hooks/useI18n.ts +5 -3
  115. package/src/ui/screens/AccountSettingsScreen.tsx +10 -29
  116. package/src/ui/screens/CreateAccountScreen.tsx +9 -27
  117. package/src/ui/screens/EditProfileFieldScreen.tsx +10 -8
  118. package/src/ui/utils/deviceSessionTitle.ts +1 -2
@@ -1 +1 @@
1
- {"version":3,"file":"useI18n.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useI18n.ts"],"names":[],"mappings":"AAWA;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO;aAWN,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;;EAI9D"}
1
+ {"version":3,"file":"useI18n.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useI18n.ts"],"names":[],"mappings":"AAWA,2FAA2F;AAC3F,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAC;AAExF;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO;;;EActB"}
@@ -1 +1 @@
1
- {"version":3,"file":"AccountSettingsScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/AccountSettingsScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAwB/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAa3D;;;;;;GAMG;AACH,QAAA,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAiRpD,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"AccountSettingsScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/AccountSettingsScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAoB/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAY3D;;;;;;GAMG;AACH,QAAA,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAmQpD,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"CreateAccountScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/CreateAccountScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA8B/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAsF3D;;;;;GAKG;AACH,QAAA,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CA6dlD,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"CreateAccountScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/CreateAccountScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA0B/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAqF3D;;;;;GAKG;AACH,QAAA,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAgdlD,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"EditProfileFieldScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/EditProfileFieldScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAI9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAuB3D;;GAEG;AACH,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,UAAU,GACV,KAAK,GACL,OAAO,GACP,SAAS,GACT,UAAU,GACV,UAAU,GACV,WAAW,GACX,OAAO,CAAC;AA4BZ,UAAU,2BAA4B,SAAQ,eAAe;IAC3D,6BAA6B;IAC7B,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;;AAyoBD,wBAAkD"}
1
+ {"version":3,"file":"EditProfileFieldScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/EditProfileFieldScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAI9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAuB3D;;GAEG;AACH,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,UAAU,GACV,KAAK,GACL,OAAO,GACP,SAAS,GACT,UAAU,GACV,UAAU,GACV,WAAW,GACX,OAAO,CAAC;AA4BZ,UAAU,2BAA4B,SAAQ,eAAe;IAC3D,6BAA6B;IAC7B,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;;AA2oBD,wBAAkD"}
@@ -1,5 +1,5 @@
1
1
  import { type DeviceLinkedSession } from '@oxy.so/core';
2
- type Translate = (key: string, vars?: Record<string, string | number>) => string;
2
+ import type { Translate } from '../hooks/useI18n.js';
3
3
  /**
4
4
  * The title of one row in "Sessions & devices".
5
5
  *
@@ -14,5 +14,4 @@ type Translate = (key: string, vars?: Record<string, string | number>) => string
14
14
  * response does carry. Never the string "undefined".
15
15
  */
16
16
  export declare function deviceSessionTitle(session: Pick<DeviceLinkedSession, 'deviceName' | 'isCurrent' | 'user'>, t: Translate): string;
17
- export {};
18
17
  //# sourceMappingURL=deviceSessionTitle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deviceSessionTitle.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/deviceSessionTitle.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEjF,KAAK,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAC;AAEjF;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,IAAI,CAAC,mBAAmB,EAAE,YAAY,GAAG,WAAW,GAAG,MAAM,CAAC,EACvE,CAAC,EAAE,SAAS,GACX,MAAM,CAQR"}
1
+ {"version":3,"file":"deviceSessionTitle.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/deviceSessionTitle.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAkB,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,IAAI,CAAC,mBAAmB,EAAE,YAAY,GAAG,WAAW,GAAG,MAAM,CAAC,EACvE,CAAC,EAAE,SAAS,GACX,MAAM,CAQR"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/services",
3
- "version": "12.4.0",
3
+ "version": "12.6.0",
4
4
  "description": "OxyHQ Expo/React Native SDK — UI components, screens, and native features",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -151,7 +151,7 @@
151
151
  "prepublishOnly": "node ../../scripts/assert-bun-publish.mjs && bun run typescript && bun run test && bun run build"
152
152
  },
153
153
  "dependencies": {
154
- "@oxy.so/contracts": "^5.1.0",
154
+ "@oxy.so/contracts": "^5.3.0",
155
155
  "color": "^5.0.3",
156
156
  "@peable.to/sdk": "0.4.1"
157
157
  },
@@ -160,7 +160,7 @@
160
160
  "@commitlint/config-conventional": "^21.2.0",
161
161
  "@expo/app-integrity": "~58.0.2",
162
162
  "@expo/vector-icons": "^15.0.3",
163
- "@oxy.so/core": "6.1.0",
163
+ "@oxy.so/core": "6.4.0",
164
164
  "@react-native-async-storage/async-storage": "^2.0.0",
165
165
  "@react-native-community/netinfo": "^12.0.1",
166
166
  "@react-native/babel-preset": "^0.88.0",
@@ -175,7 +175,7 @@
175
175
  "@types/jest": "^30.0.0",
176
176
  "@types/node": "^20.19.43",
177
177
  "@types/react": "*",
178
- "@oxy.so/bloom": "^11.1.1",
178
+ "@oxy.so/bloom": "12.0.0",
179
179
  "copyfiles": "^2.4.1",
180
180
  "expo-clipboard": "~58.0.3",
181
181
  "expo-constants": "~58.0.12",
@@ -210,12 +210,12 @@
210
210
  "expo-video": "~58.0.11"
211
211
  },
212
212
  "//@oxy.so/bloom": "Bloom 11.1.0 adds the shared grouped sections, usage card, subscription plans, frame appearance and overlay navigation APIs used by this SDK. First-party Bloom peers remain open above this tested minimum.",
213
- "//@oxy.so/core": "Core 6.1.0 supplies `oxy.auth.email.{startChange,change,remove}` and `oxy.auth.password.remove`, which the sign-in & security panels call, and their translations. Keep one Core instance per app; raise this floor with the root catalogue when new runtime APIs are required.",
213
+ "//@oxy.so/core": "Core 6.4.0 supplies the `follow.*` strings the follow buttons read for every verb and language; 6.1.0 the `oxy.auth.email.{startChange,change,remove}` and `oxy.auth.password.remove` the sign-in & security panels call. Keep one Core instance per app; raise this floor with the root catalogue when new runtime APIs are required.",
214
214
  "peerDependencies": {
215
215
  "@expo/app-integrity": ">=56.0.0 <59.0.0",
216
216
  "@expo/vector-icons": "^15.0.3",
217
217
  "@oxy.so/bloom": ">=11.1.0",
218
- "@oxy.so/core": "^6.1.0",
218
+ "@oxy.so/core": "^6.4.0",
219
219
  "@react-native-async-storage/async-storage": "^2.0.0",
220
220
  "@react-native-community/netinfo": ">=11.4.1 <13.0.0",
221
221
  "@tanstack/query-async-storage-persister": "^5.101",
package/src/index.ts CHANGED
@@ -344,11 +344,8 @@ export {
344
344
  FOLLOW_ACTION_LEAVES_ACTIVE,
345
345
  } from './ui/components/followRules';
346
346
  export type { FollowDuration, FollowMenuItem } from './ui/components/followRules';
347
- export type {
348
- FollowTargetButtonProps,
349
- FollowVerb,
350
- FollowLabels,
351
- } from './ui/components/FollowTargetButton';
347
+ export type { FollowTargetButtonProps } from './ui/components/FollowTargetButton';
348
+ export type { FollowVerb, FollowLabels } from './ui/components/followLabels';
352
349
  export { useFollowTarget } from './ui/hooks/useFollowTarget';
353
350
  export type { UseFollowTargetResult } from './ui/hooks/useFollowTarget';
354
351
  export {
package/src/ui/client.ts CHANGED
@@ -23,12 +23,14 @@ export type { OxyAuthPromptProps } from './components/OxyAuthPrompt';
23
23
  export { LogoIcon } from './components/logo/LogoIcon';
24
24
  export { LogoText } from './components/logo/LogoText';
25
25
  export { default as FollowButton } from './components/FollowButton';
26
- export { FollowTargetButton } from './components/FollowTargetButton';
27
26
  export type {
28
- FollowTargetButtonProps,
29
- FollowVerb,
30
- FollowLabels,
31
- } from './components/FollowTargetButton';
27
+ FollowButtonProps,
28
+ SingleFollowButtonProps,
29
+ MultiFollowButtonProps,
30
+ } from './components/FollowButton';
31
+ export { FollowTargetButton } from './components/FollowTargetButton';
32
+ export type { FollowTargetButtonProps } from './components/FollowTargetButton';
33
+ export type { FollowVerb, FollowLabels } from './components/followLabels';
32
34
  export { default as ProfileButton } from './components/ProfileButton';
33
35
  export type { ProfileButtonProps } from './components/ProfileButton';
34
36
  export { default as PeableButton } from './components/PeableButton';
@@ -8,10 +8,9 @@ import { useFollow, useFollowForButton } from '../hooks/useFollow';
8
8
  import { useFollowStore } from '../stores/followStore';
9
9
  import type { OxyServices, BulkFollowResult, BulkUnfollowResult } from '@oxy.so/core';
10
10
  import { useShallow } from 'zustand/react/shallow';
11
+ import { useI18n } from '../hooks/useI18n';
11
12
  import { describeFollowAllButton, describeFollowButton } from './followButtonAccessibility';
12
-
13
- const DEFAULT_FOLLOW_ALL_LABEL = 'Follow all';
14
- const DEFAULT_FOLLOWED_ALL_LABEL = 'Following';
13
+ import { type FollowLabels, type FollowVerb, useFollowLabels } from './followLabels';
15
14
 
16
15
  /** Props shared by both single- and multi-user follow modes. */
17
16
  interface FollowButtonBaseProps {
@@ -35,6 +34,16 @@ export interface SingleFollowButtonProps extends FollowButtonBaseProps {
35
34
  * still follows the state.
36
35
  */
37
36
  username?: string;
37
+ /**
38
+ * The word for following this account: a fediverse community reads
39
+ * "Join"/"Joined". Translated, like every default label here.
40
+ */
41
+ verb?: FollowVerb;
42
+ /**
43
+ * Overrides for a verb the vocabulary does not carry. This button shows
44
+ * `idle` and `active`; a user follow has no pending or switched-off state.
45
+ */
46
+ labels?: Partial<FollowLabels>;
38
47
  userIds?: never;
39
48
  }
40
49
 
@@ -42,7 +51,9 @@ export interface SingleFollowButtonProps extends FollowButtonBaseProps {
42
51
  export interface MultiFollowButtonProps extends FollowButtonBaseProps {
43
52
  userIds: string[];
44
53
  initiallyAllFollowing?: boolean;
54
+ /** Default: "Follow all", translated. */
45
55
  followAllLabel?: string;
56
+ /** Default: "Following", translated. */
46
57
  followedAllLabel?: string;
47
58
  onBulkFollow?: (result: BulkFollowResult) => void;
48
59
  onBulkUnfollow?: (result: BulkUnfollowResult) => void;
@@ -63,6 +74,8 @@ const FollowButtonInner = memo(function FollowButtonInner({
63
74
  oxyServices,
64
75
  initiallyFollowing,
65
76
  username,
77
+ verb = 'follow',
78
+ labels,
66
79
  size = 'md',
67
80
  onFollowChange,
68
81
  style,
@@ -71,6 +84,8 @@ const FollowButtonInner = memo(function FollowButtonInner({
71
84
  showLoadingState = true,
72
85
  preventParentActions = true,
73
86
  }: SingleFollowButtonProps & { oxyServices: OxyServices }) {
87
+ const { t } = useI18n();
88
+ const text = useFollowLabels(verb, labels);
74
89
  const { isFollowing, isKnown, isLoading, toggleFollow, resolveStatus } = useFollowForButton(
75
90
  userId,
76
91
  oxyServices,
@@ -91,10 +106,19 @@ const FollowButtonInner = memo(function FollowButtonInner({
91
106
  if (accepted) onFollowChange?.(!isFollowing);
92
107
  } catch (err: unknown) {
93
108
  const error = err instanceof Error ? err : new Error(String(err));
94
- toast.error(error.message || 'Failed to update follow status');
109
+ toast.error(error.message || t('follow.errors.update'));
95
110
  }
96
111
  },
97
- [disabled, isLoading, isKnown, toggleFollow, onFollowChange, isFollowing, preventParentActions],
112
+ [
113
+ disabled,
114
+ isLoading,
115
+ isKnown,
116
+ toggleFollow,
117
+ onFollowChange,
118
+ isFollowing,
119
+ preventParentActions,
120
+ t,
121
+ ],
98
122
  );
99
123
 
100
124
  // Enqueue a batched status fetch ONLY when the status is genuinely unknown
@@ -110,12 +134,10 @@ const FollowButtonInner = memo(function FollowButtonInner({
110
134
  const isBusy = isLoading || !isKnown;
111
135
  const showFollowing = isKnown && isFollowing;
112
136
  const showSpinner = showLoadingState && isBusy;
113
- const { accessibilityLabel, accessibilityHint } = describeFollowButton({
114
- isKnown,
115
- isFollowing: showFollowing,
116
- isPending: isLoading,
117
- username,
118
- });
137
+ const { accessibilityLabel, accessibilityHint } = describeFollowButton(
138
+ { isKnown, isFollowing: showFollowing, isPending: isLoading, username, verb, labels: text },
139
+ t,
140
+ );
119
141
 
120
142
  return (
121
143
  <BloomFollowButton
@@ -128,7 +150,8 @@ const FollowButtonInner = memo(function FollowButtonInner({
128
150
  disabled={disabled || isBusy}
129
151
  style={style}
130
152
  textStyle={textStyle}
131
- label={isKnown || showSpinner ? 'Follow' : ''}
153
+ label={isKnown || showSpinner ? text.idle : ''}
154
+ followingLabel={text.active}
132
155
  accessibilityLabel={accessibilityLabel}
133
156
  accessibilityHint={accessibilityHint}
134
157
  />
@@ -139,8 +162,8 @@ const FollowButtonMultiInner = memo(function FollowButtonMultiInner({
139
162
  userIds,
140
163
  initiallyAllFollowing = false,
141
164
  size = 'md',
142
- followAllLabel = DEFAULT_FOLLOW_ALL_LABEL,
143
- followedAllLabel = DEFAULT_FOLLOWED_ALL_LABEL,
165
+ followAllLabel: followAllLabelProp,
166
+ followedAllLabel: followedAllLabelProp,
144
167
  onFollowChange,
145
168
  onBulkFollow,
146
169
  onBulkUnfollow,
@@ -150,6 +173,9 @@ const FollowButtonMultiInner = memo(function FollowButtonMultiInner({
150
173
  showLoadingState = true,
151
174
  preventParentActions = true,
152
175
  }: MultiFollowButtonProps) {
176
+ const { t } = useI18n();
177
+ const followAllLabel = followAllLabelProp ?? t('follow.all.idle');
178
+ const followedAllLabel = followedAllLabelProp ?? t('follow.verbs.follow.active');
153
179
  const follow = useFollow(userIds);
154
180
  const followAllUsers = 'followAllUsers' in follow ? follow.followAllUsers : undefined;
155
181
  const unfollowAllUsers = 'unfollowAllUsers' in follow ? follow.unfollowAllUsers : undefined;
@@ -209,7 +235,7 @@ const FollowButtonMultiInner = memo(function FollowButtonMultiInner({
209
235
  onBulkUnfollow?.(result);
210
236
  } catch (err: unknown) {
211
237
  const error = err instanceof Error ? err : new Error(String(err));
212
- toast.error(error.message || 'Failed to update follow status');
238
+ toast.error(error.message || t('follow.errors.update'));
213
239
  } finally {
214
240
  setIsSubmitting(false);
215
241
  }
@@ -236,7 +262,7 @@ const FollowButtonMultiInner = memo(function FollowButtonMultiInner({
236
262
  onBulkFollow?.(result);
237
263
  } catch (err: unknown) {
238
264
  const error = err instanceof Error ? err : new Error(String(err));
239
- toast.error(error.message || 'Failed to update follow status');
265
+ toast.error(error.message || t('follow.errors.update'));
240
266
  } finally {
241
267
  setIsSubmitting(false);
242
268
  }
@@ -251,17 +277,21 @@ const FollowButtonMultiInner = memo(function FollowButtonMultiInner({
251
277
  onBulkFollow,
252
278
  onBulkUnfollow,
253
279
  preventParentActions,
280
+ t,
254
281
  ],
255
282
  );
256
283
 
257
284
  const showSpinner = showLoadingState && isLoading;
258
- const { accessibilityLabel, accessibilityHint } = describeFollowAllButton({
259
- allFollowing,
260
- isPending: Boolean(isLoading),
261
- count: userIds.length,
262
- followAllLabel,
263
- followedAllLabel,
264
- });
285
+ const { accessibilityLabel, accessibilityHint } = describeFollowAllButton(
286
+ {
287
+ allFollowing,
288
+ isPending: Boolean(isLoading),
289
+ count: userIds.length,
290
+ followAllLabel,
291
+ followedAllLabel,
292
+ },
293
+ t,
294
+ );
265
295
 
266
296
  return (
267
297
  <BloomFollowButton
@@ -22,6 +22,8 @@
22
22
  * small vocabulary of defaults and a full override, rather than a lookup keyed
23
23
  * on kind — a kind→verb table in this package would need a release every time
24
24
  * an application invents a kind, which is exactly the coupling #809 removes.
25
+ * The vocabulary and its translated words live in `followLabels`, shared with
26
+ * `FollowButton`.
25
27
  *
26
28
  * ## Relationship to the legacy `FollowButton`
27
29
  *
@@ -58,55 +60,28 @@ import {
58
60
  type FollowDuration,
59
61
  type FollowMenuItem,
60
62
  } from './followRules';
61
-
62
- /**
63
- * The small vocabulary of verbs the ecosystem actually uses. An application
64
- * whose verb is not here passes `labels` instead — the escape hatch exists so
65
- * this union never has to grow to accommodate one product's wording.
66
- */
67
- export type FollowVerb = 'follow' | 'subscribe' | 'join' | 'watch';
68
-
69
- export interface FollowLabels {
70
- /** Not following yet. */
71
- idle: string;
72
- /** Following, and this application acts on it. */
73
- active: string;
74
- /** Asked, waiting for the other side to accept. */
75
- pending: string;
76
- /** Following globally, but switched off in this application. */
77
- disabled: string;
78
- }
79
-
80
- const VERB_LABELS: Record<FollowVerb, FollowLabels> = {
81
- follow: { idle: 'Follow', active: 'Following', pending: 'Requested', disabled: 'Off here' },
82
- subscribe: {
83
- idle: 'Subscribe',
84
- active: 'Subscribed',
85
- pending: 'Requested',
86
- disabled: 'Off here',
87
- },
88
- join: { idle: 'Join', active: 'Joined', pending: 'Requested', disabled: 'Off here' },
89
- watch: { idle: 'Watch', active: 'Watching', pending: 'Requested', disabled: 'Off here' },
90
- };
63
+ import { type FollowLabels, type FollowVerb, useFollowLabels } from './followLabels';
64
+ import { useI18n } from '../hooks/useI18n';
91
65
 
92
66
  const HOUR = 60 * 60;
93
67
 
94
68
  /**
95
69
  * Defaults chosen for the cases a timed follow is actually for: a live event
96
70
  * tonight, a story running this week. Not a general-purpose duration picker —
97
- * an application that needs one passes its own.
71
+ * an application that needs one passes its own. Labels are catalog keys, read
72
+ * at render in the app's language.
98
73
  */
99
- const DEFAULT_DURATIONS: FollowDuration[] = [
100
- { label: '24 hours', seconds: 24 * HOUR },
101
- { label: '72 hours', seconds: 72 * HOUR },
102
- { label: 'A week', seconds: 7 * 24 * HOUR },
103
- ];
74
+ const DEFAULT_DURATIONS = [
75
+ { key: 'follow.durations.day', seconds: 24 * HOUR },
76
+ { key: 'follow.durations.threeDays', seconds: 72 * HOUR },
77
+ { key: 'follow.durations.week', seconds: 7 * 24 * HOUR },
78
+ ] as const;
104
79
 
105
80
  export interface FollowTargetButtonProps {
106
81
  /** The registered target's id. Registration is separate — see the SDK. */
107
82
  targetId: string;
108
83
  verb?: FollowVerb;
109
- /** Full override, for a verb the vocabulary above does not carry. */
84
+ /** Full override, for a verb the vocabulary does not carry. */
110
85
  labels?: Partial<FollowLabels>;
111
86
  size?: 'sm' | 'md' | 'lg';
112
87
  style?: StyleProp<ViewStyle>;
@@ -118,7 +93,8 @@ export interface FollowTargetButtonProps {
118
93
  showOptions?: boolean;
119
94
  /**
120
95
  * Offer timed follows. Pass `false` where a temporary follow makes no sense —
121
- * a store you buy from, an account you know.
96
+ * a store you buy from, an account you know. Default: 24 hours, 72 hours and
97
+ * a week, translated.
122
98
  */
123
99
  durations?: FollowDuration[] | false;
124
100
  /**
@@ -156,7 +132,7 @@ export const FollowTargetButton = memo(function FollowTargetButton({
156
132
  style,
157
133
  disabled = false,
158
134
  showOptions = true,
159
- durations = DEFAULT_DURATIONS,
135
+ durations,
160
136
  applicationName,
161
137
  initialStatus,
162
138
  onChange,
@@ -164,13 +140,18 @@ export const FollowTargetButton = memo(function FollowTargetButton({
164
140
  const { status, isFollowing, isUnknown, isPending, follow, unfollow, disableHere, enableHere } =
165
141
  useFollowTarget(targetId, { initialStatus });
166
142
 
143
+ const { t } = useI18n();
144
+ const text = useFollowLabels(verb, labels);
145
+
146
+ const timedChoices = useMemo<FollowDuration[] | false>(
147
+ () => durations ?? DEFAULT_DURATIONS.map(({ key, seconds }) => ({ label: t(key), seconds })),
148
+ [durations, t],
149
+ );
150
+
167
151
  const reportMutationFailure = useCallback(() => {
168
- const message =
169
- useFollowTargetStore.getState().errors[targetId] ?? 'Could not update follow status';
152
+ const message = useFollowTargetStore.getState().errors[targetId] ?? t('follow.errors.update');
170
153
  toast.error(message);
171
- }, [targetId]);
172
-
173
- const text = useMemo(() => ({ ...VERB_LABELS[verb], ...labels }), [verb, labels]);
154
+ }, [targetId, t]);
174
155
 
175
156
  const label = useMemo(() => {
176
157
  if (status.globalState === 'requested') return text.pending;
@@ -224,9 +205,9 @@ export const FollowTargetButton = memo(function FollowTargetButton({
224
205
  // The confirmation is the point: a follow that ends on its own is a
225
206
  // promise, and a user who does not see it made will not believe it.
226
207
  // Which is also why it must not appear when the promise was not made.
227
- toast.success(`Following for ${durationLabel.toLowerCase()}`);
208
+ toast.success(t('follow.menu.forDuration', { label: text.active, duration: durationLabel }));
228
209
  },
229
- [follow, onChange, reportMutationFailure],
210
+ [follow, onChange, reportMutationFailure, t, text.active],
230
211
  );
231
212
 
232
213
  const menuItems = useMemo(
@@ -236,18 +217,22 @@ export const FollowTargetButton = memo(function FollowTargetButton({
236
217
  applicationMode: status.applicationMode,
237
218
  hasRelationship: Boolean(status.relationshipId),
238
219
  isPending,
239
- durations,
220
+ durations: timedChoices,
240
221
  idleVerb: text.idle,
241
- applicationName: applicationName ?? 'this app',
222
+ applicationName: applicationName ?? t('follow.menu.thisApp'),
223
+ verb,
224
+ t,
242
225
  }),
243
226
  [
244
227
  isFollowing,
245
228
  status.applicationMode,
246
229
  status.relationshipId,
247
230
  isPending,
248
- durations,
231
+ timedChoices,
249
232
  text.idle,
250
233
  applicationName,
234
+ verb,
235
+ t,
251
236
  ],
252
237
  );
253
238
 
@@ -304,6 +289,8 @@ export const FollowTargetButton = memo(function FollowTargetButton({
304
289
  return <View style={style}>{primary}</View>;
305
290
  }
306
291
 
292
+ const optionsLabel = t('follow.menu.options', { label });
293
+
307
294
  return (
308
295
  <View style={[{ flexDirection: 'row', alignItems: 'center', gap: 4 }, style]}>
309
296
  {primary}
@@ -321,11 +308,11 @@ export const FollowTargetButton = memo(function FollowTargetButton({
321
308
  size={size === 'lg' ? 'lg' : 'sm'}
322
309
  icon={<ChevronDown width={16} />}
323
310
  disabled={disabled || isUnknown}
324
- accessibilityLabel={`${label} options`}
325
- accessibilityHint="Opens follow options"
311
+ accessibilityLabel={optionsLabel}
312
+ accessibilityHint={t('follow.menu.optionsHint')}
326
313
  />
327
314
  </DropdownMenuTrigger>
328
- <DropdownMenuContent label={`${label} options`}>
315
+ <DropdownMenuContent label={optionsLabel}>
329
316
  {menuItems.map((item) => (
330
317
  <DropdownMenuItem key={item.key} onPress={() => runItem(item)}>
331
318
  {item.label}
@@ -27,7 +27,7 @@ import { Avatar } from '@oxy.so/bloom/avatar';
27
27
  import { Button, LinkButton } from '@oxy.so/bloom/button';
28
28
  import { Text } from '@oxy.so/bloom/typography';
29
29
  import { useTheme } from '@oxy.so/bloom/theme';
30
- import { useI18n } from '../hooks/useI18n';
30
+ import { type Translate, useI18n } from '../hooks/useI18n';
31
31
  import { LogoIcon } from './logo/LogoIcon';
32
32
 
33
33
  /** The requesting application, resolved by the caller (never user-supplied raw). */
@@ -133,8 +133,6 @@ const SCOPE_LABEL_KEYS: Readonly<Record<string, string>> = {
133
133
  'federation:write': 'consent.scopes.federationWrite',
134
134
  };
135
135
 
136
- type Translate = ReturnType<typeof useI18n>['t'];
137
-
138
136
  /** One permission row of the "Permissions requested" list. */
139
137
  interface PermissionRow {
140
138
  /** Stable list key / testID suffix — the first scope that produced this row. */
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import React from 'react';
11
11
  import { render } from '@testing-library/react';
12
+ import { translate } from '@oxy.so/core';
12
13
  import {
13
14
  describeFollowAllButton,
14
15
  describeFollowButton,
@@ -52,6 +53,7 @@ jest.mock('../../hooks/useFollow', () => ({
52
53
 
53
54
  jest.mock('../../context/oxyRuntimeContext', () => ({
54
55
  useOxy: () => ({ oxyServices: {}, canUsePrivateApi: true, user: { id: 'viewer' } }),
56
+ useOptionalOxy: () => null,
55
57
  }));
56
58
 
57
59
  let mockTarget = {
@@ -73,8 +75,16 @@ jest.mock('../../hooks/useFollowTarget', () => ({
73
75
  import { FollowButton } from '../FollowButton';
74
76
  import { FollowTargetButton } from '../FollowTargetButton';
75
77
 
78
+ const t = (key: string, vars?: Record<string, string | number>) => translate('en-US', key, vars);
79
+
76
80
  describe('describeFollowButton', () => {
77
- const known = { isKnown: true, isPending: false, username: 'nate' };
81
+ const known = {
82
+ isKnown: true,
83
+ isPending: false,
84
+ username: 'nate',
85
+ verb: 'follow' as const,
86
+ labels: { idle: 'Follow', active: 'Following' },
87
+ };
78
88
 
79
89
  it.each([
80
90
  [{ ...known, isFollowing: true }, 'Following @nate', 'Unfollows @nate'],
@@ -94,7 +104,7 @@ describe('describeFollowButton', () => {
94
104
  undefined,
95
105
  ],
96
106
  ])('%o reads "%s"', (input, label, hint) => {
97
- expect(describeFollowButton(input)).toEqual(
107
+ expect(describeFollowButton(input, t)).toEqual(
98
108
  hint ? { accessibilityLabel: label, accessibilityHint: hint } : { accessibilityLabel: label },
99
109
  );
100
110
  });
@@ -112,22 +122,22 @@ describe('describeFollowAllButton', () => {
112
122
 
113
123
  it('names the state and counts the accounts in the hint', () => {
114
124
  expect(
115
- describeFollowAllButton({ ...labels, allFollowing: false, isPending: false, count: 3 }),
125
+ describeFollowAllButton({ ...labels, allFollowing: false, isPending: false, count: 3 }, t),
116
126
  ).toEqual({
117
127
  accessibilityLabel: 'Follow all',
118
128
  accessibilityHint: 'Follows all 3 accounts',
119
129
  });
120
130
  expect(
121
- describeFollowAllButton({ ...labels, allFollowing: true, isPending: false, count: 1 }),
131
+ describeFollowAllButton({ ...labels, allFollowing: true, isPending: false, count: 1 }, t),
122
132
  ).toEqual({
123
133
  accessibilityLabel: 'Following',
124
- accessibilityHint: 'Unfollows all 1 account',
134
+ accessibilityHint: 'Unfollows 1 account',
125
135
  });
126
136
  });
127
137
 
128
138
  it('drops the hint while the bulk write is in flight', () => {
129
139
  expect(
130
- describeFollowAllButton({ ...labels, allFollowing: true, isPending: true, count: 3 }),
140
+ describeFollowAllButton({ ...labels, allFollowing: true, isPending: true, count: 3 }, t),
131
141
  ).toEqual({
132
142
  accessibilityLabel: 'Following',
133
143
  });
@@ -178,6 +188,22 @@ describe('FollowButton hands Bloom a name that follows the state', () => {
178
188
  });
179
189
  });
180
190
 
191
+ it('reads its verb: "Join" and "Joined", named and hinted to match', () => {
192
+ mockFollowState = { isFollowing: false, isKnown: true, isLoading: false };
193
+ render(<FollowButton userId="c-id" username="fediverse" verb="join" />);
194
+ expect(lastBloomProps()).toMatchObject({
195
+ label: 'Join',
196
+ followingLabel: 'Joined',
197
+ accessibilityLabel: 'Join @fediverse',
198
+ });
199
+ mockFollowState = { isFollowing: true, isKnown: true, isLoading: false };
200
+ render(<FollowButton userId="c-id" username="fediverse" verb="join" />);
201
+ expect(lastBloomProps()).toMatchObject({
202
+ accessibilityLabel: 'Joined @fediverse',
203
+ accessibilityHint: 'Leaves @fediverse',
204
+ });
205
+ });
206
+
181
207
  it('names the "Follow all" button by its state too', () => {
182
208
  mockMulti = { allFollowing: true, isAnyLoading: false };
183
209
  render(<FollowButton userIds={['a', 'b']} />);
@@ -28,7 +28,7 @@ import {
28
28
 
29
29
  const DURATIONS = [
30
30
  { label: '24 hours', seconds: 86400 },
31
- { label: 'A week', seconds: 604800 },
31
+ { label: 'a week', seconds: 604800 },
32
32
  ];
33
33
 
34
34
  const base = {
@@ -97,6 +97,12 @@ describe('buildFollowMenuItems', () => {
97
97
  expect(buildFollowMenuItems(following)[0].label).toBe('Don’t show in Mention');
98
98
  });
99
99
 
100
+ it('words "everywhere" with the button’s verb', () => {
101
+ expect(buildFollowMenuItems({ ...following, verb: 'join' })[1].label).toBe(
102
+ 'Leave everywhere',
103
+ );
104
+ });
105
+
100
106
  it('offers turning it back on once it is off here', () => {
101
107
  const items = buildFollowMenuItems({ ...following, applicationMode: 'disabled' });
102
108
  expect(keys(items)).toEqual(['enable-here', 'unfollow-everywhere']);
@@ -0,0 +1,13 @@
1
+ import { resolveAccentHex } from '../types';
2
+ import { APP_COLOR_PRESETS } from '@oxy.so/bloom/color-presets';
3
+
4
+ jest.mock('@oxy.so/bloom/theme', () => jest.requireActual('@oxy.so/bloom/color-presets'));
5
+
6
+ it('preserves the account identity for every app using the shared chooser', () => {
7
+ expect(resolveAccentHex('grove', '#fff')).toBe(APP_COLOR_PRESETS.grove.hex);
8
+ expect(resolveAccentHex('amber', '#fff')).toBe(APP_COLOR_PRESETS.yellow.hex);
9
+ expect(resolveAccentHex('#ABC', '#fff')).toBe('#aabbcc');
10
+ expect(resolveAccentHex('#123456', '#fff')).toBe('#123456');
11
+ expect(resolveAccentHex(null, '#fff')).toBe('#fff');
12
+ expect(resolveAccentHex('unknown', '#fff')).toBe('#fff');
13
+ });