@oxy.so/services 9.1.0 → 9.2.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 (38) hide show
  1. package/lib/commonjs/index.js +4 -3
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/client.js +174 -0
  4. package/lib/commonjs/ui/client.js.map +1 -1
  5. package/lib/commonjs/ui/components/FollowTargetButton.js +11 -101
  6. package/lib/commonjs/ui/components/FollowTargetButton.js.map +1 -1
  7. package/lib/commonjs/ui/components/followRules.js +105 -0
  8. package/lib/commonjs/ui/components/followRules.js.map +1 -0
  9. package/lib/module/index.js +3 -2
  10. package/lib/module/index.js.map +1 -1
  11. package/lib/module/ui/client.js +17 -4
  12. package/lib/module/ui/client.js.map +1 -1
  13. package/lib/module/ui/components/FollowTargetButton.js +1 -89
  14. package/lib/module/ui/components/FollowTargetButton.js.map +1 -1
  15. package/lib/module/ui/components/followRules.js +99 -0
  16. package/lib/module/ui/components/followRules.js.map +1 -0
  17. package/lib/typescript/commonjs/index.d.ts +4 -2
  18. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  19. package/lib/typescript/commonjs/ui/client.d.ts +19 -4
  20. package/lib/typescript/commonjs/ui/client.d.ts.map +1 -1
  21. package/lib/typescript/commonjs/ui/components/FollowTargetButton.d.ts +2 -62
  22. package/lib/typescript/commonjs/ui/components/FollowTargetButton.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/ui/components/followRules.d.ts +71 -0
  24. package/lib/typescript/commonjs/ui/components/followRules.d.ts.map +1 -0
  25. package/lib/typescript/module/index.d.ts +4 -2
  26. package/lib/typescript/module/index.d.ts.map +1 -1
  27. package/lib/typescript/module/ui/client.d.ts +19 -4
  28. package/lib/typescript/module/ui/client.d.ts.map +1 -1
  29. package/lib/typescript/module/ui/components/FollowTargetButton.d.ts +2 -62
  30. package/lib/typescript/module/ui/components/FollowTargetButton.d.ts.map +1 -1
  31. package/lib/typescript/module/ui/components/followRules.d.ts +71 -0
  32. package/lib/typescript/module/ui/components/followRules.d.ts.map +1 -0
  33. package/package.json +1 -1
  34. package/src/index.ts +3 -3
  35. package/src/ui/client.ts +55 -4
  36. package/src/ui/components/FollowTargetButton.tsx +8 -112
  37. package/src/ui/components/__tests__/followTargetButton.test.ts +1 -1
  38. package/src/ui/components/followRules.ts +121 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The follow button's product rules, without the button.
3
+ *
4
+ * Pure: no React, no Bloom. `FollowTargetButton` renders them, and an
5
+ * application drawing its own affordance (a chip grid, a compact row) imports
6
+ * them from here — through `@oxy.so/services/ui/client` — without paying for the
7
+ * button's dropdown menu.
8
+ */
9
+
10
+ import type { FollowApplicationMode } from '@oxy.so/contracts';
11
+
12
+ /** A timed-follow choice offered in the menu. */
13
+ export interface FollowDuration {
14
+ label: string;
15
+ seconds: number;
16
+ }
17
+
18
+ /**
19
+ * Whether an action leaves the target ACTIVE in this application.
20
+ *
21
+ * One table, read by both the primary button and the menu, because the same
22
+ * action reached from two controls once reported differently — and a rule
23
+ * living in two switch statements is a rule that drifts. `Record` rather than
24
+ * a function with a default, so a new action is a compile error instead of a
25
+ * silent `false`.
26
+ */
27
+ export const FOLLOW_ACTION_LEAVES_ACTIVE: Record<
28
+ 'follow' | 'follow-timed' | 'enable-here' | 'disable-here' | 'unfollow',
29
+ boolean
30
+ > = {
31
+ follow: true,
32
+ 'follow-timed': true,
33
+ // Re-enabling here does not change the global follow — the user already had
34
+ // it — but it does change whether this application acts on it, which is what
35
+ // a mirror is asking about.
36
+ 'enable-here': true,
37
+ 'disable-here': false,
38
+ unfollow: false,
39
+ };
40
+
41
+ /** One line in the disclosure menu. */
42
+ export interface FollowMenuItem {
43
+ key: string;
44
+ label: string;
45
+ /** What the component should call. Named so the table below stays pure. */
46
+ action:
47
+ | { type: 'follow-timed'; seconds: number; durationLabel: string }
48
+ | { type: 'enable-here' }
49
+ | { type: 'disable-here' }
50
+ | { type: 'unfollow' };
51
+ }
52
+
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
+ * What the main button should do for the current state.
64
+ *
65
+ * Exported because the product rule is not obvious from the label alone: a
66
+ * follow switched off here still reads as "following" globally, so the primary
67
+ * press must re-enable here — not unfollow everywhere.
68
+ */
69
+ export function resolveFollowPrimaryAction(input: {
70
+ isFollowing: boolean;
71
+ applicationMode: FollowApplicationMode;
72
+ }): 'follow' | 'unfollow' | 'enable-here' {
73
+ if (!input.isFollowing) return 'follow';
74
+ if (input.applicationMode === 'disabled') return 'enable-here';
75
+ return 'unfollow';
76
+ }
77
+
78
+ export function buildFollowMenuItems(input: {
79
+ following: boolean;
80
+ applicationMode: FollowApplicationMode;
81
+ hasRelationship: boolean;
82
+ isPending: boolean;
83
+ durations: FollowDuration[] | false;
84
+ idleVerb: string;
85
+ applicationName: string;
86
+ }): FollowMenuItem[] {
87
+ const items: FollowMenuItem[] = [];
88
+
89
+ if (!input.following) {
90
+ if (input.durations === false) return items;
91
+ for (const d of input.durations) {
92
+ items.push({
93
+ key: `for-${d.seconds}`,
94
+ label: `${input.idleVerb} for ${d.label.toLowerCase()}`,
95
+ action: { type: 'follow-timed', seconds: d.seconds, durationLabel: d.label },
96
+ });
97
+ }
98
+ return items;
99
+ }
100
+
101
+ if (!input.hasRelationship || input.isPending) return items;
102
+
103
+ items.push(
104
+ input.applicationMode === 'disabled'
105
+ ? { key: 'enable-here', label: `Show in ${input.applicationName}`, action: { type: 'enable-here' } }
106
+ : {
107
+ key: 'disable-here',
108
+ label: `Don’t show in ${input.applicationName}`,
109
+ action: { type: 'disable-here' },
110
+ }
111
+ );
112
+ items.push({
113
+ // Named for what it does. "Unfollow" beside "don't show here" would read as
114
+ // the same action twice, and the user would pick the wrong one.
115
+ key: 'unfollow-everywhere',
116
+ label: 'Unfollow everywhere',
117
+ action: { type: 'unfollow' },
118
+ });
119
+
120
+ return items;
121
+ }