@appsupportkit/support-sdk 0.1.2 → 0.3.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/api.js +17 -2
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/components/BottomSheet.js +67 -47
- package/lib/commonjs/components/BottomSheet.js.map +1 -1
- package/lib/commonjs/components/LauncherButton.js +177 -0
- package/lib/commonjs/components/LauncherButton.js.map +1 -0
- package/lib/commonjs/components/SupportButton.js +168 -190
- package/lib/commonjs/components/SupportButton.js.map +1 -1
- package/lib/commonjs/components/icons.js +110 -23
- package/lib/commonjs/components/icons.js.map +1 -1
- package/lib/commonjs/components/primitives.js +191 -44
- package/lib/commonjs/components/primitives.js.map +1 -1
- package/lib/commonjs/components/screens/ComposeScreen.js +108 -60
- package/lib/commonjs/components/screens/ComposeScreen.js.map +1 -1
- package/lib/commonjs/components/screens/HomeScreen.js +221 -118
- package/lib/commonjs/components/screens/HomeScreen.js.map +1 -1
- package/lib/commonjs/components/screens/LanguageScreen.js +150 -0
- package/lib/commonjs/components/screens/LanguageScreen.js.map +1 -0
- package/lib/commonjs/components/screens/ThreadScreen.js +237 -111
- package/lib/commonjs/components/screens/ThreadScreen.js.map +1 -1
- package/lib/commonjs/i18n/index.js +101 -0
- package/lib/commonjs/i18n/index.js.map +1 -0
- package/lib/commonjs/i18n/languages.js +95 -0
- package/lib/commonjs/i18n/languages.js.map +1 -0
- package/lib/commonjs/i18n/strings/de.js +72 -0
- package/lib/commonjs/i18n/strings/de.js.map +1 -0
- package/lib/commonjs/i18n/strings/en.js +88 -0
- package/lib/commonjs/i18n/strings/en.js.map +1 -0
- package/lib/commonjs/i18n/strings/es.js +72 -0
- package/lib/commonjs/i18n/strings/es.js.map +1 -0
- package/lib/commonjs/i18n/strings/fr.js +72 -0
- package/lib/commonjs/i18n/strings/fr.js.map +1 -0
- package/lib/commonjs/i18n/strings/hi.js +72 -0
- package/lib/commonjs/i18n/strings/hi.js.map +1 -0
- package/lib/commonjs/i18n/strings/it.js +72 -0
- package/lib/commonjs/i18n/strings/it.js.map +1 -0
- package/lib/commonjs/i18n/strings/ja.js +72 -0
- package/lib/commonjs/i18n/strings/ja.js.map +1 -0
- package/lib/commonjs/i18n/strings/ko.js +72 -0
- package/lib/commonjs/i18n/strings/ko.js.map +1 -0
- package/lib/commonjs/i18n/strings/nl.js +72 -0
- package/lib/commonjs/i18n/strings/nl.js.map +1 -0
- package/lib/commonjs/i18n/strings/pl.js +72 -0
- package/lib/commonjs/i18n/strings/pl.js.map +1 -0
- package/lib/commonjs/i18n/strings/pt.js +72 -0
- package/lib/commonjs/i18n/strings/pt.js.map +1 -0
- package/lib/commonjs/i18n/strings/ru.js +72 -0
- package/lib/commonjs/i18n/strings/ru.js.map +1 -0
- package/lib/commonjs/i18n/strings/tr.js +72 -0
- package/lib/commonjs/i18n/strings/tr.js.map +1 -0
- package/lib/commonjs/i18n/strings/zh.js +72 -0
- package/lib/commonjs/i18n/strings/zh.js.map +1 -0
- package/lib/commonjs/index.js +31 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/theme.js +136 -17
- package/lib/commonjs/theme.js.map +1 -1
- package/lib/commonjs/types.js +18 -0
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/unread.js +92 -0
- package/lib/commonjs/unread.js.map +1 -0
- package/lib/commonjs/useSupportThreads.js +296 -0
- package/lib/commonjs/useSupportThreads.js.map +1 -0
- package/lib/commonjs/utils.js +58 -10
- package/lib/commonjs/utils.js.map +1 -1
- package/lib/module/api.js +16 -2
- package/lib/module/api.js.map +1 -1
- package/lib/module/components/BottomSheet.js +68 -48
- package/lib/module/components/BottomSheet.js.map +1 -1
- package/lib/module/components/LauncherButton.js +172 -0
- package/lib/module/components/LauncherButton.js.map +1 -0
- package/lib/module/components/SupportButton.js +172 -194
- package/lib/module/components/SupportButton.js.map +1 -1
- package/lib/module/components/icons.js +107 -22
- package/lib/module/components/icons.js.map +1 -1
- package/lib/module/components/primitives.js +187 -45
- package/lib/module/components/primitives.js.map +1 -1
- package/lib/module/components/screens/ComposeScreen.js +114 -64
- package/lib/module/components/screens/ComposeScreen.js.map +1 -1
- package/lib/module/components/screens/HomeScreen.js +224 -121
- package/lib/module/components/screens/HomeScreen.js.map +1 -1
- package/lib/module/components/screens/LanguageScreen.js +147 -0
- package/lib/module/components/screens/LanguageScreen.js.map +1 -0
- package/lib/module/components/screens/ThreadScreen.js +243 -115
- package/lib/module/components/screens/ThreadScreen.js.map +1 -1
- package/lib/module/i18n/index.js +56 -0
- package/lib/module/i18n/index.js.map +1 -0
- package/lib/module/i18n/languages.js +88 -0
- package/lib/module/i18n/languages.js.map +1 -0
- package/lib/module/i18n/strings/de.js +68 -0
- package/lib/module/i18n/strings/de.js.map +1 -0
- package/lib/module/i18n/strings/en.js +84 -0
- package/lib/module/i18n/strings/en.js.map +1 -0
- package/lib/module/i18n/strings/es.js +68 -0
- package/lib/module/i18n/strings/es.js.map +1 -0
- package/lib/module/i18n/strings/fr.js +68 -0
- package/lib/module/i18n/strings/fr.js.map +1 -0
- package/lib/module/i18n/strings/hi.js +68 -0
- package/lib/module/i18n/strings/hi.js.map +1 -0
- package/lib/module/i18n/strings/it.js +68 -0
- package/lib/module/i18n/strings/it.js.map +1 -0
- package/lib/module/i18n/strings/ja.js +68 -0
- package/lib/module/i18n/strings/ja.js.map +1 -0
- package/lib/module/i18n/strings/ko.js +68 -0
- package/lib/module/i18n/strings/ko.js.map +1 -0
- package/lib/module/i18n/strings/nl.js +68 -0
- package/lib/module/i18n/strings/nl.js.map +1 -0
- package/lib/module/i18n/strings/pl.js +68 -0
- package/lib/module/i18n/strings/pl.js.map +1 -0
- package/lib/module/i18n/strings/pt.js +68 -0
- package/lib/module/i18n/strings/pt.js.map +1 -0
- package/lib/module/i18n/strings/ru.js +68 -0
- package/lib/module/i18n/strings/ru.js.map +1 -0
- package/lib/module/i18n/strings/tr.js +68 -0
- package/lib/module/i18n/strings/tr.js.map +1 -0
- package/lib/module/i18n/strings/zh.js +68 -0
- package/lib/module/i18n/strings/zh.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/theme.js +135 -16
- package/lib/module/theme.js.map +1 -1
- package/lib/module/types.js +18 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/unread.js +83 -0
- package/lib/module/unread.js.map +1 -0
- package/lib/module/useSupportThreads.js +291 -0
- package/lib/module/useSupportThreads.js.map +1 -0
- package/lib/module/utils.js +55 -10
- package/lib/module/utils.js.map +1 -1
- package/lib/typescript/api.d.ts +9 -0
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/components/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/components/LauncherButton.d.ts +14 -0
- package/lib/typescript/components/LauncherButton.d.ts.map +1 -0
- package/lib/typescript/components/SupportButton.d.ts +3 -3
- package/lib/typescript/components/SupportButton.d.ts.map +1 -1
- package/lib/typescript/components/icons.d.ts +18 -2
- package/lib/typescript/components/icons.d.ts.map +1 -1
- package/lib/typescript/components/primitives.d.ts +42 -7
- package/lib/typescript/components/primitives.d.ts.map +1 -1
- package/lib/typescript/components/screens/ComposeScreen.d.ts +3 -1
- package/lib/typescript/components/screens/ComposeScreen.d.ts.map +1 -1
- package/lib/typescript/components/screens/HomeScreen.d.ts +8 -2
- package/lib/typescript/components/screens/HomeScreen.d.ts.map +1 -1
- package/lib/typescript/components/screens/LanguageScreen.d.ts +30 -0
- package/lib/typescript/components/screens/LanguageScreen.d.ts.map +1 -0
- package/lib/typescript/components/screens/ThreadScreen.d.ts +3 -1
- package/lib/typescript/components/screens/ThreadScreen.d.ts.map +1 -1
- package/lib/typescript/i18n/index.d.ts +15 -0
- package/lib/typescript/i18n/index.d.ts.map +1 -0
- package/lib/typescript/i18n/languages.d.ts +36 -0
- package/lib/typescript/i18n/languages.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/de.d.ts +3 -0
- package/lib/typescript/i18n/strings/de.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/en.d.ts +83 -0
- package/lib/typescript/i18n/strings/en.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/es.d.ts +3 -0
- package/lib/typescript/i18n/strings/es.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/fr.d.ts +3 -0
- package/lib/typescript/i18n/strings/fr.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/hi.d.ts +3 -0
- package/lib/typescript/i18n/strings/hi.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/it.d.ts +3 -0
- package/lib/typescript/i18n/strings/it.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/ja.d.ts +3 -0
- package/lib/typescript/i18n/strings/ja.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/ko.d.ts +3 -0
- package/lib/typescript/i18n/strings/ko.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/nl.d.ts +3 -0
- package/lib/typescript/i18n/strings/nl.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/pl.d.ts +3 -0
- package/lib/typescript/i18n/strings/pl.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/pt.d.ts +3 -0
- package/lib/typescript/i18n/strings/pt.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/ru.d.ts +3 -0
- package/lib/typescript/i18n/strings/ru.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/tr.d.ts +3 -0
- package/lib/typescript/i18n/strings/tr.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/zh.d.ts +3 -0
- package/lib/typescript/i18n/strings/zh.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/theme.d.ts +44 -3
- package/lib/typescript/theme.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +54 -2
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/unread.d.ts +22 -0
- package/lib/typescript/unread.d.ts.map +1 -0
- package/lib/typescript/useSupportThreads.d.ts +38 -0
- package/lib/typescript/useSupportThreads.d.ts.map +1 -0
- package/lib/typescript/utils.d.ts +8 -2
- package/lib/typescript/utils.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/api.ts +24 -1
- package/src/components/BottomSheet.tsx +61 -43
- package/src/components/LauncherButton.tsx +217 -0
- package/src/components/SupportButton.tsx +195 -210
- package/src/components/icons.tsx +113 -19
- package/src/components/primitives.tsx +230 -39
- package/src/components/screens/ComposeScreen.tsx +129 -63
- package/src/components/screens/HomeScreen.tsx +230 -120
- package/src/components/screens/LanguageScreen.tsx +172 -0
- package/src/components/screens/ThreadScreen.tsx +319 -122
- package/src/i18n/index.ts +81 -0
- package/src/i18n/languages.ts +106 -0
- package/src/i18n/strings/de.ts +78 -0
- package/src/i18n/strings/en.ts +94 -0
- package/src/i18n/strings/es.ts +77 -0
- package/src/i18n/strings/fr.ts +78 -0
- package/src/i18n/strings/hi.ts +77 -0
- package/src/i18n/strings/it.ts +77 -0
- package/src/i18n/strings/ja.ts +77 -0
- package/src/i18n/strings/ko.ts +76 -0
- package/src/i18n/strings/nl.ts +77 -0
- package/src/i18n/strings/pl.ts +77 -0
- package/src/i18n/strings/pt.ts +77 -0
- package/src/i18n/strings/ru.ts +77 -0
- package/src/i18n/strings/tr.ts +77 -0
- package/src/i18n/strings/zh.ts +75 -0
- package/src/index.tsx +9 -0
- package/src/theme.ts +165 -18
- package/src/types.ts +56 -2
- package/src/unread.ts +91 -0
- package/src/useSupportThreads.ts +392 -0
- package/src/utils.ts +48 -10
package/src/theme.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// The SDK consumes the same design tokens as the admin dashboard, so both
|
|
2
|
-
// surfaces look like one product.
|
|
2
|
+
// surfaces look like one product. On top of those it adds the few values
|
|
3
|
+
// that only exist on mobile: sheet/bubble radii and the elevation used by
|
|
4
|
+
// the launcher and the sheet (the web side gets those from CSS).
|
|
3
5
|
import {
|
|
4
6
|
darkColors,
|
|
5
7
|
lightColors,
|
|
@@ -8,32 +10,177 @@ import {
|
|
|
8
10
|
spacing,
|
|
9
11
|
type ThemeColors,
|
|
10
12
|
} from '@appsupportkit/design-tokens';
|
|
13
|
+
import type { ViewStyle } from 'react-native';
|
|
11
14
|
import type { ThemeName } from './types';
|
|
12
15
|
|
|
16
|
+
/** Rounder than the dashboard: touch targets read better with more radius. */
|
|
17
|
+
export const supportRadii = {
|
|
18
|
+
...radii,
|
|
19
|
+
control: 10,
|
|
20
|
+
card: 14,
|
|
21
|
+
cardLg: 18,
|
|
22
|
+
/** Message bubbles. */
|
|
23
|
+
bubble: 18,
|
|
24
|
+
/** The tight corner on a bubble's sender side. */
|
|
25
|
+
bubbleTail: 6,
|
|
26
|
+
sheet: 24,
|
|
27
|
+
pill: 999,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export interface SupportShadows {
|
|
31
|
+
/** Floating launcher button. */
|
|
32
|
+
launcher: ViewStyle;
|
|
33
|
+
/** The bottom sheet itself. */
|
|
34
|
+
sheet: ViewStyle;
|
|
35
|
+
/** Unread badge, so it lifts off the button. */
|
|
36
|
+
badge: ViewStyle;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The launcher sits on the host app's own background rather than on an SDK
|
|
41
|
+
* surface, so the two themes need different treatment: on near-black a
|
|
42
|
+
* solid fill needs a hairline to separate from the background and a black
|
|
43
|
+
* shadow, while on white the accent-tinted shadow does that work and a
|
|
44
|
+
* borderless mark has to be darkened to stay legible.
|
|
45
|
+
*/
|
|
46
|
+
export interface LauncherPalette {
|
|
47
|
+
/** Hairline drawn over the solid fill. */
|
|
48
|
+
solidRing: string;
|
|
49
|
+
/** Mark color for the borderless (minimal) variant. */
|
|
50
|
+
minimalMark: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
13
53
|
export interface SupportTheme {
|
|
14
54
|
name: ThemeName;
|
|
15
55
|
colors: ThemeColors;
|
|
16
56
|
spacing: typeof spacing;
|
|
17
|
-
radii: typeof
|
|
57
|
+
radii: typeof supportRadii;
|
|
18
58
|
motion: typeof motion;
|
|
59
|
+
shadows: SupportShadows;
|
|
60
|
+
launcher: LauncherPalette;
|
|
19
61
|
}
|
|
20
62
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
radii,
|
|
26
|
-
motion,
|
|
27
|
-
};
|
|
63
|
+
// Color helpers come first: the themes below are built at module load, so
|
|
64
|
+
// anything they reach for has to already be initialized. (A `const` used
|
|
65
|
+
// before its definition survives tsc but lands as `undefined` on Hermes.)
|
|
66
|
+
const HEX = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
28
67
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
68
|
+
function parseHex(color: string): [number, number, number] | null {
|
|
69
|
+
const match = HEX.exec(color.trim());
|
|
70
|
+
const digits = match?.[1];
|
|
71
|
+
if (digits === undefined) return null;
|
|
72
|
+
const hex =
|
|
73
|
+
digits.length === 3
|
|
74
|
+
? digits
|
|
75
|
+
.split('')
|
|
76
|
+
.map((c) => c + c)
|
|
77
|
+
.join('')
|
|
78
|
+
: digits;
|
|
79
|
+
return [
|
|
80
|
+
parseInt(hex.slice(0, 2), 16),
|
|
81
|
+
parseInt(hex.slice(2, 4), 16),
|
|
82
|
+
parseInt(hex.slice(4, 6), 16),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mix([r, g, b]: [number, number, number], target: number, amount: number) {
|
|
87
|
+
const channel = (c: number) => Math.round(c + (target - c) * amount);
|
|
88
|
+
return `rgb(${channel(r)}, ${channel(g)}, ${channel(b)})`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function shadowsFor(name: ThemeName, accent: string): SupportShadows {
|
|
92
|
+
const dark = name === 'dark';
|
|
93
|
+
return {
|
|
94
|
+
launcher: {
|
|
95
|
+
// Tinting the launcher's shadow with the accent is what makes it read
|
|
96
|
+
// as lifted rather than pasted on.
|
|
97
|
+
shadowColor: dark ? '#000000' : accent,
|
|
98
|
+
shadowOpacity: dark ? 0.45 : 0.3,
|
|
99
|
+
shadowRadius: 14,
|
|
100
|
+
shadowOffset: { width: 0, height: 6 },
|
|
101
|
+
elevation: 8,
|
|
102
|
+
},
|
|
103
|
+
sheet: {
|
|
104
|
+
shadowColor: '#000000',
|
|
105
|
+
shadowOpacity: dark ? 0.5 : 0.16,
|
|
106
|
+
shadowRadius: 24,
|
|
107
|
+
shadowOffset: { width: 0, height: -6 },
|
|
108
|
+
elevation: 24,
|
|
109
|
+
},
|
|
110
|
+
badge: {
|
|
111
|
+
shadowColor: '#000000',
|
|
112
|
+
shadowOpacity: 0.25,
|
|
113
|
+
shadowRadius: 3,
|
|
114
|
+
shadowOffset: { width: 0, height: 1 },
|
|
115
|
+
elevation: 3,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function launcherFor(name: ThemeName, accent: string): LauncherPalette {
|
|
121
|
+
const rgb = parseHex(accent);
|
|
122
|
+
return {
|
|
123
|
+
solidRing:
|
|
124
|
+
name === 'dark' ? 'rgba(255, 255, 255, 0.14)' : 'rgba(0, 0, 0, 0.06)',
|
|
125
|
+
// On a dark app background the raw accent is too close to the surface
|
|
126
|
+
// to read as a glyph; lift it toward white.
|
|
127
|
+
minimalMark:
|
|
128
|
+
name === 'dark' && rgb !== null ? mix(rgb, 255, 0.35) : accent,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildTheme(name: ThemeName, colors: ThemeColors): SupportTheme {
|
|
133
|
+
return {
|
|
134
|
+
name,
|
|
135
|
+
colors,
|
|
136
|
+
spacing,
|
|
137
|
+
radii: supportRadii,
|
|
138
|
+
motion,
|
|
139
|
+
shadows: shadowsFor(name, colors.accent),
|
|
140
|
+
launcher: launcherFor(name, colors.accent),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const supportDarkTheme: SupportTheme = buildTheme('dark', darkColors);
|
|
145
|
+
export const supportLightTheme: SupportTheme = buildTheme('light', lightColors);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Derives the accent family from a single brand color: a pressed shade, the
|
|
149
|
+
* 12% wash used behind chips and own-message bubbles, and a foreground that
|
|
150
|
+
* stays legible on top (dark text on light brands, white on dark ones).
|
|
151
|
+
*/
|
|
152
|
+
function withAccent(theme: SupportTheme, accent: string): SupportTheme {
|
|
153
|
+
const rgb = parseHex(accent);
|
|
154
|
+
if (!rgb) return theme;
|
|
155
|
+
const [r, g, b] = rgb;
|
|
156
|
+
// Relative luminance, sRGB coefficients — good enough to pick a foreground.
|
|
157
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
158
|
+
const colors: ThemeColors = {
|
|
159
|
+
...theme.colors,
|
|
160
|
+
accent,
|
|
161
|
+
accentHover: mix(rgb, theme.name === 'dark' ? 255 : 0, 0.12),
|
|
162
|
+
accentMuted: `rgba(${r}, ${g}, ${b}, 0.12)`,
|
|
163
|
+
accentForeground: luminance > 0.62 ? '#111113' : '#FFFFFF',
|
|
164
|
+
statusOpen: accent,
|
|
165
|
+
statusOpenBg: `rgba(${r}, ${g}, ${b}, 0.10)`,
|
|
166
|
+
focusRing: accent,
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
...theme,
|
|
170
|
+
colors,
|
|
171
|
+
shadows: shadowsFor(theme.name, accent),
|
|
172
|
+
launcher: launcherFor(theme.name, accent),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
36
175
|
|
|
37
|
-
|
|
38
|
-
|
|
176
|
+
/**
|
|
177
|
+
* @param name "light" | "dark" (default dark).
|
|
178
|
+
* @param accentColor Optional brand color (hex) replacing the default accent.
|
|
179
|
+
*/
|
|
180
|
+
export function resolveTheme(
|
|
181
|
+
name: ThemeName | undefined,
|
|
182
|
+
accentColor?: string,
|
|
183
|
+
): SupportTheme {
|
|
184
|
+
const base = name === 'light' ? supportLightTheme : supportDarkTheme;
|
|
185
|
+
return accentColor ? withAccent(base, accentColor) : base;
|
|
39
186
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import type { Language } from './i18n/languages';
|
|
3
|
+
|
|
1
4
|
export type QueryType = 'feedback' | 'feature_request' | 'bug_report';
|
|
2
5
|
export type QueryStatus = 'open' | 'resolved';
|
|
3
6
|
export type MessageSender = 'user' | 'admin';
|
|
4
7
|
export type ThemeName = 'dark' | 'light';
|
|
5
8
|
export type PushPermissionStatus = 'undetermined' | 'granted' | 'denied';
|
|
6
9
|
|
|
10
|
+
/**
|
|
11
|
+
* How the launcher renders:
|
|
12
|
+
* solid — accent fill, elevated. The floating action button.
|
|
13
|
+
* soft — app surface + hairline border. Sits in a header or toolbar.
|
|
14
|
+
* minimal — bare mark, no fill or border. Sits in a nav bar.
|
|
15
|
+
*/
|
|
16
|
+
export type LauncherVariant = 'solid' | 'soft' | 'minimal';
|
|
17
|
+
|
|
7
18
|
export interface SupportUser {
|
|
8
19
|
/** Display name shown to the support team. */
|
|
9
20
|
name: string;
|
|
@@ -30,6 +41,20 @@ export interface SupportButtonProps {
|
|
|
30
41
|
supabaseUrl: string;
|
|
31
42
|
/** Visual theme. Matches the admin dashboard tokens. Default "dark". */
|
|
32
43
|
theme?: ThemeName;
|
|
44
|
+
/**
|
|
45
|
+
* Brand color (hex, e.g. "#FF5A1F") replacing the default accent across
|
|
46
|
+
* the launcher, buttons, and the user's own message bubbles. The pressed
|
|
47
|
+
* shade, the 12% wash, and a legible foreground are derived from it.
|
|
48
|
+
*/
|
|
49
|
+
accentColor?: string;
|
|
50
|
+
/** Launcher style. Default "solid" — the floating action button. */
|
|
51
|
+
variant?: LauncherVariant;
|
|
52
|
+
/** Launcher diameter in px (height when `label` is set). Default 52. */
|
|
53
|
+
size?: number;
|
|
54
|
+
/** Renders the launcher as a pill with this text beside the mark. */
|
|
55
|
+
label?: string;
|
|
56
|
+
/** Style applied to the launcher — position it, add margins, etc. */
|
|
57
|
+
style?: StyleProp<ViewStyle>;
|
|
33
58
|
/**
|
|
34
59
|
* Idle poll interval (ms) — how often the unread badge is refreshed while
|
|
35
60
|
* the sheet is CLOSED, using a lightweight count-only endpoint. Default
|
|
@@ -47,23 +72,45 @@ export interface SupportButtonProps {
|
|
|
47
72
|
* installed — it is an optional peer dependency.
|
|
48
73
|
*/
|
|
49
74
|
expoProjectId?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Pre-selects a language in the picker the first time a user opens support.
|
|
77
|
+
* Pass it when the host app already knows what the user reads — an in-app
|
|
78
|
+
* language setting beats the device locale, which is what the SDK falls
|
|
79
|
+
* back to. It is a default, not an override: the user still chooses, and a
|
|
80
|
+
* choice they have already made always wins.
|
|
81
|
+
*/
|
|
82
|
+
defaultLanguage?: Language;
|
|
50
83
|
}
|
|
51
84
|
|
|
52
85
|
export interface ThreadMessage {
|
|
53
86
|
id: string;
|
|
54
87
|
sender: MessageSender;
|
|
88
|
+
/**
|
|
89
|
+
* Already in the user's language: their own messages as they wrote them,
|
|
90
|
+
* support replies translated. The server does the choosing — see the
|
|
91
|
+
* get-threads function.
|
|
92
|
+
*/
|
|
55
93
|
body: string;
|
|
56
94
|
created_at: string;
|
|
57
|
-
|
|
95
|
+
/**
|
|
96
|
+
* A support reply whose translation hasn't landed yet. `body` currently
|
|
97
|
+
* holds the English original; it will change under the reader shortly, so
|
|
98
|
+
* the UI says so rather than letting it flicker.
|
|
99
|
+
*/
|
|
100
|
+
translation_pending: boolean;
|
|
58
101
|
}
|
|
59
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Note the absence of any admin identity: end users see one support team,
|
|
105
|
+
* not the people staffing it, so the names never leave the server. The
|
|
106
|
+
* dashboard still shows who wrote and who resolved what.
|
|
107
|
+
*/
|
|
60
108
|
export interface Thread {
|
|
61
109
|
id: string;
|
|
62
110
|
type: QueryType;
|
|
63
111
|
status: QueryStatus;
|
|
64
112
|
created_at: string;
|
|
65
113
|
resolved_at: string | null;
|
|
66
|
-
resolved_by_name: string | null;
|
|
67
114
|
app_version: string;
|
|
68
115
|
unread_count: number;
|
|
69
116
|
last_message: {
|
|
@@ -77,8 +124,15 @@ export interface Thread {
|
|
|
77
124
|
export interface ThreadsResponse {
|
|
78
125
|
threads: Thread[];
|
|
79
126
|
total_unread: number;
|
|
127
|
+
/** Null when this user has never picked one — the SDK then asks. */
|
|
128
|
+
language: string | null;
|
|
80
129
|
}
|
|
81
130
|
|
|
131
|
+
/**
|
|
132
|
+
* English labels, kept for host apps that render their own entry points.
|
|
133
|
+
* Inside the sheet the labels come from the string table instead, so they
|
|
134
|
+
* follow the user's language.
|
|
135
|
+
*/
|
|
82
136
|
export const QUERY_TYPE_LABEL: Record<QueryType, string> = {
|
|
83
137
|
feedback: 'Feedback',
|
|
84
138
|
feature_request: 'Feature request',
|
package/src/unread.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Unread accounting.
|
|
2
|
+
//
|
|
3
|
+
// The server counts a thread's unread as "admin messages newer than the
|
|
4
|
+
// user's read receipt". That number is authoritative, but it is always a
|
|
5
|
+
// little behind the device: between the moment the user opens a thread and
|
|
6
|
+
// the moment `mark-read` lands, every already-in-flight poll still reports
|
|
7
|
+
// the old count — which is what made the badge flicker back on after being
|
|
8
|
+
// cleared, and what made a thread the user was actively reading accumulate
|
|
9
|
+
// unread replies.
|
|
10
|
+
//
|
|
11
|
+
// So the client keeps its own watermark per thread: the `created_at` of the
|
|
12
|
+
// newest admin message the user has actually seen. Those are *server*
|
|
13
|
+
// timestamps, so nothing here compares against the device clock.
|
|
14
|
+
//
|
|
15
|
+
// Displayed unread is then the stricter of the two counts, which is correct
|
|
16
|
+
// in both directions:
|
|
17
|
+
//
|
|
18
|
+
// read here, receipt not written yet → local watermark wins (0)
|
|
19
|
+
// read on another device → server count wins (0)
|
|
20
|
+
|
|
21
|
+
import type { Thread } from './types';
|
|
22
|
+
|
|
23
|
+
/** Watermark map: thread id → `created_at` (ms) of the newest seen reply. */
|
|
24
|
+
export type ReadWatermarks = Map<string, number>;
|
|
25
|
+
|
|
26
|
+
/** `created_at` (ms) of the newest admin message, or 0 if there are none. */
|
|
27
|
+
export function latestAdminMessageAt(thread: Thread): number {
|
|
28
|
+
let latest = 0;
|
|
29
|
+
for (const message of thread.messages) {
|
|
30
|
+
if (message.sender !== 'admin') continue;
|
|
31
|
+
const at = Date.parse(message.created_at);
|
|
32
|
+
if (Number.isFinite(at) && at > latest) latest = at;
|
|
33
|
+
}
|
|
34
|
+
return latest;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Admin messages in `thread` newer than `watermark`. */
|
|
38
|
+
export function unreadSince(thread: Thread, watermark: number): number {
|
|
39
|
+
let count = 0;
|
|
40
|
+
for (const message of thread.messages) {
|
|
41
|
+
if (message.sender !== 'admin') continue;
|
|
42
|
+
const at = Date.parse(message.created_at);
|
|
43
|
+
if (Number.isFinite(at) && at > watermark) count += 1;
|
|
44
|
+
}
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns `thread` with `unread_count` narrowed by what this device knows
|
|
50
|
+
* the user has read. Identity is preserved when nothing changes so lists
|
|
51
|
+
* stay referentially stable between polls.
|
|
52
|
+
*/
|
|
53
|
+
export function reconcileThread(
|
|
54
|
+
thread: Thread,
|
|
55
|
+
watermarks: ReadWatermarks,
|
|
56
|
+
): Thread {
|
|
57
|
+
const watermark = watermarks.get(thread.id);
|
|
58
|
+
if (watermark === undefined || thread.unread_count === 0) return thread;
|
|
59
|
+
const unread = Math.min(thread.unread_count, unreadSince(thread, watermark));
|
|
60
|
+
return unread === thread.unread_count ? thread : { ...thread, unread_count: unread };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function reconcileThreads(
|
|
64
|
+
threads: Thread[],
|
|
65
|
+
watermarks: ReadWatermarks,
|
|
66
|
+
): Thread[] {
|
|
67
|
+
return threads.map((thread) => reconcileThread(thread, watermarks));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function sumUnread(threads: Thread[]): number {
|
|
71
|
+
return threads.reduce((total, thread) => total + thread.unread_count, 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Drops watermarks the server has caught up on (its own count is already 0)
|
|
76
|
+
* and ones for threads that no longer exist, so the map can't grow without
|
|
77
|
+
* bound over a long session.
|
|
78
|
+
*/
|
|
79
|
+
export function pruneWatermarks(
|
|
80
|
+
watermarks: ReadWatermarks,
|
|
81
|
+
serverThreads: Thread[],
|
|
82
|
+
): ReadWatermarks {
|
|
83
|
+
const keep: ReadWatermarks = new Map();
|
|
84
|
+
for (const thread of serverThreads) {
|
|
85
|
+
const watermark = watermarks.get(thread.id);
|
|
86
|
+
if (watermark !== undefined && thread.unread_count > 0) {
|
|
87
|
+
keep.set(thread.id, watermark);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return keep;
|
|
91
|
+
}
|