@ethora/chat-component-rn 26.7.3 → 26.7.4
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/CHANGELOG.md +45 -0
- package/README.md +9 -8
- package/lib/commonjs/assets/icons.js +7 -2
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/Header/HeaderRoomList.js +20 -6
- package/lib/commonjs/components/Header/HeaderRoomList.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +26 -4
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MessageList.js +2 -1
- package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.js +5 -5
- package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.js.map +1 -1
- package/lib/commonjs/components/MainComponents/RoomList.js +62 -32
- package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
- package/lib/commonjs/components/Menu/HeaderRoomListMenu.js +208 -70
- package/lib/commonjs/components/Menu/HeaderRoomListMenu.js.map +1 -1
- package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js +307 -99
- package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +552 -213
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ReportChatModal.js +226 -0
- package/lib/commonjs/components/Modals/ChatProfileModal/ReportChatModal.js.map +1 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +6 -2
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/Modals/Modal/Modal.js +8 -0
- package/lib/commonjs/components/Modals/Modal/Modal.js.map +1 -1
- package/lib/commonjs/components/Modals/NewChatModal/NewChatModal.js +202 -104
- package/lib/commonjs/components/Modals/NewChatModal/NewChatModal.js.map +1 -1
- package/lib/commonjs/components/Modals/ProfileHeader/ProfileHeader.js +556 -0
- package/lib/commonjs/components/Modals/ProfileHeader/ProfileHeader.js.map +1 -0
- package/lib/commonjs/components/Modals/ProfileHeader/ProfileMenu.js +210 -0
- package/lib/commonjs/components/Modals/ProfileHeader/ProfileMenu.js.map +1 -0
- package/lib/commonjs/components/Modals/SelectUsersModal/SelectUsersModal.js +4 -2
- package/lib/commonjs/components/Modals/SelectUsersModal/SelectUsersModal.js.map +1 -1
- package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +507 -244
- package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
- package/lib/commonjs/components/Modals/UserSettingsModal/UserSettingsModal.js +74 -46
- package/lib/commonjs/components/Modals/UserSettingsModal/UserSettingsModal.js.map +1 -1
- package/lib/commonjs/components/Modals/styledModalComponents.js +9 -2
- package/lib/commonjs/components/Modals/styledModalComponents.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/ChatRoomItem.js +17 -7
- package/lib/commonjs/components/RoomComponents/ChatRoomItem.js.map +1 -1
- package/lib/commonjs/components/styled/SendInput.js +80 -1
- package/lib/commonjs/components/styled/SendInput.js.map +1 -1
- package/lib/commonjs/components/styled/StyledComponents.js +9 -3
- package/lib/commonjs/components/styled/StyledComponents.js.map +1 -1
- package/lib/commonjs/components/styled/StyledInputComponents/StyledInputComponents.js +11 -4
- package/lib/commonjs/components/styled/StyledInputComponents/StyledInputComponents.js.map +1 -1
- package/lib/commonjs/helpers/getChatBackground.js +23 -0
- package/lib/commonjs/helpers/getChatBackground.js.map +1 -0
- package/lib/commonjs/helpers/mediaLibraryRuntime.js +31 -0
- package/lib/commonjs/helpers/mediaLibraryRuntime.js.map +1 -0
- package/lib/commonjs/helpers/sheetGestures.js +32 -0
- package/lib/commonjs/helpers/sheetGestures.js.map +1 -0
- package/lib/commonjs/i18n/strings.js +121 -1
- package/lib/commonjs/i18n/strings.js.map +1 -1
- package/lib/commonjs/networking/api-requests/user.api.js +59 -0
- package/lib/commonjs/networking/api-requests/user.api.js.map +1 -1
- package/lib/commonjs/roomStore/chatSettingsSlice.js +3 -1
- package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/module/assets/icons.js +6 -1
- package/lib/module/assets/icons.js.map +1 -1
- package/lib/module/components/Header/HeaderRoomList.js +20 -6
- package/lib/module/components/Header/HeaderRoomList.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +26 -4
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/MessageList.js +2 -1
- package/lib/module/components/MainComponents/MessageList.js.map +1 -1
- package/lib/module/components/MainComponents/ProfileImagePlaceholder.js +5 -5
- package/lib/module/components/MainComponents/ProfileImagePlaceholder.js.map +1 -1
- package/lib/module/components/MainComponents/RoomList.js +62 -32
- package/lib/module/components/MainComponents/RoomList.js.map +1 -1
- package/lib/module/components/Menu/HeaderRoomListMenu.js +211 -73
- package/lib/module/components/Menu/HeaderRoomListMenu.js.map +1 -1
- package/lib/module/components/Modals/AttachSheet/AttachSheet.js +298 -102
- package/lib/module/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +555 -217
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ReportChatModal.js +223 -0
- package/lib/module/components/Modals/ChatProfileModal/ReportChatModal.js.map +1 -0
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +6 -2
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/Modals/Modal/Modal.js +9 -1
- package/lib/module/components/Modals/Modal/Modal.js.map +1 -1
- package/lib/module/components/Modals/NewChatModal/NewChatModal.js +205 -106
- package/lib/module/components/Modals/NewChatModal/NewChatModal.js.map +1 -1
- package/lib/module/components/Modals/ProfileHeader/ProfileHeader.js +548 -0
- package/lib/module/components/Modals/ProfileHeader/ProfileHeader.js.map +1 -0
- package/lib/module/components/Modals/ProfileHeader/ProfileMenu.js +205 -0
- package/lib/module/components/Modals/ProfileHeader/ProfileMenu.js.map +1 -0
- package/lib/module/components/Modals/SelectUsersModal/SelectUsersModal.js +4 -2
- package/lib/module/components/Modals/SelectUsersModal/SelectUsersModal.js.map +1 -1
- package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +509 -248
- package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
- package/lib/module/components/Modals/UserSettingsModal/UserSettingsModal.js +76 -48
- package/lib/module/components/Modals/UserSettingsModal/UserSettingsModal.js.map +1 -1
- package/lib/module/components/Modals/styledModalComponents.js +9 -2
- package/lib/module/components/Modals/styledModalComponents.js.map +1 -1
- package/lib/module/components/RoomComponents/ChatRoomItem.js +17 -7
- package/lib/module/components/RoomComponents/ChatRoomItem.js.map +1 -1
- package/lib/module/components/styled/SendInput.js +80 -1
- package/lib/module/components/styled/SendInput.js.map +1 -1
- package/lib/module/components/styled/StyledComponents.js +9 -3
- package/lib/module/components/styled/StyledComponents.js.map +1 -1
- package/lib/module/components/styled/StyledInputComponents/StyledInputComponents.js +11 -4
- package/lib/module/components/styled/StyledInputComponents/StyledInputComponents.js.map +1 -1
- package/lib/module/helpers/getChatBackground.js +18 -0
- package/lib/module/helpers/getChatBackground.js.map +1 -0
- package/lib/module/helpers/mediaLibraryRuntime.js +25 -0
- package/lib/module/helpers/mediaLibraryRuntime.js.map +1 -0
- package/lib/module/helpers/sheetGestures.js +26 -0
- package/lib/module/helpers/sheetGestures.js.map +1 -0
- package/lib/module/i18n/strings.js +121 -1
- package/lib/module/i18n/strings.js.map +1 -1
- package/lib/module/networking/api-requests/user.api.js +56 -0
- package/lib/module/networking/api-requests/user.api.js.map +1 -1
- package/lib/module/roomStore/chatSettingsSlice.js +3 -1
- package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/typescript/assets/icons.d.ts +12 -0
- package/lib/typescript/assets/icons.d.ts.map +1 -1
- package/lib/typescript/components/Header/HeaderRoomList.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
- package/lib/typescript/components/Menu/HeaderRoomListMenu.d.ts.map +1 -1
- package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts +9 -0
- package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts +10 -0
- package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/ReportChatModal.d.ts +9 -0
- package/lib/typescript/components/Modals/ChatProfileModal/ReportChatModal.d.ts.map +1 -0
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/Modal/Modal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/NewChatModal/NewChatModal.d.ts +1 -1
- package/lib/typescript/components/Modals/NewChatModal/NewChatModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ProfileHeader/ProfileHeader.d.ts +75 -0
- package/lib/typescript/components/Modals/ProfileHeader/ProfileHeader.d.ts.map +1 -0
- package/lib/typescript/components/Modals/ProfileHeader/ProfileMenu.d.ts +25 -0
- package/lib/typescript/components/Modals/ProfileHeader/ProfileMenu.d.ts.map +1 -0
- package/lib/typescript/components/Modals/SelectUsersModal/SelectUsersModal.d.ts +6 -1
- package/lib/typescript/components/Modals/SelectUsersModal/SelectUsersModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts +16 -0
- package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/UserSettingsModal/UserSettingsModal.d.ts +1 -0
- package/lib/typescript/components/Modals/UserSettingsModal/UserSettingsModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/styledModalComponents.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/ChatRoomItem.d.ts.map +1 -1
- package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
- package/lib/typescript/components/styled/StyledComponents.d.ts.map +1 -1
- package/lib/typescript/components/styled/StyledInputComponents/StyledInputComponents.d.ts.map +1 -1
- package/lib/typescript/helpers/getChatBackground.d.ts +15 -0
- package/lib/typescript/helpers/getChatBackground.d.ts.map +1 -0
- package/lib/typescript/helpers/mediaLibraryRuntime.d.ts +3 -0
- package/lib/typescript/helpers/mediaLibraryRuntime.d.ts.map +1 -0
- package/lib/typescript/helpers/sheetGestures.d.ts +20 -0
- package/lib/typescript/helpers/sheetGestures.d.ts.map +1 -0
- package/lib/typescript/i18n/strings.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/user.api.d.ts +27 -0
- package/lib/typescript/networking/api-requests/user.api.d.ts.map +1 -1
- package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +8 -5
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/assets/icons.tsx +29 -9
- package/src/components/Header/HeaderRoomList.tsx +20 -5
- package/src/components/MainComponents/ChatRoom.tsx +29 -7
- package/src/components/MainComponents/MessageList.tsx +2 -1
- package/src/components/MainComponents/ProfileImagePlaceholder.tsx +5 -5
- package/src/components/MainComponents/RoomList.tsx +65 -34
- package/src/components/Menu/HeaderRoomListMenu.tsx +209 -64
- package/src/components/Modals/AttachSheet/AttachSheet.tsx +318 -86
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +598 -279
- package/src/components/Modals/ChatProfileModal/ReportChatModal.tsx +244 -0
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +4 -2
- package/src/components/Modals/Modal/Modal.tsx +7 -0
- package/src/components/Modals/NewChatModal/NewChatModal.tsx +219 -117
- package/src/components/Modals/ProfileHeader/ProfileHeader.tsx +615 -0
- package/src/components/Modals/ProfileHeader/ProfileMenu.tsx +217 -0
- package/src/components/Modals/SelectUsersModal/SelectUsersModal.tsx +14 -4
- package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +617 -307
- package/src/components/Modals/UserSettingsModal/UserSettingsModal.tsx +75 -58
- package/src/components/Modals/styledModalComponents.tsx +9 -2
- package/src/components/RoomComponents/ChatRoomItem.tsx +14 -7
- package/src/components/styled/SendInput.tsx +83 -2
- package/src/components/styled/StyledComponents.ts +9 -3
- package/src/components/styled/StyledInputComponents/StyledInputComponents.tsx +11 -4
- package/src/helpers/getChatBackground.ts +18 -0
- package/src/helpers/mediaLibraryRuntime.ts +25 -0
- package/src/helpers/sheetGestures.ts +25 -0
- package/src/i18n/strings.ts +121 -1
- package/src/networking/api-requests/user.api.ts +71 -0
- package/src/roomStore/chatSettingsSlice.ts +3 -1
- package/src/types/types.ts +8 -5
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
/** @format */
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Animated,
|
|
6
|
+
Image,
|
|
7
|
+
StyleProp,
|
|
8
|
+
StyleSheet,
|
|
9
|
+
Text,
|
|
10
|
+
TextStyle,
|
|
11
|
+
TouchableOpacity,
|
|
12
|
+
View,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
15
|
+
import { BackIcon } from '../../../assets/icons';
|
|
16
|
+
import { getAvatarTextColor } from '../../../helpers/getAvatarColor';
|
|
17
|
+
|
|
18
|
+
/** Height of the expanded photo hero. */
|
|
19
|
+
export const HERO_HEIGHT = 320;
|
|
20
|
+
/** Back / "…" row, directly below the status-bar inset. */
|
|
21
|
+
export const TOP_BAR_BUTTONS = 60;
|
|
22
|
+
/**
|
|
23
|
+
* Avatar + name row underneath it, revealed as the header collapses.
|
|
24
|
+
* Taller than the 48pt avatar it holds, so the row breathes: the extra
|
|
25
|
+
* height splits evenly above and below (the row centres its content),
|
|
26
|
+
* putting real space between the avatar and both the buttons above and
|
|
27
|
+
* the content below.
|
|
28
|
+
*/
|
|
29
|
+
export const TOP_BAR_NAME = 92;
|
|
30
|
+
/** Content band of the collapsed bar, below the status-bar inset. */
|
|
31
|
+
export const TOP_BAR_BAND = TOP_BAR_BUTTONS + TOP_BAR_NAME;
|
|
32
|
+
/** How far above the fully-collapsed point the bar starts fading in. */
|
|
33
|
+
const COLLAPSE_RAMP = 48;
|
|
34
|
+
/**
|
|
35
|
+
* Blur strength once the header is fully collapsed. The design's collapsed
|
|
36
|
+
* bar is frosted heavily — the picture is a mood, not a subject — so this
|
|
37
|
+
* sits near the top of expo-blur's 0-100 range, with a dim over it to keep
|
|
38
|
+
* the white text and the sharp round avatar reading against it.
|
|
39
|
+
*/
|
|
40
|
+
const BLUR_INTENSITY = 92;
|
|
41
|
+
/** Darkening applied on top of the blur (and used alone without it). */
|
|
42
|
+
const FROST_DIM = 'rgba(0,0,0,0.28)';
|
|
43
|
+
/** Height of the readability gradient under the name and buttons. */
|
|
44
|
+
const SCRIM_HEIGHT = 220;
|
|
45
|
+
/** Bands the gradient is built from — enough that no step is visible. */
|
|
46
|
+
const SCRIM_BANDS = 16;
|
|
47
|
+
/** Darkest the gradient gets, at the very bottom of the hero. */
|
|
48
|
+
const SCRIM_MAX_ALPHA = 0.45;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* How far above the hero its backdrop reaches. Over-scrolling downwards
|
|
52
|
+
* exposes that band, and the picture grows into it (anchored at its own
|
|
53
|
+
* bottom edge) rather than a second copy being painted there.
|
|
54
|
+
*/
|
|
55
|
+
const OVERSCROLL_REACH = 400;
|
|
56
|
+
|
|
57
|
+
// expo-blur is an OPTIONAL peer. Having the JS package is not enough — the
|
|
58
|
+
// native view has to be in the binary too, and an app that installed the
|
|
59
|
+
// package without rebuilding renders a red "Unimplemented component:
|
|
60
|
+
// ExpoBlurView" box instead of a blur. `requireNativeModule` throws when
|
|
61
|
+
// that native half is missing, which is the only reliable way to tell the
|
|
62
|
+
// two apart, so probe it once and fall back to a plain dim otherwise.
|
|
63
|
+
let BlurView: any = null;
|
|
64
|
+
try {
|
|
65
|
+
const { requireNativeModule } = require('expo-modules-core');
|
|
66
|
+
requireNativeModule('ExpoBlur');
|
|
67
|
+
BlurView = require('expo-blur').BlurView ?? null;
|
|
68
|
+
} catch {
|
|
69
|
+
BlurView = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** True when the frosted-glass layer is the real thing, not the dim. */
|
|
73
|
+
export const hasNativeBlur = () => !!BlurView;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Bottom-up darkening so white text stays readable over a photo.
|
|
77
|
+
*
|
|
78
|
+
* A single flat overlay (what this was) has a hard top edge that reads as
|
|
79
|
+
* "the bottom half of the picture is dark". There is no gradient
|
|
80
|
+
* dependency in this package, so it is stacked out of thin bands whose
|
|
81
|
+
* alpha ramps quadratically — at 16 bands over 220 pt the steps are below
|
|
82
|
+
* what the eye picks up on a photo.
|
|
83
|
+
*/
|
|
84
|
+
const ScrimGradient: React.FC = () => (
|
|
85
|
+
<View pointerEvents="none" style={styles.scrim}>
|
|
86
|
+
{Array.from({ length: SCRIM_BANDS }, (_, i) => (
|
|
87
|
+
<View
|
|
88
|
+
key={i}
|
|
89
|
+
style={{
|
|
90
|
+
height: SCRIM_HEIGHT / SCRIM_BANDS,
|
|
91
|
+
backgroundColor: `rgba(0,0,0,${(
|
|
92
|
+
SCRIM_MAX_ALPHA * ((i + 1) / SCRIM_BANDS) ** 2
|
|
93
|
+
).toFixed(3)})`,
|
|
94
|
+
}}
|
|
95
|
+
/>
|
|
96
|
+
))}
|
|
97
|
+
</View>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const FrostLayer: React.FC<{ tint?: 'dark' | 'light' }> = ({ tint = 'dark' }) =>
|
|
101
|
+
BlurView ? (
|
|
102
|
+
<>
|
|
103
|
+
<BlurView
|
|
104
|
+
intensity={BLUR_INTENSITY}
|
|
105
|
+
tint={tint}
|
|
106
|
+
style={StyleSheet.absoluteFill}
|
|
107
|
+
/>
|
|
108
|
+
<View style={[StyleSheet.absoluteFill, styles.frostDim]} />
|
|
109
|
+
</>
|
|
110
|
+
) : (
|
|
111
|
+
<View style={[StyleSheet.absoluteFill, styles.frostFallback]} />
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
export interface HeroAction {
|
|
115
|
+
key: string;
|
|
116
|
+
label: string;
|
|
117
|
+
/** Rendered inside a circle tinted for the current background. */
|
|
118
|
+
icon: (color: string) => React.ReactNode;
|
|
119
|
+
onPress: () => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface CommonProps {
|
|
123
|
+
title: string;
|
|
124
|
+
subtitle?: string;
|
|
125
|
+
imageUri?: string | null;
|
|
126
|
+
/** Background when the chat has no picture — one flat colour, no scrims. */
|
|
127
|
+
fallbackColor: string;
|
|
128
|
+
/** Two uppercase letters shown on that background. */
|
|
129
|
+
initials: string;
|
|
130
|
+
scrollY: Animated.Value;
|
|
131
|
+
/** Prefixes every testID, so each screen's tests address their own. */
|
|
132
|
+
testIDPrefix?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Everything the screen needs to line its scroll content up with the bar. */
|
|
136
|
+
export const useHeaderMetrics = () => {
|
|
137
|
+
const insets = useSafeAreaInsets();
|
|
138
|
+
const barHeight = insets.top + TOP_BAR_BAND;
|
|
139
|
+
return {
|
|
140
|
+
insets,
|
|
141
|
+
barHeight,
|
|
142
|
+
/** Scroll distance between the expanded hero and the collapsed bar. */
|
|
143
|
+
collapseDistance: Math.max(HERO_HEIGHT - barHeight, 1),
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const useCollapse = (scrollY: Animated.Value, collapseDistance: number) =>
|
|
148
|
+
scrollY.interpolate({
|
|
149
|
+
inputRange: [
|
|
150
|
+
Math.max(collapseDistance - COLLAPSE_RAMP, 0),
|
|
151
|
+
collapseDistance,
|
|
152
|
+
],
|
|
153
|
+
outputRange: [0, 1],
|
|
154
|
+
extrapolate: 'clamp',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The chat picture, blown up to a full-bleed hero with the name, member
|
|
159
|
+
* count and the round action buttons on top of it.
|
|
160
|
+
*
|
|
161
|
+
* It lives INSIDE the ScrollView (rather than behind it) so the action
|
|
162
|
+
* buttons stay tappable — an absolutely-positioned hero would sit under a
|
|
163
|
+
* full-screen scroll surface that swallows every touch. Parallax is done
|
|
164
|
+
* on the inner image instead: the wrapper keeps its layout height while
|
|
165
|
+
* the picture drifts at ~⅓ of the scroll speed, and over-scrolling
|
|
166
|
+
* upwards scales it up the way iOS profile headers do.
|
|
167
|
+
*/
|
|
168
|
+
export const ProfileHero: React.FC<
|
|
169
|
+
CommonProps & {
|
|
170
|
+
actions: HeroAction[];
|
|
171
|
+
/** Rendered to the right of the name (the profile's Leave button). */
|
|
172
|
+
titleAccessory?: React.ReactNode;
|
|
173
|
+
titleStyle?: StyleProp<TextStyle>;
|
|
174
|
+
subtitleStyle?: StyleProp<TextStyle>;
|
|
175
|
+
}
|
|
176
|
+
> = ({
|
|
177
|
+
title,
|
|
178
|
+
subtitle,
|
|
179
|
+
imageUri,
|
|
180
|
+
fallbackColor,
|
|
181
|
+
initials,
|
|
182
|
+
scrollY,
|
|
183
|
+
actions,
|
|
184
|
+
titleAccessory,
|
|
185
|
+
titleStyle,
|
|
186
|
+
subtitleStyle,
|
|
187
|
+
testIDPrefix = 'chat-profile',
|
|
188
|
+
}) => {
|
|
189
|
+
const { collapseDistance } = useHeaderMetrics();
|
|
190
|
+
const parallax = scrollY.interpolate({
|
|
191
|
+
inputRange: [0, HERO_HEIGHT],
|
|
192
|
+
outputRange: [0, HERO_HEIGHT * 0.35],
|
|
193
|
+
// Clamped on the left: with `extend` an over-scroll pulled the picture
|
|
194
|
+
// UP off its own bottom edge, exposing a band of the backdrop colour
|
|
195
|
+
// under it. Pulling down is the scale's job alone.
|
|
196
|
+
extrapolateLeft: 'clamp',
|
|
197
|
+
extrapolateRight: 'clamp',
|
|
198
|
+
});
|
|
199
|
+
const scale = scrollY.interpolate({
|
|
200
|
+
inputRange: [-HERO_HEIGHT, 0],
|
|
201
|
+
outputRange: [2, 1],
|
|
202
|
+
extrapolateRight: 'clamp',
|
|
203
|
+
});
|
|
204
|
+
// The name and buttons are gone well before the collapsed bar arrives,
|
|
205
|
+
// so the two never overlap mid-scroll.
|
|
206
|
+
const contentOpacity = scrollY.interpolate({
|
|
207
|
+
inputRange: [0, HERO_HEIGHT * 0.4],
|
|
208
|
+
outputRange: [1, 0],
|
|
209
|
+
extrapolate: 'clamp',
|
|
210
|
+
});
|
|
211
|
+
const collapsed = useCollapse(scrollY, collapseDistance);
|
|
212
|
+
|
|
213
|
+
// On a photo everything is white over a bottom scrim. On the flat
|
|
214
|
+
// fallback colour there is no scrim at all — that double-tone banding
|
|
215
|
+
// was two stacked scrims showing their seam — so the text takes
|
|
216
|
+
// whichever of black/white reads on that colour.
|
|
217
|
+
const onPhoto = !!imageUri;
|
|
218
|
+
const contentColor = onPhoto ? '#FFFFFF' : getAvatarTextColor(fallbackColor);
|
|
219
|
+
// The glyphs inside the circles are always white — several of the SDK's
|
|
220
|
+
// icons paint white regardless of their `color` prop, so tinting the
|
|
221
|
+
// rest to match the title left a mix of black and white glyphs in one
|
|
222
|
+
// row. The circle darkens instead, which keeps them readable on a light
|
|
223
|
+
// fill as well as on a photo.
|
|
224
|
+
const iconColor = '#FFFFFF';
|
|
225
|
+
const circleTint = onPhoto
|
|
226
|
+
? 'rgba(255,255,255,0.22)'
|
|
227
|
+
: contentColor === '#FFFFFF'
|
|
228
|
+
? 'rgba(255,255,255,0.22)'
|
|
229
|
+
: 'rgba(0,0,0,0.22)';
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<View style={[styles.hero, { backgroundColor: fallbackColor }]}>
|
|
233
|
+
<View style={[styles.backdropClip, { backgroundColor: fallbackColor }]}>
|
|
234
|
+
<Animated.View
|
|
235
|
+
style={[
|
|
236
|
+
styles.backdrop,
|
|
237
|
+
{
|
|
238
|
+
transform: [
|
|
239
|
+
{ translateY: parallax },
|
|
240
|
+
// Scale about the bottom edge (translate → scale →
|
|
241
|
+
// translate back, RN has no transform-origin) so pulling
|
|
242
|
+
// down grows the picture upwards into the exposed band by
|
|
243
|
+
// exactly the amount that band opened.
|
|
244
|
+
{ translateY: HERO_HEIGHT / 2 },
|
|
245
|
+
{ scale },
|
|
246
|
+
{ translateY: -HERO_HEIGHT / 2 },
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
]}
|
|
250
|
+
>
|
|
251
|
+
{onPhoto ? (
|
|
252
|
+
<Image
|
|
253
|
+
testID={`${testIDPrefix}-hero-image`}
|
|
254
|
+
source={{ uri: imageUri as string }}
|
|
255
|
+
style={StyleSheet.absoluteFill}
|
|
256
|
+
resizeMode="cover"
|
|
257
|
+
/>
|
|
258
|
+
) : (
|
|
259
|
+
<View style={styles.initialsWrap}>
|
|
260
|
+
<Text
|
|
261
|
+
testID={`${testIDPrefix}-hero-initials`}
|
|
262
|
+
style={[styles.heroInitials, { color: contentColor }]}
|
|
263
|
+
>
|
|
264
|
+
{initials}
|
|
265
|
+
</Text>
|
|
266
|
+
</View>
|
|
267
|
+
)}
|
|
268
|
+
</Animated.View>
|
|
269
|
+
|
|
270
|
+
{/* Frosts the picture as the header collapses — the same layer the
|
|
271
|
+
* collapsed bar sits on, so the transition reads as one motion.
|
|
272
|
+
* Only over a photo: there is nothing to blur on a flat colour,
|
|
273
|
+
* and the dim fallback would just darken it into a second shade
|
|
274
|
+
* of itself. */}
|
|
275
|
+
{onPhoto && (
|
|
276
|
+
<Animated.View
|
|
277
|
+
testID={`${testIDPrefix}-hero-blur`}
|
|
278
|
+
pointerEvents="none"
|
|
279
|
+
style={[StyleSheet.absoluteFill, { opacity: collapsed }]}
|
|
280
|
+
>
|
|
281
|
+
<FrostLayer />
|
|
282
|
+
</Animated.View>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{onPhoto && <ScrimGradient />}
|
|
286
|
+
</View>
|
|
287
|
+
|
|
288
|
+
<Animated.View style={[styles.heroContent, { opacity: contentOpacity }]}>
|
|
289
|
+
<View style={styles.titleRow}>
|
|
290
|
+
<Text
|
|
291
|
+
numberOfLines={2}
|
|
292
|
+
style={[
|
|
293
|
+
styles.heroTitle,
|
|
294
|
+
styles.titleText,
|
|
295
|
+
{ color: contentColor },
|
|
296
|
+
titleStyle,
|
|
297
|
+
]}
|
|
298
|
+
>
|
|
299
|
+
{title}
|
|
300
|
+
</Text>
|
|
301
|
+
{titleAccessory}
|
|
302
|
+
</View>
|
|
303
|
+
{!!subtitle && (
|
|
304
|
+
<Text
|
|
305
|
+
style={[
|
|
306
|
+
styles.heroSubtitle,
|
|
307
|
+
{ color: contentColor, opacity: 0.85 },
|
|
308
|
+
subtitleStyle,
|
|
309
|
+
]}
|
|
310
|
+
>
|
|
311
|
+
{subtitle}
|
|
312
|
+
</Text>
|
|
313
|
+
)}
|
|
314
|
+
{actions.length > 0 && (
|
|
315
|
+
<View style={styles.actionsRow}>
|
|
316
|
+
{actions.map((action) => (
|
|
317
|
+
<TouchableOpacity
|
|
318
|
+
key={action.key}
|
|
319
|
+
testID={`${testIDPrefix}-action-${action.key}`}
|
|
320
|
+
activeOpacity={0.7}
|
|
321
|
+
style={styles.action}
|
|
322
|
+
onPress={action.onPress}
|
|
323
|
+
>
|
|
324
|
+
<View
|
|
325
|
+
style={[styles.actionCircle, { backgroundColor: circleTint }]}
|
|
326
|
+
>
|
|
327
|
+
{action.icon(iconColor)}
|
|
328
|
+
</View>
|
|
329
|
+
<Text style={[styles.actionLabel, { color: contentColor }]}>
|
|
330
|
+
{action.label}
|
|
331
|
+
</Text>
|
|
332
|
+
</TouchableOpacity>
|
|
333
|
+
))}
|
|
334
|
+
</View>
|
|
335
|
+
)}
|
|
336
|
+
</Animated.View>
|
|
337
|
+
</View>
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The bar pinned over the hero: back button and the "…" menu are always
|
|
343
|
+
* there, while the frosted picture, small avatar and name fade in as the
|
|
344
|
+
* hero scrolls away — the collapsed state from the design's third screen.
|
|
345
|
+
*/
|
|
346
|
+
export const ProfileTopBar: React.FC<
|
|
347
|
+
CommonProps & {
|
|
348
|
+
onBack: () => void;
|
|
349
|
+
menu?: React.ReactNode;
|
|
350
|
+
titleStyle?: StyleProp<TextStyle>;
|
|
351
|
+
}
|
|
352
|
+
> = ({
|
|
353
|
+
title,
|
|
354
|
+
subtitle,
|
|
355
|
+
imageUri,
|
|
356
|
+
fallbackColor,
|
|
357
|
+
initials,
|
|
358
|
+
scrollY,
|
|
359
|
+
onBack,
|
|
360
|
+
menu,
|
|
361
|
+
titleStyle,
|
|
362
|
+
testIDPrefix = 'chat-profile',
|
|
363
|
+
}) => {
|
|
364
|
+
const { insets, barHeight, collapseDistance } = useHeaderMetrics();
|
|
365
|
+
const collapsed = useCollapse(scrollY, collapseDistance);
|
|
366
|
+
const rise = collapsed.interpolate({
|
|
367
|
+
inputRange: [0, 1],
|
|
368
|
+
outputRange: [10, 0],
|
|
369
|
+
});
|
|
370
|
+
const onPhoto = !!imageUri;
|
|
371
|
+
const contentColor = onPhoto ? '#FFFFFF' : getAvatarTextColor(fallbackColor);
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
<View
|
|
375
|
+
pointerEvents="box-none"
|
|
376
|
+
style={[styles.topBar, { height: barHeight, paddingTop: insets.top }]}
|
|
377
|
+
>
|
|
378
|
+
{/* Background layer: the same picture, cropped to the bar and frosted,
|
|
379
|
+
* so nothing flips colour mid-scroll. */}
|
|
380
|
+
<Animated.View
|
|
381
|
+
pointerEvents="none"
|
|
382
|
+
style={[
|
|
383
|
+
StyleSheet.absoluteFill,
|
|
384
|
+
{ backgroundColor: fallbackColor, opacity: collapsed },
|
|
385
|
+
]}
|
|
386
|
+
>
|
|
387
|
+
{onPhoto && (
|
|
388
|
+
<>
|
|
389
|
+
<Image
|
|
390
|
+
source={{ uri: imageUri as string }}
|
|
391
|
+
style={StyleSheet.absoluteFill}
|
|
392
|
+
resizeMode="cover"
|
|
393
|
+
/>
|
|
394
|
+
<FrostLayer />
|
|
395
|
+
</>
|
|
396
|
+
)}
|
|
397
|
+
</Animated.View>
|
|
398
|
+
|
|
399
|
+
<View style={styles.topBarButtons}>
|
|
400
|
+
<TouchableOpacity
|
|
401
|
+
testID={`${testIDPrefix}-back`}
|
|
402
|
+
activeOpacity={0.7}
|
|
403
|
+
onPress={onBack}
|
|
404
|
+
style={styles.roundButton}
|
|
405
|
+
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
406
|
+
>
|
|
407
|
+
<BackIcon color="#FFFFFF" />
|
|
408
|
+
</TouchableOpacity>
|
|
409
|
+
<View style={styles.topBarRight}>{menu}</View>
|
|
410
|
+
</View>
|
|
411
|
+
|
|
412
|
+
{/* Avatar + name on their own row, as in the design's collapsed
|
|
413
|
+
* screen. It only appears once the hero has scrolled away, so it
|
|
414
|
+
* never competes with the big title over the picture. */}
|
|
415
|
+
<Animated.View
|
|
416
|
+
testID={`${testIDPrefix}-collapsed-title`}
|
|
417
|
+
pointerEvents="none"
|
|
418
|
+
style={[
|
|
419
|
+
styles.collapsedTitle,
|
|
420
|
+
{ opacity: collapsed, transform: [{ translateY: rise }] },
|
|
421
|
+
]}
|
|
422
|
+
>
|
|
423
|
+
<View style={styles.collapsedAvatar}>
|
|
424
|
+
{onPhoto ? (
|
|
425
|
+
<Image
|
|
426
|
+
source={{ uri: imageUri as string }}
|
|
427
|
+
style={styles.collapsedAvatarImage}
|
|
428
|
+
/>
|
|
429
|
+
) : (
|
|
430
|
+
<View
|
|
431
|
+
style={[
|
|
432
|
+
styles.collapsedAvatarImage,
|
|
433
|
+
styles.initialsWrap,
|
|
434
|
+
{ backgroundColor: fallbackColor },
|
|
435
|
+
]}
|
|
436
|
+
>
|
|
437
|
+
<Text style={[styles.collapsedInitials, { color: contentColor }]}>
|
|
438
|
+
{initials}
|
|
439
|
+
</Text>
|
|
440
|
+
</View>
|
|
441
|
+
)}
|
|
442
|
+
</View>
|
|
443
|
+
<View style={styles.collapsedText}>
|
|
444
|
+
<Text numberOfLines={1} style={[styles.collapsedName, titleStyle]}>
|
|
445
|
+
{title}
|
|
446
|
+
</Text>
|
|
447
|
+
{!!subtitle && (
|
|
448
|
+
<Text numberOfLines={1} style={styles.collapsedSubtitle}>
|
|
449
|
+
{subtitle}
|
|
450
|
+
</Text>
|
|
451
|
+
)}
|
|
452
|
+
</View>
|
|
453
|
+
</Animated.View>
|
|
454
|
+
</View>
|
|
455
|
+
);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const styles = StyleSheet.create({
|
|
459
|
+
hero: {
|
|
460
|
+
width: '100%',
|
|
461
|
+
height: HERO_HEIGHT,
|
|
462
|
+
justifyContent: 'flex-end',
|
|
463
|
+
},
|
|
464
|
+
// Reaches above the hero and clips its own contents, so the picture can
|
|
465
|
+
// grow up into the over-scroll band without spilling over the cards
|
|
466
|
+
// below when it parallaxes down.
|
|
467
|
+
backdropClip: {
|
|
468
|
+
position: 'absolute',
|
|
469
|
+
left: 0,
|
|
470
|
+
right: 0,
|
|
471
|
+
bottom: 0,
|
|
472
|
+
height: HERO_HEIGHT + OVERSCROLL_REACH,
|
|
473
|
+
overflow: 'hidden',
|
|
474
|
+
},
|
|
475
|
+
backdrop: {
|
|
476
|
+
position: 'absolute',
|
|
477
|
+
left: 0,
|
|
478
|
+
right: 0,
|
|
479
|
+
bottom: 0,
|
|
480
|
+
height: HERO_HEIGHT,
|
|
481
|
+
},
|
|
482
|
+
initialsWrap: {
|
|
483
|
+
...StyleSheet.absoluteFill,
|
|
484
|
+
alignItems: 'center',
|
|
485
|
+
justifyContent: 'center',
|
|
486
|
+
},
|
|
487
|
+
heroInitials: {
|
|
488
|
+
fontSize: 64,
|
|
489
|
+
fontWeight: '600',
|
|
490
|
+
// Sit in the upper half so the name below never collides with it.
|
|
491
|
+
marginBottom: 120,
|
|
492
|
+
},
|
|
493
|
+
frostDim: {
|
|
494
|
+
backgroundColor: FROST_DIM,
|
|
495
|
+
},
|
|
496
|
+
frostFallback: {
|
|
497
|
+
backgroundColor: 'rgba(0,0,0,0.35)',
|
|
498
|
+
},
|
|
499
|
+
// Only over a photo: keeps white text readable without tinting the flat
|
|
500
|
+
// fallback colour.
|
|
501
|
+
scrim: {
|
|
502
|
+
...StyleSheet.absoluteFill,
|
|
503
|
+
top: undefined,
|
|
504
|
+
height: SCRIM_HEIGHT,
|
|
505
|
+
justifyContent: 'flex-end',
|
|
506
|
+
},
|
|
507
|
+
heroContent: {
|
|
508
|
+
paddingHorizontal: 20,
|
|
509
|
+
paddingBottom: 20,
|
|
510
|
+
},
|
|
511
|
+
// The accessory (the profile's Leave button) is pushed to the far right;
|
|
512
|
+
// the name takes what is left of the row.
|
|
513
|
+
titleRow: {
|
|
514
|
+
flexDirection: 'row',
|
|
515
|
+
alignItems: 'center',
|
|
516
|
+
justifyContent: 'space-between',
|
|
517
|
+
gap: 12,
|
|
518
|
+
},
|
|
519
|
+
titleText: {
|
|
520
|
+
flexShrink: 1,
|
|
521
|
+
},
|
|
522
|
+
heroTitle: {
|
|
523
|
+
fontSize: 26,
|
|
524
|
+
fontWeight: '600',
|
|
525
|
+
},
|
|
526
|
+
heroSubtitle: {
|
|
527
|
+
fontSize: 14,
|
|
528
|
+
marginTop: 2,
|
|
529
|
+
},
|
|
530
|
+
// Centred: Search / Leave / Report.
|
|
531
|
+
actionsRow: {
|
|
532
|
+
flexDirection: 'row',
|
|
533
|
+
justifyContent: 'center',
|
|
534
|
+
marginTop: 20,
|
|
535
|
+
gap: 16,
|
|
536
|
+
},
|
|
537
|
+
action: {
|
|
538
|
+
alignItems: 'center',
|
|
539
|
+
width: 64,
|
|
540
|
+
},
|
|
541
|
+
actionCircle: {
|
|
542
|
+
width: 56,
|
|
543
|
+
height: 56,
|
|
544
|
+
borderRadius: 28,
|
|
545
|
+
alignItems: 'center',
|
|
546
|
+
justifyContent: 'center',
|
|
547
|
+
},
|
|
548
|
+
actionLabel: {
|
|
549
|
+
fontSize: 12,
|
|
550
|
+
marginTop: 6,
|
|
551
|
+
},
|
|
552
|
+
topBar: {
|
|
553
|
+
position: 'absolute',
|
|
554
|
+
top: 0,
|
|
555
|
+
left: 0,
|
|
556
|
+
right: 0,
|
|
557
|
+
zIndex: 10,
|
|
558
|
+
},
|
|
559
|
+
topBarButtons: {
|
|
560
|
+
height: TOP_BAR_BUTTONS,
|
|
561
|
+
flexDirection: 'row',
|
|
562
|
+
alignItems: 'flex-end',
|
|
563
|
+
paddingBottom: 4,
|
|
564
|
+
// 20 = the collapsed row's 16pt inset + half the difference between
|
|
565
|
+
// the 48pt avatar and these 40pt buttons, so a button's centre line
|
|
566
|
+
// matches the avatar's once the header is collapsed.
|
|
567
|
+
paddingHorizontal: 20,
|
|
568
|
+
},
|
|
569
|
+
roundButton: {
|
|
570
|
+
width: 40,
|
|
571
|
+
height: 40,
|
|
572
|
+
borderRadius: 20,
|
|
573
|
+
backgroundColor: 'rgba(0,0,0,0.28)',
|
|
574
|
+
alignItems: 'center',
|
|
575
|
+
justifyContent: 'center',
|
|
576
|
+
},
|
|
577
|
+
topBarRight: {
|
|
578
|
+
marginLeft: 'auto',
|
|
579
|
+
},
|
|
580
|
+
collapsedTitle: {
|
|
581
|
+
height: TOP_BAR_NAME,
|
|
582
|
+
flexDirection: 'row',
|
|
583
|
+
alignItems: 'center',
|
|
584
|
+
paddingHorizontal: 16,
|
|
585
|
+
gap: 12,
|
|
586
|
+
},
|
|
587
|
+
collapsedAvatar: {
|
|
588
|
+
width: 48,
|
|
589
|
+
height: 48,
|
|
590
|
+
borderRadius: 24,
|
|
591
|
+
overflow: 'hidden',
|
|
592
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
593
|
+
},
|
|
594
|
+
collapsedAvatarImage: {
|
|
595
|
+
width: '100%',
|
|
596
|
+
height: '100%',
|
|
597
|
+
},
|
|
598
|
+
collapsedInitials: {
|
|
599
|
+
fontSize: 18,
|
|
600
|
+
fontWeight: '600',
|
|
601
|
+
},
|
|
602
|
+
collapsedText: {
|
|
603
|
+
flex: 1,
|
|
604
|
+
},
|
|
605
|
+
collapsedName: {
|
|
606
|
+
color: '#FFFFFF',
|
|
607
|
+
fontSize: 20,
|
|
608
|
+
fontWeight: '600',
|
|
609
|
+
},
|
|
610
|
+
collapsedSubtitle: {
|
|
611
|
+
color: 'rgba(255,255,255,0.8)',
|
|
612
|
+
fontSize: 13,
|
|
613
|
+
marginTop: 1,
|
|
614
|
+
},
|
|
615
|
+
});
|