@planningcenter/chat-react-native 1.4.2-rc.1 → 1.4.2
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/build/components/display/avatar.d.ts +10 -0
- package/build/components/display/avatar.d.ts.map +1 -0
- package/build/components/display/avatar.js +11 -0
- package/build/components/display/avatar.js.map +1 -0
- package/build/components/display/avatar_group.d.ts +9 -0
- package/build/components/display/avatar_group.d.ts.map +1 -0
- package/build/components/display/avatar_group.js +11 -0
- package/build/components/display/avatar_group.js.map +1 -0
- package/build/components/display/heading.d.ts +8 -0
- package/build/components/display/heading.d.ts.map +1 -0
- package/build/components/display/heading.js +53 -0
- package/build/components/display/heading.js.map +1 -0
- package/build/components/display/image.d.ts +10 -3
- package/build/components/display/image.d.ts.map +1 -1
- package/build/components/display/image.js +7 -5
- package/build/components/display/image.js.map +1 -1
- package/build/components/display/index.d.ts +4 -1
- package/build/components/display/index.d.ts.map +1 -1
- package/build/components/display/index.js +4 -1
- package/build/components/display/index.js.map +1 -1
- package/build/components/display/text.d.ts +1 -1
- package/build/components/display/text.d.ts.map +1 -1
- package/build/components/display/text.js +7 -6
- package/build/components/display/text.js.map +1 -1
- package/build/components/error_boundary.d.ts +1 -1
- package/build/components/primitive/avatar_primitive.d.ts +39 -0
- package/build/components/primitive/avatar_primitive.d.ts.map +1 -0
- package/build/components/primitive/avatar_primitive.js +204 -0
- package/build/components/primitive/avatar_primitive.js.map +1 -0
- package/build/contexts/api_provider.d.ts.map +1 -1
- package/build/contexts/api_provider.js +3 -14
- package/build/contexts/api_provider.js.map +1 -1
- package/build/contexts/chat_context.js +2 -2
- package/build/contexts/chat_context.js.map +1 -1
- package/build/index.js.map +1 -1
- package/build/screens/display.d.ts.map +1 -1
- package/build/screens/display.js +51 -10
- package/build/screens/display.js.map +1 -1
- package/build/utils/api.d.ts +9 -0
- package/build/utils/api.d.ts.map +1 -0
- package/build/utils/api.js +36 -0
- package/build/utils/api.js.map +1 -0
- package/build/utils/platform_styles.d.ts +2 -0
- package/build/utils/platform_styles.d.ts.map +1 -0
- package/build/utils/platform_styles.js +7 -0
- package/build/utils/platform_styles.js.map +1 -0
- package/build/utils/space.d.ts +3 -0
- package/build/utils/space.d.ts.map +1 -0
- package/build/utils/space.js +22 -0
- package/build/utils/space.js.map +1 -0
- package/build/utils/theme.d.ts +2 -2
- package/build/utils/theme.d.ts.map +1 -1
- package/build/utils/theme.js +2 -2
- package/build/utils/theme.js.map +1 -1
- package/build/vendor/tapestry/alias_tokens_color_map.d.ts +55 -0
- package/build/vendor/tapestry/alias_tokens_color_map.d.ts.map +1 -0
- package/build/vendor/tapestry/{tapestry_alias_tokens_color_map.js → alias_tokens_color_map.js} +4 -2
- package/build/vendor/tapestry/alias_tokens_color_map.js.map +1 -0
- package/build/vendor/tapestry/tokens.d.ts +49 -35
- package/build/vendor/tapestry/tokens.d.ts.map +1 -1
- package/build/vendor/tapestry/tokens.js +18 -0
- package/build/vendor/tapestry/tokens.js.map +1 -1
- package/package.json +8 -9
- package/src/__mocks__/@react-native-async-storage/async-storage.js +3 -0
- package/src/__mocks__/react-native-device-info.js +3 -0
- package/src/__tests__/hooks/useTheme.tsx +37 -0
- package/src/__tests__/utils/space.tsx +60 -0
- package/src/components/display/avatar.tsx +23 -0
- package/src/components/display/avatar_group.tsx +21 -0
- package/src/components/display/heading.tsx +71 -0
- package/src/components/display/image.tsx +20 -8
- package/src/components/display/index.ts +4 -1
- package/src/components/display/text.tsx +7 -6
- package/src/components/primitive/avatar_primitive.tsx +374 -0
- package/src/contexts/api_provider.tsx +4 -17
- package/src/contexts/chat_context.tsx +2 -2
- package/src/index.tsx +0 -1
- package/src/screens/display.tsx +52 -13
- package/src/utils/api.ts +47 -0
- package/src/utils/platform_styles.ts +7 -0
- package/src/utils/space.ts +39 -0
- package/src/utils/theme.ts +3 -3
- package/src/vendor/tapestry/{tapestry_alias_tokens_color_map.ts → alias_tokens_color_map.ts} +8 -5
- package/src/vendor/tapestry/tokens.ts +25 -50
- package/build/vendor/tapestry/tapestry_alias_tokens_color_map.d.ts +0 -53
- package/build/vendor/tapestry/tapestry_alias_tokens_color_map.d.ts.map +0 -1
- package/build/vendor/tapestry/tapestry_alias_tokens_color_map.js.map +0 -1
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useState } from 'react'
|
|
2
|
+
import { StyleSheet, View, ViewProps } from 'react-native'
|
|
3
|
+
import { useTheme } from '../../hooks'
|
|
4
|
+
import { Image, ImageProps } from '../display/image'
|
|
5
|
+
import { Spinner } from '../display/spinner'
|
|
6
|
+
|
|
7
|
+
// =================================
|
|
8
|
+
// ====== Exports ==================
|
|
9
|
+
// =================================
|
|
10
|
+
|
|
11
|
+
const Avatar = {
|
|
12
|
+
Root: AvatarRoot,
|
|
13
|
+
Image: AvatarImage,
|
|
14
|
+
Presence: AvatarPresence,
|
|
15
|
+
Group: AvatarGroup,
|
|
16
|
+
GroupLoader: AvatarGroupLoader,
|
|
17
|
+
Mask: AvatarMask,
|
|
18
|
+
} as const
|
|
19
|
+
|
|
20
|
+
type AvatarComponents = {
|
|
21
|
+
Root: React.FC<AvatarRootProps>
|
|
22
|
+
Image: React.FC<AvatarImageProps>
|
|
23
|
+
Presence: React.FC<AvatarPresenceProps>
|
|
24
|
+
Group: React.FC<AvatarGroupProps>
|
|
25
|
+
GroupLoader: React.FC<Record<string, never>>
|
|
26
|
+
Mask: React.FC<AvatarMaskProps>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default Avatar as AvatarComponents
|
|
30
|
+
export type {
|
|
31
|
+
AvatarImageProps,
|
|
32
|
+
AvatarPresenceProps,
|
|
33
|
+
AvatarGroupProps,
|
|
34
|
+
AvatarMaskProps,
|
|
35
|
+
AvatarRootProps,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// =================================
|
|
39
|
+
// ====== Constants & Types ========
|
|
40
|
+
// =================================
|
|
41
|
+
|
|
42
|
+
const AVATAR_SIZES = {
|
|
43
|
+
md: 'md',
|
|
44
|
+
lg: 'lg',
|
|
45
|
+
} as const
|
|
46
|
+
|
|
47
|
+
const AVATAR_PRESENCE_TYPES = {
|
|
48
|
+
online: 'online',
|
|
49
|
+
offline: 'offline',
|
|
50
|
+
} as const
|
|
51
|
+
|
|
52
|
+
// Progrmatically creates type unions
|
|
53
|
+
type AvatarSize = (typeof AVATAR_SIZES)[keyof typeof AVATAR_SIZES]
|
|
54
|
+
type AvatarPresenceType = (typeof AVATAR_PRESENCE_TYPES)[keyof typeof AVATAR_PRESENCE_TYPES]
|
|
55
|
+
|
|
56
|
+
const AVATAR_PX: Record<AvatarSize, number> = {
|
|
57
|
+
[AVATAR_SIZES.md]: 32,
|
|
58
|
+
[AVATAR_SIZES.lg]: 40,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const AVATAR_PRESENCE_PX: Record<AvatarSize, number> = {
|
|
62
|
+
[AVATAR_SIZES.md]: 12,
|
|
63
|
+
[AVATAR_SIZES.lg]: 14,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// =================================
|
|
67
|
+
// ====== Context ==================
|
|
68
|
+
// =================================
|
|
69
|
+
|
|
70
|
+
interface AvatarContextType {
|
|
71
|
+
size: AvatarSize
|
|
72
|
+
allImagesLoaded: boolean
|
|
73
|
+
setAllImagesLoaded: React.Dispatch<React.SetStateAction<boolean>>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const AvatarContext = createContext<AvatarContextType | null>(null)
|
|
77
|
+
|
|
78
|
+
function useAvatarContext() {
|
|
79
|
+
const context = useContext(AvatarContext)
|
|
80
|
+
if (!context) {
|
|
81
|
+
throw new Error('Avatar components must be used within Avatar.Root')
|
|
82
|
+
}
|
|
83
|
+
return context
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// =================================
|
|
87
|
+
// ====== AvatarRoot ===============
|
|
88
|
+
// =================================
|
|
89
|
+
|
|
90
|
+
interface AvatarRootProps {
|
|
91
|
+
children: React.ReactNode
|
|
92
|
+
size?: AvatarSize
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function AvatarRoot({ children, size = 'md' }: AvatarRootProps) {
|
|
96
|
+
const [allImagesLoaded, setAllImagesLoaded] = useState(false)
|
|
97
|
+
const styles = useStyles(size)
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<AvatarContext.Provider value={{ size, allImagesLoaded, setAllImagesLoaded }}>
|
|
101
|
+
<View style={styles.rootContainer}>{children}</View>
|
|
102
|
+
</AvatarContext.Provider>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
AvatarRoot.displayName = 'Avatar.Root'
|
|
107
|
+
|
|
108
|
+
// =================================
|
|
109
|
+
// ====== AvatarMask ===============
|
|
110
|
+
// =================================
|
|
111
|
+
|
|
112
|
+
type AvatarMaskProps = ViewProps
|
|
113
|
+
|
|
114
|
+
function AvatarMask({ children, ...props }: AvatarMaskProps) {
|
|
115
|
+
const styles = useStyles()
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<View style={styles.mask} {...props}>
|
|
119
|
+
{children}
|
|
120
|
+
</View>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
AvatarMask.displayName = 'Avatar.Mask'
|
|
125
|
+
|
|
126
|
+
// =================================
|
|
127
|
+
// ====== AvatarImage ============
|
|
128
|
+
// =================================
|
|
129
|
+
|
|
130
|
+
interface AvatarImageProps extends Omit<ImageProps, 'source'> {
|
|
131
|
+
sourceUri: string
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function AvatarImage({ sourceUri, ...props }: AvatarImageProps) {
|
|
135
|
+
const { size } = useAvatarContext()
|
|
136
|
+
|
|
137
|
+
return <Image source={{ uri: sourceUri }} loaderSize={AVATAR_PX[size]} {...props} />
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
AvatarImage.displayName = 'Avatar.Image'
|
|
141
|
+
|
|
142
|
+
interface AvatarGroupImageProps {
|
|
143
|
+
sourceUri: string
|
|
144
|
+
style?: ImageProps['wrapperStyle']
|
|
145
|
+
onLoad?: () => void
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function AvatarGroupImage({ sourceUri, style, onLoad }: AvatarGroupImageProps) {
|
|
149
|
+
return (
|
|
150
|
+
<Image
|
|
151
|
+
source={{ uri: sourceUri }}
|
|
152
|
+
loadingEnabled={false}
|
|
153
|
+
wrapperStyle={style}
|
|
154
|
+
onLoad={onLoad}
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// =================================
|
|
160
|
+
// ====== AvatarGroup ============
|
|
161
|
+
// =================================
|
|
162
|
+
|
|
163
|
+
interface AvatarGroupProps {
|
|
164
|
+
sourceUris: string[]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function AvatarGroup({ sourceUris }: AvatarGroupProps) {
|
|
168
|
+
const styles = useStyles()
|
|
169
|
+
const { setAllImagesLoaded } = useAvatarContext()
|
|
170
|
+
const [loadingStatus, setLoadingStatus] = useState({
|
|
171
|
+
0: false,
|
|
172
|
+
1: false,
|
|
173
|
+
2: false,
|
|
174
|
+
3: false,
|
|
175
|
+
})
|
|
176
|
+
const displayUris = sourceUris.slice(0, 4)
|
|
177
|
+
const hasDisplayUris = displayUris.length > 0
|
|
178
|
+
|
|
179
|
+
const handleImageLoaded = index => {
|
|
180
|
+
setLoadingStatus(prev => ({
|
|
181
|
+
...prev,
|
|
182
|
+
[index]: true,
|
|
183
|
+
}))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
const allImagesLoaded =
|
|
188
|
+
hasDisplayUris && displayUris.every((_, index) => loadingStatus[index] === true)
|
|
189
|
+
|
|
190
|
+
setAllImagesLoaded(allImagesLoaded)
|
|
191
|
+
}, [displayUris, hasDisplayUris, loadingStatus, setAllImagesLoaded])
|
|
192
|
+
|
|
193
|
+
if (!hasDisplayUris) {
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (displayUris.length === 1) {
|
|
198
|
+
return <AvatarGroupImage sourceUri={displayUris[0]} onLoad={() => handleImageLoaded(0)} />
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (displayUris.length === 2) {
|
|
202
|
+
return (
|
|
203
|
+
<View style={styles.groupRow}>
|
|
204
|
+
<AvatarGroupImage
|
|
205
|
+
sourceUri={displayUris[0]}
|
|
206
|
+
style={styles.halfWidthFullHeight}
|
|
207
|
+
onLoad={() => handleImageLoaded(0)}
|
|
208
|
+
/>
|
|
209
|
+
<AvatarGroupImage
|
|
210
|
+
sourceUri={displayUris[1]}
|
|
211
|
+
style={styles.halfWidthFullHeight}
|
|
212
|
+
onLoad={() => handleImageLoaded(1)}
|
|
213
|
+
/>
|
|
214
|
+
</View>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (displayUris.length === 3) {
|
|
219
|
+
return (
|
|
220
|
+
<View style={styles.groupColumn}>
|
|
221
|
+
<View style={styles.groupRow}>
|
|
222
|
+
<AvatarGroupImage
|
|
223
|
+
sourceUri={displayUris[0]}
|
|
224
|
+
style={styles.halfWidthFullHeight}
|
|
225
|
+
onLoad={() => handleImageLoaded(0)}
|
|
226
|
+
/>
|
|
227
|
+
<View style={styles.groupColumn}>
|
|
228
|
+
<AvatarGroupImage
|
|
229
|
+
sourceUri={displayUris[1]}
|
|
230
|
+
style={styles.fullWidthHalfHeight}
|
|
231
|
+
onLoad={() => handleImageLoaded(1)}
|
|
232
|
+
/>
|
|
233
|
+
<AvatarGroupImage
|
|
234
|
+
sourceUri={displayUris[2]}
|
|
235
|
+
style={styles.fullWidthHalfHeight}
|
|
236
|
+
onLoad={() => handleImageLoaded(2)}
|
|
237
|
+
/>
|
|
238
|
+
</View>
|
|
239
|
+
</View>
|
|
240
|
+
</View>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<View style={styles.groupColumn}>
|
|
246
|
+
<View style={styles.groupRow}>
|
|
247
|
+
<AvatarGroupImage
|
|
248
|
+
sourceUri={displayUris[0]}
|
|
249
|
+
style={styles.halfWidthFullHeight}
|
|
250
|
+
onLoad={() => handleImageLoaded(0)}
|
|
251
|
+
/>
|
|
252
|
+
<AvatarGroupImage
|
|
253
|
+
sourceUri={displayUris[1]}
|
|
254
|
+
style={styles.halfWidthFullHeight}
|
|
255
|
+
onLoad={() => handleImageLoaded(1)}
|
|
256
|
+
/>
|
|
257
|
+
</View>
|
|
258
|
+
<View style={styles.groupRow}>
|
|
259
|
+
<AvatarGroupImage
|
|
260
|
+
sourceUri={displayUris[2]}
|
|
261
|
+
style={styles.halfWidthFullHeight}
|
|
262
|
+
onLoad={() => handleImageLoaded(2)}
|
|
263
|
+
/>
|
|
264
|
+
<AvatarGroupImage
|
|
265
|
+
sourceUri={displayUris[3]}
|
|
266
|
+
style={styles.halfWidthFullHeight}
|
|
267
|
+
onLoad={() => handleImageLoaded(3)}
|
|
268
|
+
/>
|
|
269
|
+
</View>
|
|
270
|
+
</View>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
AvatarGroup.displayName = 'Avatar.Group'
|
|
275
|
+
|
|
276
|
+
// =================================
|
|
277
|
+
// ====== AvatarGroupLoader =========
|
|
278
|
+
// =================================
|
|
279
|
+
|
|
280
|
+
function AvatarGroupLoader() {
|
|
281
|
+
const { size, allImagesLoaded } = useAvatarContext()
|
|
282
|
+
const styles = useStyles(size)
|
|
283
|
+
|
|
284
|
+
if (allImagesLoaded) return null
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<View style={styles.groupLoader}>
|
|
288
|
+
<Spinner size={AVATAR_PX[size]} />
|
|
289
|
+
</View>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
AvatarGroupLoader.displayName = 'Avatar.GroupLoader'
|
|
294
|
+
|
|
295
|
+
// =================================
|
|
296
|
+
// ====== AvatarPresence =========
|
|
297
|
+
// =================================
|
|
298
|
+
|
|
299
|
+
interface AvatarPresenceProps extends ViewProps {
|
|
300
|
+
presence: AvatarPresenceType
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function AvatarPresence({ presence, ...props }: AvatarPresenceProps) {
|
|
304
|
+
const { size } = useAvatarContext()
|
|
305
|
+
const styles = useStyles(size, presence)
|
|
306
|
+
|
|
307
|
+
return <View style={styles.presence} {...props} />
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
AvatarPresence.displayName = 'Avatar.Presence'
|
|
311
|
+
|
|
312
|
+
// =================================
|
|
313
|
+
// ====== Styles ===================
|
|
314
|
+
// =================================
|
|
315
|
+
|
|
316
|
+
const useStyles = (size: AvatarSize = 'md', presence: AvatarPresenceType = 'offline') => {
|
|
317
|
+
const { colors } = useTheme()
|
|
318
|
+
const PRESENCE_COLOR = {
|
|
319
|
+
online: colors.fillColorInteractionOnlineDefault,
|
|
320
|
+
offline: colors.iconColorDefaultDisabled,
|
|
321
|
+
}
|
|
322
|
+
const presenceDiameter = AVATAR_PRESENCE_PX[size]
|
|
323
|
+
const avatarDiameter = AVATAR_PX[size]
|
|
324
|
+
const groupGap = 1
|
|
325
|
+
|
|
326
|
+
return StyleSheet.create({
|
|
327
|
+
rootContainer: {
|
|
328
|
+
height: avatarDiameter,
|
|
329
|
+
width: avatarDiameter,
|
|
330
|
+
},
|
|
331
|
+
mask: {
|
|
332
|
+
borderRadius: avatarDiameter,
|
|
333
|
+
overflow: 'hidden',
|
|
334
|
+
width: '100%',
|
|
335
|
+
height: '100%',
|
|
336
|
+
},
|
|
337
|
+
presence: {
|
|
338
|
+
height: presenceDiameter,
|
|
339
|
+
width: presenceDiameter,
|
|
340
|
+
backgroundColor: PRESENCE_COLOR[presence],
|
|
341
|
+
borderColor: colors.fillColorNeutral100Inverted,
|
|
342
|
+
borderWidth: 2,
|
|
343
|
+
borderRadius: presenceDiameter,
|
|
344
|
+
position: 'absolute',
|
|
345
|
+
bottom: -1,
|
|
346
|
+
right: -1,
|
|
347
|
+
},
|
|
348
|
+
groupLoader: {
|
|
349
|
+
position: 'absolute',
|
|
350
|
+
top: 0,
|
|
351
|
+
left: 0,
|
|
352
|
+
borderRadius: avatarDiameter,
|
|
353
|
+
width: avatarDiameter,
|
|
354
|
+
height: avatarDiameter,
|
|
355
|
+
},
|
|
356
|
+
groupColumn: {
|
|
357
|
+
flex: 1,
|
|
358
|
+
gap: groupGap,
|
|
359
|
+
},
|
|
360
|
+
groupRow: {
|
|
361
|
+
flexDirection: 'row',
|
|
362
|
+
flex: 1,
|
|
363
|
+
gap: groupGap,
|
|
364
|
+
},
|
|
365
|
+
halfWidthFullHeight: {
|
|
366
|
+
width: '50%',
|
|
367
|
+
height: '100%',
|
|
368
|
+
},
|
|
369
|
+
fullWidthHalfHeight: {
|
|
370
|
+
width: '100%',
|
|
371
|
+
height: '50%',
|
|
372
|
+
},
|
|
373
|
+
})
|
|
374
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { JSONAPIResponse } from '@planningcenter/chat-core'
|
|
1
2
|
import { QueryClient, QueryClientProvider, QueryKey } from '@tanstack/react-query'
|
|
2
3
|
import React, { useEffect } from 'react'
|
|
3
4
|
import { ViewProps } from 'react-native'
|
|
4
5
|
import { OAuthToken } from '../types'
|
|
6
|
+
import apiRequest from '../utils/api'
|
|
5
7
|
import { ENV, session } from '../utils/session'
|
|
6
8
|
|
|
7
9
|
let handleTokenExpired: () => void
|
|
@@ -13,13 +15,8 @@ const defaultQueryFn = ({ queryKey }: { queryKey: QueryKey }) => {
|
|
|
13
15
|
|
|
14
16
|
const url = `${session.baseUrl}${queryKey[0]}`
|
|
15
17
|
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
Authorization: `Bearer ${session.token?.access_token}`,
|
|
19
|
-
},
|
|
20
|
-
})
|
|
21
|
-
.then(validateResponse)
|
|
22
|
-
.then(response => response.json())
|
|
18
|
+
return apiRequest<JSONAPIResponse>(url)
|
|
19
|
+
.then(r => r.json)
|
|
23
20
|
.catch(error => {
|
|
24
21
|
if (error.message === 'Token expired') {
|
|
25
22
|
handleTokenExpired()
|
|
@@ -52,13 +49,3 @@ export function ApiProvider({
|
|
|
52
49
|
|
|
53
50
|
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
54
51
|
}
|
|
55
|
-
|
|
56
|
-
const validateResponse = (response: Response) => {
|
|
57
|
-
const isExpired = response.status === 401
|
|
58
|
-
|
|
59
|
-
if (isExpired) {
|
|
60
|
-
throw new Error('Token expired')
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return response
|
|
64
|
-
}
|
|
@@ -3,7 +3,7 @@ import React, { createContext, useMemo } from 'react'
|
|
|
3
3
|
import { ColorSchemeName, useColorScheme } from 'react-native'
|
|
4
4
|
import { DeepPartial, OAuthToken } from '../types'
|
|
5
5
|
import { defaultTheme, DefaultTheme } from '../utils/theme'
|
|
6
|
-
import {
|
|
6
|
+
import { aliasTokensColorMap } from '../vendor/tapestry/alias_tokens_color_map'
|
|
7
7
|
import { ApiProvider } from './api_provider'
|
|
8
8
|
|
|
9
9
|
type ContextValue = {
|
|
@@ -51,7 +51,7 @@ export const useCreateChatTheme = ({
|
|
|
51
51
|
...merge({}, defaultTheme(colorScheme), customTheme),
|
|
52
52
|
colors: {
|
|
53
53
|
...merge({}, defaultTheme(colorScheme).colors, customTheme?.colors),
|
|
54
|
-
...
|
|
54
|
+
...aliasTokensColorMap[colorScheme],
|
|
55
55
|
},
|
|
56
56
|
}
|
|
57
57
|
}, [colorScheme, customTheme])
|
package/src/index.tsx
CHANGED
package/src/screens/display.tsx
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { ScrollView, StyleSheet, View } from 'react-native'
|
|
3
3
|
import { useTheme } from '../hooks'
|
|
4
|
-
import { Image, Spinner, Text } from '../components/display'
|
|
4
|
+
import { Avatar, AvatarGroup, Heading, Image, Spinner, Text } from '../components/display'
|
|
5
|
+
import { space } from '../utils/space'
|
|
6
|
+
|
|
7
|
+
const URL = {
|
|
8
|
+
image: 'https://picsum.photos/seed/picsum/200',
|
|
9
|
+
broken: 'https://broken.url',
|
|
10
|
+
avatar: 'https://i.pravatar.cc/200?img=22',
|
|
11
|
+
avatar_fallback: 'https://avatars.planningcenteronline.com/uploads/initials/PR.png',
|
|
12
|
+
two_avatars: ['https://i.pravatar.cc/200?img=22', 'https://i.pravatar.cc/200?img=23'],
|
|
13
|
+
three_avatars: [
|
|
14
|
+
'https://i.pravatar.cc/200?img=22',
|
|
15
|
+
'https://i.pravatar.cc/200?img=23',
|
|
16
|
+
'https://i.pravatar.cc/200?img=24',
|
|
17
|
+
],
|
|
18
|
+
four_avatars: [
|
|
19
|
+
'https://i.pravatar.cc/200?img=30',
|
|
20
|
+
'https://i.pravatar.cc/200?img=29',
|
|
21
|
+
'https://i.pravatar.cc/200?img=28',
|
|
22
|
+
'https://i.pravatar.cc/200?img=27',
|
|
23
|
+
],
|
|
24
|
+
}
|
|
5
25
|
|
|
6
26
|
export function DisplayScreen() {
|
|
7
27
|
const styles = useStyles()
|
|
@@ -13,13 +33,26 @@ export function DisplayScreen() {
|
|
|
13
33
|
<Spinner size={24} />
|
|
14
34
|
</View>
|
|
15
35
|
<View style={styles.row}>
|
|
16
|
-
<Image source={{ uri:
|
|
17
|
-
<Image
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
/>
|
|
36
|
+
<Image source={{ uri: URL.broken }} style={styles.image} />
|
|
37
|
+
<Image source={{ uri: URL.image }} style={styles.image} />
|
|
38
|
+
</View>
|
|
39
|
+
<View style={styles.row}>
|
|
40
|
+
<Avatar sourceUri={URL.broken} />
|
|
41
|
+
<Avatar size="md" sourceUri={URL.avatar_fallback} />
|
|
42
|
+
<Avatar sourceUri={URL.avatar} />
|
|
43
|
+
</View>
|
|
44
|
+
<View style={styles.row}>
|
|
45
|
+
<Avatar presence="offline" sourceUri={URL.broken} />
|
|
46
|
+
<Avatar presence="online" size="md" sourceUri={URL.avatar_fallback} />
|
|
47
|
+
<Avatar presence="offline" sourceUri={URL.avatar} />
|
|
48
|
+
</View>
|
|
49
|
+
<View style={styles.row}>
|
|
50
|
+
<AvatarGroup sourceUris={[URL.broken]} />
|
|
51
|
+
<AvatarGroup sourceUris={[URL.broken, URL.broken, ...URL.two_avatars]} />
|
|
52
|
+
<AvatarGroup sourceUris={[URL.avatar]} />
|
|
53
|
+
<AvatarGroup sourceUris={URL.two_avatars} />
|
|
54
|
+
<AvatarGroup sourceUris={URL.three_avatars} />
|
|
55
|
+
<AvatarGroup sourceUris={URL.four_avatars} />
|
|
23
56
|
</View>
|
|
24
57
|
<View style={styles.row}>
|
|
25
58
|
<Text>Plain text</Text>
|
|
@@ -27,6 +60,12 @@ export function DisplayScreen() {
|
|
|
27
60
|
<Text variant="tertiary">Tertiary</Text>
|
|
28
61
|
<Text variant="footnote">Footnote</Text>
|
|
29
62
|
</View>
|
|
63
|
+
<View style={styles.row}>
|
|
64
|
+
<Heading>Heading 1</Heading>
|
|
65
|
+
<Heading variant="h2">Heading 2</Heading>
|
|
66
|
+
<Heading variant="h3">Heading 3</Heading>
|
|
67
|
+
<Heading variant="h4">Heading 4</Heading>
|
|
68
|
+
</View>
|
|
30
69
|
</View>
|
|
31
70
|
</ScrollView>
|
|
32
71
|
)
|
|
@@ -36,19 +75,19 @@ const useStyles = () => {
|
|
|
36
75
|
const { colors } = useTheme()
|
|
37
76
|
|
|
38
77
|
return StyleSheet.create({
|
|
39
|
-
scrollView: { flex: 1, backgroundColor: colors.
|
|
40
|
-
container: { gap:
|
|
78
|
+
scrollView: { flex: 1, backgroundColor: colors.fillColorNeutral100Inverted },
|
|
79
|
+
container: { gap: space(2), padding: space(3) },
|
|
41
80
|
listItem: { color: colors.fillColorNeutral020 },
|
|
42
81
|
row: {
|
|
43
|
-
gap:
|
|
82
|
+
gap: space(2),
|
|
44
83
|
flexDirection: 'row',
|
|
45
84
|
alignItems: 'center',
|
|
46
85
|
justifyContent: 'center',
|
|
47
86
|
flexWrap: 'wrap',
|
|
48
87
|
},
|
|
49
|
-
column: { gap:
|
|
88
|
+
column: { gap: space(4) },
|
|
50
89
|
spinnerContainer: {
|
|
51
|
-
height:
|
|
90
|
+
height: space(2.5),
|
|
52
91
|
},
|
|
53
92
|
image: {
|
|
54
93
|
width: 100,
|
package/src/utils/api.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import DeviceInfo from 'react-native-device-info'
|
|
2
|
+
import { session } from './session'
|
|
3
|
+
|
|
4
|
+
const brand = DeviceInfo.getBrand()
|
|
5
|
+
const model = DeviceInfo.getModel()
|
|
6
|
+
const systemName = DeviceInfo.getSystemName()
|
|
7
|
+
const systemVersion = DeviceInfo.getSystemVersion()
|
|
8
|
+
const readableVersion = DeviceInfo.getReadableVersion()
|
|
9
|
+
const appName = DeviceInfo.getApplicationName()
|
|
10
|
+
|
|
11
|
+
export default function apiRequest<T = unknown>(
|
|
12
|
+
url: string,
|
|
13
|
+
{ method = 'GET', data = null } = {}
|
|
14
|
+
): Promise<{ json: T; ok: boolean; response: Response }> {
|
|
15
|
+
const options: RequestInit = {
|
|
16
|
+
headers: {
|
|
17
|
+
Accept: 'application/vnd.api+json',
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
'User-Agent': `${appName}/${readableVersion} (${brand}, ${model}, ${systemName}, ${systemVersion})`,
|
|
20
|
+
Authorization: `Bearer ${session.token?.access_token}`,
|
|
21
|
+
},
|
|
22
|
+
method,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (data && method !== 'GET') {
|
|
26
|
+
options.body = JSON.stringify(data)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return fetch(url, options)
|
|
30
|
+
.then(validateResponse)
|
|
31
|
+
.then(response =>
|
|
32
|
+
response
|
|
33
|
+
.json()
|
|
34
|
+
.then(json => ({ json: json as T, ok: response.ok, response }))
|
|
35
|
+
.catch(() => ({ json: null as T, ok: response.ok, response }))
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const validateResponse = (response: Response) => {
|
|
40
|
+
const isExpired = response.status === 401
|
|
41
|
+
|
|
42
|
+
if (isExpired) {
|
|
43
|
+
throw new Error('Token expired')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return response
|
|
47
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { tokens } from '../vendor/tapestry/tokens'
|
|
2
|
+
|
|
3
|
+
export type SpacingValues =
|
|
4
|
+
| 0.25
|
|
5
|
+
| 0.5
|
|
6
|
+
| 1
|
|
7
|
+
| 1.5
|
|
8
|
+
| 2
|
|
9
|
+
| 2.5
|
|
10
|
+
| 3
|
|
11
|
+
| 3.5
|
|
12
|
+
| 4
|
|
13
|
+
| 4.5
|
|
14
|
+
| 5
|
|
15
|
+
| 5.5
|
|
16
|
+
| 6
|
|
17
|
+
| 6.5
|
|
18
|
+
| 7
|
|
19
|
+
| 7.5
|
|
20
|
+
|
|
21
|
+
export function space(value: SpacingValues): number {
|
|
22
|
+
if (value === 0.25) return tokens.spacingFourth
|
|
23
|
+
if (value === 0.5) return tokens.spacingHalf
|
|
24
|
+
if (value < 1 || value > 7.5) return handleInvalidSpace(value)
|
|
25
|
+
|
|
26
|
+
// Reject fractional values that are not 0 or 0.5
|
|
27
|
+
const wholeValue = Math.floor(value)
|
|
28
|
+
const fractionalValue = value % 1
|
|
29
|
+
if (fractionalValue !== 0 && fractionalValue !== 0.5) return handleInvalidSpace(value)
|
|
30
|
+
|
|
31
|
+
// Deliver a whole value or add a half spacing token to it
|
|
32
|
+
const remainderValue = fractionalValue === 0.5 ? tokens.spacingHalf : 0
|
|
33
|
+
return tokens[`spacing${wholeValue}`] + remainderValue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleInvalidSpace(value: number) {
|
|
37
|
+
console.warn(`Invalid space value: ${value} — Must be a whole or half number between 1–7.`)
|
|
38
|
+
return 0
|
|
39
|
+
}
|
package/src/utils/theme.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { TextStyle, ViewStyle, ColorSchemeName } from 'react-native'
|
|
2
2
|
import { tokens } from '../vendor/tapestry/tokens'
|
|
3
|
-
import {
|
|
3
|
+
import { aliasTokensColorMap } from '../vendor/tapestry/alias_tokens_color_map'
|
|
4
4
|
|
|
5
5
|
export interface ChatTheme extends DefaultTheme {
|
|
6
6
|
colors: DefaultTheme['colors'] &
|
|
7
|
-
(typeof
|
|
7
|
+
(typeof aliasTokensColorMap.light | typeof aliasTokensColorMap.dark)
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/** =============================================
|
|
@@ -37,7 +37,7 @@ export const defaultTheme = (colorScheme: ColorSchemeName): DefaultTheme => {
|
|
|
37
37
|
temporaryProductBadge: {
|
|
38
38
|
container: {
|
|
39
39
|
paddingHorizontal: tokens.spacing1,
|
|
40
|
-
backgroundColor:
|
|
40
|
+
backgroundColor: aliasTokensColorMap[scheme].fillColorNeutral070,
|
|
41
41
|
borderWidth: tokens.borderSizeDefault,
|
|
42
42
|
},
|
|
43
43
|
text: {
|