@repzo/widget-react-native 1.0.4 → 1.0.5
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/state/widget-store.js +19 -4
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +80 -64
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/Gradient.js +18 -2
- package/lib/commonjs/ui/components/Gradient.js.map +1 -1
- package/lib/commonjs/ui/hooks/use-top-inset.js +22 -0
- package/lib/commonjs/ui/hooks/use-top-inset.js.map +1 -0
- package/lib/commonjs/ui/screens/chat/ChatView.js +8 -1
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/home/HomeView.js +42 -9
- package/lib/commonjs/ui/screens/home/HomeView.js.map +1 -1
- package/lib/commonjs/ui/screens/home/HomeView.styles.js +5 -3
- package/lib/commonjs/ui/screens/home/HomeView.styles.js.map +1 -1
- package/lib/module/state/widget-store.js +19 -4
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +83 -67
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/Gradient.js +18 -2
- package/lib/module/ui/components/Gradient.js.map +1 -1
- package/lib/module/ui/hooks/use-top-inset.js +17 -0
- package/lib/module/ui/hooks/use-top-inset.js.map +1 -0
- package/lib/module/ui/screens/chat/ChatView.js +9 -2
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/home/HomeView.js +44 -12
- package/lib/module/ui/screens/home/HomeView.js.map +1 -1
- package/lib/module/ui/screens/home/HomeView.styles.js +4 -2
- package/lib/module/ui/screens/home/HomeView.styles.js.map +1 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts +7 -0
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/Gradient.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/use-top-inset.d.ts +11 -0
- package/lib/typescript/commonjs/ui/hooks/use-top-inset.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts +7 -0
- package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts +3 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts.map +1 -1
- package/lib/typescript/module/state/widget-store.d.ts +7 -0
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/Gradient.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/use-top-inset.d.ts +11 -0
- package/lib/typescript/module/ui/hooks/use-top-inset.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.d.ts +7 -0
- package/lib/typescript/module/ui/screens/home/HomeView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts +3 -1
- package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/state/widget-store.ts +19 -4
- package/src/ui/WidgetProvider.tsx +41 -4
- package/src/ui/components/Gradient.tsx +19 -2
- package/src/ui/hooks/use-top-inset.ts +16 -0
- package/src/ui/screens/chat/ChatView.tsx +9 -2
- package/src/ui/screens/home/HomeView.styles.ts +4 -2
- package/src/ui/screens/home/HomeView.tsx +36 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WidgetProvider.d.ts","sourceRoot":"","sources":["../../../../src/ui/WidgetProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"WidgetProvider.d.ts","sourceRoot":"","sources":["../../../../src/ui/WidgetProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAkDH,MAAM,WAAW,wBAAwB;IACvC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,8EAA8E;IAC9E,iBAAiB,CAAC,EAAE,WAAW,GAAG,YAAY,CAAC;CAChD;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,iBAA+B,GAChC,EAAE,wBAAwB,+BAwb1B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Gradient.d.ts","sourceRoot":"","sources":["../../../../../src/ui/components/Gradient.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAoB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AA4BhF;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,QAAQ,EACR,KAAK,EACL,KAAK,GACN,EAAE;IACD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,+BAmBA;
|
|
1
|
+
{"version":3,"file":"Gradient.d.ts","sourceRoot":"","sources":["../../../../../src/ui/components/Gradient.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAoB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AA4BhF;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,QAAQ,EACR,KAAK,EACL,KAAK,GACN,EAAE;IACD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,+BAmBA;AAoDD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAqB,EACrB,MAAW,EACX,KAAK,EACL,KAAK,GACN,EAAE;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB,+BA8BA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How far a screen's header must reach up to sit under the status bar.
|
|
3
|
+
*
|
|
4
|
+
* The messenger draws edge to edge. Screens with a brand header (home, chat)
|
|
5
|
+
* extend that header's background into the status-bar area instead of leaving
|
|
6
|
+
* a white strip above it; the provider gives them the inset through this
|
|
7
|
+
* context and pads every other screen itself, so this is 0 for them.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TopInsetContext: import("react").Context<number>;
|
|
10
|
+
export declare function useTopInset(): number;
|
|
11
|
+
//# sourceMappingURL=use-top-inset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-top-inset.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/use-top-inset.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,eAAO,MAAM,eAAe,iCAAmB,CAAC;AAEhD,wBAAgB,WAAW,IAAI,MAAM,CAEpC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqDH,OAAO,KAAK,EAAc,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAElE,wBAAgB,QAAQ,CAAC,EACvB,QAAQ,EACR,OAAO,EACP,eAAe,EACf,WAAW,EACX,KAAK,EACL,SAAS,EACT,WAAW,EACX,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,WAAgB,EAChB,cAAc,EACd,MAAM,EACN,WAAW,EACX,cAAc,EACd,MAAM,EACN,iBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,QAAQ,EACR,MAAM,EACN,OAAO,GACR,EAAE,aAAa,+BA8hBf"}
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
* the visitor already has; nothing here assumes a field the backend does not
|
|
13
13
|
* send today.
|
|
14
14
|
*/
|
|
15
|
+
import type { WidgetConfig } from '../../../types';
|
|
15
16
|
import type { HomeViewProps } from './HomeView.types';
|
|
17
|
+
/**
|
|
18
|
+
* The admin's explicit black/white toggle always wins. Older configs can omit
|
|
19
|
+
* it, and defaulting to dark text over a dark brand header is unreadable, so
|
|
20
|
+
* derive something legible from the background instead.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveHeaderTextColor(config: WidgetConfig | null, fallbackColor: string): string;
|
|
16
23
|
export declare function HomeView({ config, color, visitorName, conversationCount, recentConversations, unreadByConversation, teamAgents, isOnline, offlineMessage, startConversationLabel, replyTimeLabel, allowNewConversations, showPreviousConversations, labels, onClose, onStartConversation, onViewConversations, onOpenConversation, }: HomeViewProps): import("react").JSX.Element;
|
|
17
24
|
//# sourceMappingURL=HomeView.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HomeView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/home/HomeView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;
|
|
1
|
+
{"version":3,"file":"HomeView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/home/HomeView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAcH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA2BnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAQjG;AAED,wBAAgB,QAAQ,CAAC,EACvB,MAAM,EACN,KAAK,EACL,WAAW,EACX,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,EACpB,UAAU,EACV,QAAQ,EACR,cAAc,EACd,sBAAsB,EACtB,cAAc,EACd,qBAAqB,EACrB,yBAAyB,EACzB,MAAM,EACN,OAAO,EACP,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,GACnB,EAAE,aAAa,+BAmNf"}
|
|
@@ -16,6 +16,9 @@ export declare const START_CARD_LIFT = 28;
|
|
|
16
16
|
export declare const START_CARD_MIN_HEIGHT = 76;
|
|
17
17
|
export declare const HEADER_FADE_HEIGHT = 90;
|
|
18
18
|
export declare const CLOSE_BUTTON_SIZE = 28;
|
|
19
|
+
/** The logo keeps its own shape at this height, up to the web widget's max width. */
|
|
20
|
+
export declare const LOGO_HEIGHT = 40;
|
|
21
|
+
export declare const LOGO_MAX_WIDTH = 152;
|
|
19
22
|
export declare const CLOSE_ICON_SIZE = 14;
|
|
20
23
|
export declare const styles: {
|
|
21
24
|
contentContainer: {
|
|
@@ -47,7 +50,6 @@ export declare const styles: {
|
|
|
47
50
|
minHeight: number;
|
|
48
51
|
};
|
|
49
52
|
logo: {
|
|
50
|
-
width: number;
|
|
51
53
|
height: number;
|
|
52
54
|
};
|
|
53
55
|
closeButton: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HomeView.styles.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/home/HomeView.styles.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,eAAO,MAAM,MAAM
|
|
1
|
+
{"version":3,"file":"HomeView.styles.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/home/HomeView.styles.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,qFAAqF;AACrF,eAAO,MAAM,WAAW,KAAK,CAAC;AAC9B,eAAO,MAAM,cAAc,MAAM,CAAC;AAClC,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyNjB,CAAC"}
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@ import type {
|
|
|
35
35
|
} from '../types';
|
|
36
36
|
import { setOpenUrlHandler } from '../ui/utils/safe-url';
|
|
37
37
|
|
|
38
|
-
const SDK_VERSION = '@repzo/widget-react-native@1.0.
|
|
38
|
+
const SDK_VERSION = '@repzo/widget-react-native@1.0.5';
|
|
39
39
|
|
|
40
40
|
/** The platform this session reports — mobile sessions get the Mobile SDKs overrides. */
|
|
41
41
|
function sessionPlatform(): 'ios' | 'android' | 'web' {
|
|
@@ -489,7 +489,7 @@ export class WidgetStore {
|
|
|
489
489
|
loading: false,
|
|
490
490
|
error: null,
|
|
491
491
|
errorCode: null,
|
|
492
|
-
config: result.config,
|
|
492
|
+
config: this.hydrateConfig(result.config),
|
|
493
493
|
identityId: result.identityId,
|
|
494
494
|
conversations,
|
|
495
495
|
// Also seeds the launcher badge, so a reply that landed while the app
|
|
@@ -512,7 +512,7 @@ export class WidgetStore {
|
|
|
512
512
|
},
|
|
513
513
|
}));
|
|
514
514
|
|
|
515
|
-
this.persistConfig(result.config);
|
|
515
|
+
this.persistConfig(this.hydrateConfig(result.config));
|
|
516
516
|
this.connectRealtime(result.token);
|
|
517
517
|
this.watchAppState();
|
|
518
518
|
this.loadOutboundMessage();
|
|
@@ -558,13 +558,28 @@ export class WidgetStore {
|
|
|
558
558
|
cached.platform === sessionPlatform() &&
|
|
559
559
|
!this.state.config
|
|
560
560
|
) {
|
|
561
|
-
this.setState({ config: cached.config });
|
|
561
|
+
this.setState({ config: this.hydrateConfig(cached.config) });
|
|
562
562
|
}
|
|
563
563
|
} catch {
|
|
564
564
|
// Corrupt cache — the network config overwrites it shortly.
|
|
565
565
|
}
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
+
/**
|
|
569
|
+
* The server sends the home logo and background image as paths relative to
|
|
570
|
+
* Workstation (`/api/media/:id/download`), which a React Native Image can't
|
|
571
|
+
* load. Resolved against the boot `baseUrl` they become full links (public
|
|
572
|
+
* media redirects to the CDN). Absolute URLs pass through unchanged.
|
|
573
|
+
*/
|
|
574
|
+
private hydrateConfig(config: WidgetConfig): WidgetConfig {
|
|
575
|
+
if (!config) return config;
|
|
576
|
+
return {
|
|
577
|
+
...config,
|
|
578
|
+
homeLogoUrl: this.client.resolveUrl(config.homeLogoUrl),
|
|
579
|
+
homeBackgroundImageUrl: this.client.resolveUrl(config.homeBackgroundImageUrl),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
568
583
|
private persistConfig(config: WidgetConfig): void {
|
|
569
584
|
if (!this.bootConfig) return;
|
|
570
585
|
const entry: CachedConfig = {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
Modal,
|
|
13
13
|
Platform,
|
|
14
14
|
Pressable,
|
|
15
|
+
StatusBar,
|
|
15
16
|
StyleSheet,
|
|
16
17
|
Text,
|
|
17
18
|
View,
|
|
@@ -35,13 +36,22 @@ import { InAppNotification } from './components/InAppNotification';
|
|
|
35
36
|
import { OutboundMessageCard } from './components/OutboundMessageCard';
|
|
36
37
|
import { WidgetPanel } from './components/WidgetPanel';
|
|
37
38
|
import { useKeyboardOverlap } from './hooks/use-keyboard-overlap';
|
|
39
|
+
import { TopInsetContext } from './hooks/use-top-inset';
|
|
38
40
|
import { useWidgetInsets } from './hooks/use-widget-insets';
|
|
39
41
|
import { ChatView } from './screens/chat/ChatView';
|
|
40
42
|
import { ConversationsList } from './screens/conversations/ConversationsList';
|
|
41
|
-
import { HomeView } from './screens/home/HomeView';
|
|
43
|
+
import { HomeView, resolveHeaderTextColor } from './screens/home/HomeView';
|
|
42
44
|
import { PreChatForm } from './screens/pre-chat/PreChatForm';
|
|
43
45
|
import { globalStyles } from './styles/global';
|
|
44
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
buildTheme,
|
|
48
|
+
contrastingTextColor,
|
|
49
|
+
palette,
|
|
50
|
+
radius,
|
|
51
|
+
spacing,
|
|
52
|
+
fontSize,
|
|
53
|
+
withAlpha,
|
|
54
|
+
} from './styles/theme';
|
|
45
55
|
import { pickRecentConversations, pickTeamAgents } from './utils/conversations';
|
|
46
56
|
|
|
47
57
|
export interface RepzoWidgetProviderProps {
|
|
@@ -192,6 +202,21 @@ export function RepzoWidgetProvider({
|
|
|
192
202
|
: null;
|
|
193
203
|
const isHomeSurface = state.view === 'home';
|
|
194
204
|
|
|
205
|
+
// Home and chat paint their brand header under the status bar; every other
|
|
206
|
+
// screen (and an outbound card stacked above home) keeps a plain white top.
|
|
207
|
+
const outboundVisible =
|
|
208
|
+
!!state.outboundMessage && !blockingError && state.view !== 'chat' && state.view !== 'prechat';
|
|
209
|
+
const headerOwnsTopInset =
|
|
210
|
+
!blockingError &&
|
|
211
|
+
!(state.loading && !state.config) &&
|
|
212
|
+
!outboundVisible &&
|
|
213
|
+
(state.view === 'chat' || isHomeSurface);
|
|
214
|
+
const topBarColor = !headerOwnsTopInset
|
|
215
|
+
? palette.ink
|
|
216
|
+
: state.view === 'chat'
|
|
217
|
+
? contrastingTextColor(theme.primaryColor)
|
|
218
|
+
: resolveHeaderTextColor(localizedConfig, theme.primaryColor);
|
|
219
|
+
|
|
195
220
|
const renderContent = () => {
|
|
196
221
|
if (state.loading && !state.config) {
|
|
197
222
|
return (
|
|
@@ -405,8 +430,19 @@ export function RepzoWidgetProvider({
|
|
|
405
430
|
animationType="slide"
|
|
406
431
|
presentationStyle={Platform.OS === 'ios' ? presentationStyle : undefined}
|
|
407
432
|
onRequestClose={handleClose}
|
|
408
|
-
|
|
409
|
-
|
|
433
|
+
// Edge to edge on every Android version, so the insets below are the
|
|
434
|
+
// only top spacing (a non-translucent modal used to add a second one).
|
|
435
|
+
statusBarTranslucent>
|
|
436
|
+
<StatusBar
|
|
437
|
+
barStyle={topBarColor === palette.white ? 'light-content' : 'dark-content'}
|
|
438
|
+
animated
|
|
439
|
+
/>
|
|
440
|
+
<View
|
|
441
|
+
style={[
|
|
442
|
+
globalStyles.screen,
|
|
443
|
+
{ paddingTop: headerOwnsTopInset ? 0 : topInset, paddingBottom: insets.bottom },
|
|
444
|
+
]}>
|
|
445
|
+
<TopInsetContext.Provider value={headerOwnsTopInset ? topInset : 0}>
|
|
410
446
|
<View
|
|
411
447
|
onLayout={keyboard.onLayout}
|
|
412
448
|
style={[globalStyles.fill, { paddingBottom: keyboard.overlap }]}>
|
|
@@ -463,6 +499,7 @@ export function RepzoWidgetProvider({
|
|
|
463
499
|
</WidgetPanel>
|
|
464
500
|
</WidgetErrorBoundary>
|
|
465
501
|
</View>
|
|
502
|
+
</TopInsetContext.Provider>
|
|
466
503
|
</View>
|
|
467
504
|
</Modal>
|
|
468
505
|
</>
|
|
@@ -104,6 +104,22 @@ function GradientFill({ colors, bands }: { colors: string[]; bands?: number }) {
|
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Alpha at evenly spaced points down the fade. An eased curve instead of a
|
|
109
|
+
* straight line: a linear ramp shows a visible edge where it starts and goes
|
|
110
|
+
* opaque too late, so the brand colour reads as a band rather than melting
|
|
111
|
+
* into the page. Shared with the web widget's fade so both look the same.
|
|
112
|
+
*/
|
|
113
|
+
const FADE_STOPS = [0, 0.04, 0.12, 0.26, 0.46, 0.68, 0.86, 0.96, 1];
|
|
114
|
+
|
|
115
|
+
function fadeAlphaAt(progress: number): number {
|
|
116
|
+
const scaled = Math.min(Math.max(progress, 0), 1) * (FADE_STOPS.length - 1);
|
|
117
|
+
const index = Math.min(Math.floor(scaled), FADE_STOPS.length - 2);
|
|
118
|
+
const from = FADE_STOPS[index] as number;
|
|
119
|
+
const to = FADE_STOPS[index + 1] as number;
|
|
120
|
+
return from + (to - from) * (scaled - index);
|
|
121
|
+
}
|
|
122
|
+
|
|
107
123
|
/**
|
|
108
124
|
* Washes the bottom of a header out into the page colour: a straight ramp from
|
|
109
125
|
* fully transparent at the top to opaque at the bottom edge, the same
|
|
@@ -129,7 +145,8 @@ export function FadeOverlay({
|
|
|
129
145
|
<View style={[styles.fadeOverlay, { height }, style]} pointerEvents="none">
|
|
130
146
|
{NativeGradient ? (
|
|
131
147
|
<NativeGradient
|
|
132
|
-
colors={
|
|
148
|
+
colors={FADE_STOPS.map((alpha) => withAlpha(color, alpha))}
|
|
149
|
+
locations={FADE_STOPS.map((_, index) => index / (FADE_STOPS.length - 1))}
|
|
133
150
|
start={VERTICAL.start}
|
|
134
151
|
end={VERTICAL.end}
|
|
135
152
|
style={StyleSheet.absoluteFill}
|
|
@@ -144,7 +161,7 @@ export function FadeOverlay({
|
|
|
144
161
|
// body colour, with no hairline where the two meet.
|
|
145
162
|
<View
|
|
146
163
|
key={index}
|
|
147
|
-
style={[styles.band, { backgroundColor: withAlpha(color, (index + 1) / all.length) }]}
|
|
164
|
+
style={[styles.band, { backgroundColor: withAlpha(color, fadeAlphaAt((index + 1) / all.length)) }]}
|
|
148
165
|
/>
|
|
149
166
|
))
|
|
150
167
|
)}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How far a screen's header must reach up to sit under the status bar.
|
|
3
|
+
*
|
|
4
|
+
* The messenger draws edge to edge. Screens with a brand header (home, chat)
|
|
5
|
+
* extend that header's background into the status-bar area instead of leaving
|
|
6
|
+
* a white strip above it; the provider gives them the inset through this
|
|
7
|
+
* context and pads every other screen itself, so this is 0 for them.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createContext, useContext } from 'react';
|
|
11
|
+
|
|
12
|
+
export const TopInsetContext = createContext(0);
|
|
13
|
+
|
|
14
|
+
export function useTopInset(): number {
|
|
15
|
+
return useContext(TopInsetContext);
|
|
16
|
+
}
|
|
@@ -51,8 +51,9 @@ import { ArrowUpIcon, ChatOutlineIcon, ChevronIcon, ClockIcon, PaperclipIcon } f
|
|
|
51
51
|
import { TeamAvatars } from '../../components/TeamAvatars';
|
|
52
52
|
import { Markdown } from '../../components/Markdown';
|
|
53
53
|
import { useReduceMotion } from '../../hooks/use-reduce-motion';
|
|
54
|
+
import { useTopInset } from '../../hooks/use-top-inset';
|
|
54
55
|
import { globalStyles } from '../../styles/global';
|
|
55
|
-
import { contrastingTextColor, mixHex, palette, withAlpha } from '../../styles/theme';
|
|
56
|
+
import { contrastingTextColor, mixHex, palette, spacing, withAlpha } from '../../styles/theme';
|
|
56
57
|
import { formatClockTime, formatDayLabel, isSameDay } from '../../utils/conversations';
|
|
57
58
|
import { stripHtml } from '../../utils/html';
|
|
58
59
|
import { isHttpUrl, openExternalUrl } from '../../utils/safe-url';
|
|
@@ -327,6 +328,7 @@ export function ChatView({
|
|
|
327
328
|
.find((message) => message.direction === 'inbound' && (message.messageType ?? 'message') === 'message')?.id;
|
|
328
329
|
// Two stops off the brand color: flat fill looked like an unstyled block.
|
|
329
330
|
// A non-hex brand color falls through mixHex unchanged and renders solid.
|
|
331
|
+
const topInset = useTopInset();
|
|
330
332
|
const headerGradient = [color, mixHex(color, '#000000', 0.16)];
|
|
331
333
|
const headerControl = { backgroundColor: withAlpha(headerTextColor, 0.16) };
|
|
332
334
|
|
|
@@ -336,7 +338,12 @@ export function ChatView({
|
|
|
336
338
|
was empty whenever no agent was assigned yet. */}
|
|
337
339
|
<GradientBackground
|
|
338
340
|
colors={headerGradient}
|
|
339
|
-
style={[
|
|
341
|
+
style={[
|
|
342
|
+
styles.header,
|
|
343
|
+
{ borderBottomColor: mixHex(color, '#000000', 0.26) },
|
|
344
|
+
// Reaches under the status bar so the brand colour runs to the top.
|
|
345
|
+
topInset > 0 ? { paddingTop: spacing.md + topInset, minHeight: 72 + topInset } : null,
|
|
346
|
+
]}>
|
|
340
347
|
<Pressable
|
|
341
348
|
onPress={onBack}
|
|
342
349
|
accessibilityRole="button"
|
|
@@ -23,6 +23,9 @@ export const START_CARD_LIFT = 28;
|
|
|
23
23
|
export const START_CARD_MIN_HEIGHT = 76;
|
|
24
24
|
export const HEADER_FADE_HEIGHT = 90;
|
|
25
25
|
export const CLOSE_BUTTON_SIZE = 28;
|
|
26
|
+
/** The logo keeps its own shape at this height, up to the web widget's max width. */
|
|
27
|
+
export const LOGO_HEIGHT = 40;
|
|
28
|
+
export const LOGO_MAX_WIDTH = 152;
|
|
26
29
|
export const CLOSE_ICON_SIZE = 14;
|
|
27
30
|
|
|
28
31
|
export const styles = StyleSheet.create({
|
|
@@ -58,8 +61,7 @@ export const styles = StyleSheet.create({
|
|
|
58
61
|
minHeight: 40,
|
|
59
62
|
},
|
|
60
63
|
logo: {
|
|
61
|
-
|
|
62
|
-
height: 40,
|
|
64
|
+
height: LOGO_HEIGHT,
|
|
63
65
|
},
|
|
64
66
|
closeButton: {
|
|
65
67
|
marginStart: 'auto',
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* send today.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { Fragment, useEffect, useRef, type ReactNode } from 'react';
|
|
16
|
+
import { Fragment, useEffect, useRef, useState, type ReactNode } from 'react';
|
|
17
17
|
import {
|
|
18
18
|
Animated,
|
|
19
19
|
Easing,
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
CloseIcon,
|
|
38
38
|
} from '../../components/icons';
|
|
39
39
|
import { useReduceMotion } from '../../hooks/use-reduce-motion';
|
|
40
|
+
import { useTopInset } from '../../hooks/use-top-inset';
|
|
40
41
|
import { globalStyles } from '../../styles/global';
|
|
41
42
|
import { contrastingTextColor, palette, spacing, withAlpha } from '../../styles/theme';
|
|
42
43
|
import { flipForRTL } from '../../utils/rtl';
|
|
@@ -45,6 +46,9 @@ import {
|
|
|
45
46
|
CLOSE_ICON_SIZE,
|
|
46
47
|
HEADER_BOTTOM_PADDING,
|
|
47
48
|
HEADER_FADE_HEIGHT,
|
|
49
|
+
HEADER_MIN_HEIGHT,
|
|
50
|
+
LOGO_HEIGHT,
|
|
51
|
+
LOGO_MAX_WIDTH,
|
|
48
52
|
START_CARD_LIFT,
|
|
49
53
|
styles,
|
|
50
54
|
} from './HomeView.styles';
|
|
@@ -55,7 +59,7 @@ import type { HomeViewProps } from './HomeView.types';
|
|
|
55
59
|
* it, and defaulting to dark text over a dark brand header is unreadable, so
|
|
56
60
|
* derive something legible from the background instead.
|
|
57
61
|
*/
|
|
58
|
-
function resolveHeaderTextColor(config: WidgetConfig | null, fallbackColor: string): string {
|
|
62
|
+
export function resolveHeaderTextColor(config: WidgetConfig | null, fallbackColor: string): string {
|
|
59
63
|
if (config?.homeHeaderTextColor === 'white') return palette.white;
|
|
60
64
|
if (config?.homeHeaderTextColor === 'black') return palette.ink;
|
|
61
65
|
if (config?.homeBackgroundType === 'image') return palette.white;
|
|
@@ -116,8 +120,14 @@ export function HomeView({
|
|
|
116
120
|
|
|
117
121
|
// The start card is pulled up over the seam by a fixed amount, so the header
|
|
118
122
|
// pads its bottom by that same amount and the greeting never slides under it.
|
|
123
|
+
// The header reaches up under the status bar (no white strip above it), so
|
|
124
|
+
// it grows by that inset and keeps its content where it was.
|
|
125
|
+
const topInset = useTopInset();
|
|
119
126
|
const headerStyle = [
|
|
120
127
|
styles.header,
|
|
128
|
+
topInset > 0
|
|
129
|
+
? { paddingTop: spacing.xl + topInset, minHeight: HEADER_MIN_HEIGHT + topInset }
|
|
130
|
+
: null,
|
|
121
131
|
allowNewConversations ? { paddingBottom: HEADER_BOTTOM_PADDING + START_CARD_LIFT } : null,
|
|
122
132
|
];
|
|
123
133
|
|
|
@@ -125,12 +135,7 @@ export function HomeView({
|
|
|
125
135
|
<>
|
|
126
136
|
<View style={styles.headerTop}>
|
|
127
137
|
{config?.homeLogoUrl ? (
|
|
128
|
-
<
|
|
129
|
-
source={{ uri: config.homeLogoUrl }}
|
|
130
|
-
style={styles.logo}
|
|
131
|
-
resizeMode="contain"
|
|
132
|
-
accessibilityIgnoresInvertColors
|
|
133
|
-
/>
|
|
138
|
+
<HomeLogo key={config.homeLogoUrl} uri={config.homeLogoUrl} />
|
|
134
139
|
) : (
|
|
135
140
|
<View />
|
|
136
141
|
)}
|
|
@@ -296,6 +301,29 @@ export function HomeView({
|
|
|
296
301
|
);
|
|
297
302
|
}
|
|
298
303
|
|
|
304
|
+
/**
|
|
305
|
+
* The workspace logo at its own shape, flush with the greeting's left edge.
|
|
306
|
+
* A fixed-width box with `contain` centred a square logo inside it, so it
|
|
307
|
+
* floated in from the edge; the web widget sizes it to the image instead.
|
|
308
|
+
* Square until the image reports its size, then as wide as its ratio needs,
|
|
309
|
+
* capped like the web widget.
|
|
310
|
+
*/
|
|
311
|
+
function HomeLogo({ uri }: { uri: string }) {
|
|
312
|
+
const [aspectRatio, setAspectRatio] = useState(1);
|
|
313
|
+
return (
|
|
314
|
+
<Image
|
|
315
|
+
source={{ uri }}
|
|
316
|
+
style={[styles.logo, { width: Math.min(LOGO_MAX_WIDTH, LOGO_HEIGHT * aspectRatio) }]}
|
|
317
|
+
resizeMode="contain"
|
|
318
|
+
accessibilityIgnoresInvertColors
|
|
319
|
+
onLoad={(event) => {
|
|
320
|
+
const { width, height } = event.nativeEvent.source ?? {};
|
|
321
|
+
if (width && height) setAspectRatio(width / height);
|
|
322
|
+
}}
|
|
323
|
+
/>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
299
327
|
/**
|
|
300
328
|
* Staggered entrance for the body sections. Opacity and translate only, so the
|
|
301
329
|
* whole thing runs on the native driver and never touches layout.
|