tangled
alpha
login
or
join now
stream.place
/
streamplace
Live video on the AT Protocol
74
fork
atom
overview
issues
1
pulls
pipelines
redo live dashboard UI w color and style updates
Natalie B.
5 months ago
a638af39
6247d501
+235
-171
9 changed files
expand all
collapse all
unified
split
js
app
components
live-dashboard
bento-grid.tsx
chat-panel.tsx
header.tsx
livestream-panel
index.tsx
stream-monitor.tsx
ui
button-selector.tsx
components
src
components
chat
chat-message.tsx
chat.tsx
lib
theme
tokens.ts
+2
-8
js/app/components/live-dashboard/bento-grid.tsx
···
22
const isWeb = Platform.OS === "web";
23
24
return (
25
-
<View style={[flex.values[1], gap.all[4], p[4]]}>
26
-
<View
27
-
style={[
28
-
layout.flex.column,
29
-
bg.gray[900],
30
-
{ minWidth: isWeb ? 400 : "100%" },
31
-
]}
32
-
>
33
<Header isLive={isLive} />
34
</View>
35
<View style={[flex.values[1], layout.flex.row, gap.all[4]]}>
···
22
const isWeb = Platform.OS === "web";
23
24
return (
25
+
<View style={[flex.values[1], gap.all[4], p[4], bg.black]}>
26
+
<View style={[layout.flex.column, { minWidth: isWeb ? 400 : "100%" }]}>
0
0
0
0
0
0
27
<Header isLive={isLive} />
28
</View>
29
<View style={[flex.values[1], layout.flex.row, gap.all[4]]}>
+26
-22
js/app/components/live-dashboard/chat-panel.tsx
···
9
import { useState } from "react";
10
import { Text, View } from "react-native";
11
import { useLiveUser } from "../../hooks/useLiveUser";
12
-
13
-
const { flex, bg, r, borders, p, text, layout } = zero;
14
15
export default function ChatPanel() {
16
// Get real data from hooks
···
38
<View
39
style={[
40
flex.values[1],
41
-
bg.gray[800],
42
-
r[3],
43
borders.width.thin,
44
-
borders.color.gray[700],
45
layout.flex.column,
0
0
46
]}
47
>
48
<View
···
50
layout.flex.row,
51
layout.flex.spaceBetween,
52
layout.flex.alignCenter,
53
-
p[4],
54
borders.bottom.width.thin,
55
-
borders.bottom.color.gray[700],
0
56
]}
57
>
58
<Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
59
Live Chat
60
</Text>
61
-
<View style={[layout.flex.row, layout.flex.alignCenter, { gap: 8 }]}>
62
<View
63
style={[
64
{ width: 6, height: 6, borderRadius: 3 },
65
isLive && isConnected ? bg.green[500] : bg.gray[500],
66
]}
67
/>
68
-
<Text style={[text.gray[400], { fontSize: 12 }]}>
69
{messagesPerMinute} msg/min
70
</Text>
71
</View>
72
</View>
73
-
<View style={[flex.values[1], p[2]]}>
74
-
<Chat canModerate={canModerate} shownMessages={50} />
75
-
<ChatBox
76
-
emojiData={emojiData}
77
-
chatBoxStyle={[
78
-
bg.gray[700],
79
-
borders.width.thin,
80
-
borders.color.gray[600],
81
-
r[2],
82
-
p[3],
83
-
!isConnected && { opacity: 0.6 },
84
-
]}
85
-
/>
0
0
0
0
86
</View>
87
</View>
88
);
···
9
import { useState } from "react";
10
import { Text, View } from "react-native";
11
import { useLiveUser } from "../../hooks/useLiveUser";
12
+
const { flex, bg, r, borders, p, px, py, text, layout } = zero;
0
13
14
export default function ChatPanel() {
15
// Get real data from hooks
···
37
<View
38
style={[
39
flex.values[1],
40
+
bg.neutral[900],
0
41
borders.width.thin,
42
+
borders.color.neutral[700],
43
layout.flex.column,
44
+
r.lg,
45
+
{ minWidth: 300, maxWidth: 600, flexShrink: 0 },
46
]}
47
>
48
<View
···
50
layout.flex.row,
51
layout.flex.spaceBetween,
52
layout.flex.alignCenter,
0
53
borders.bottom.width.thin,
54
+
borders.bottom.color.neutral[700],
55
+
p[4],
56
]}
57
>
58
<Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
59
Live Chat
60
</Text>
61
+
<View style={[layout.flex.row, layout.flex.alignCenter]}>
62
<View
63
style={[
64
{ width: 6, height: 6, borderRadius: 3 },
65
isLive && isConnected ? bg.green[500] : bg.gray[500],
66
]}
67
/>
68
+
<Text style={[text.gray[400], { fontSize: 12, marginLeft: 8 }]}>
69
{messagesPerMinute} msg/min
70
</Text>
71
</View>
72
</View>
73
+
<View style={[flex.values[1], px[2], { minHeight: 0 }]}>
74
+
<View style={[flex.values[1], { minHeight: 0 }]}>
75
+
<Chat canModerate={canModerate} shownMessages={50} />
76
+
</View>
77
+
<View style={[{ flexShrink: 0 }]}>
78
+
<ChatBox
79
+
emojiData={emojiData}
80
+
chatBoxStyle={[
81
+
bg.gray[700],
82
+
borders.width.thin,
83
+
borders.color.gray[600],
84
+
r.md,
85
+
p[3],
86
+
!isConnected && { opacity: 0.6 },
87
+
]}
88
+
/>
89
+
</View>
90
</View>
91
</View>
92
);
+5
-5
js/app/components/live-dashboard/header.tsx
···
10
import { useLiveUser } from "../../hooks/useLiveUser";
11
import { useSegmentTiming } from "../../hooks/useSegmentTiming";
12
13
-
const { flex, bg, r, borders, p, px, py, text, layout, gap } = zero;
14
15
interface MetricItemProps {
16
icon: any;
···
151
return (
152
<View
153
style={[
154
-
bg.gray[800],
155
-
borders.bottom.width.thin,
156
-
borders.bottom.color.gray[700],
157
px[4],
158
py[3],
0
159
layout.flex.row,
160
-
layout.flex.alignCenter,
161
layout.flex.spaceBetween,
0
0
0
162
]}
163
>
164
{/* Left side - Stream title and status */}
···
10
import { useLiveUser } from "../../hooks/useLiveUser";
11
import { useSegmentTiming } from "../../hooks/useSegmentTiming";
12
13
+
const { bg, r, borders, px, py, text, layout, gap } = zero;
14
15
interface MetricItemProps {
16
icon: any;
···
151
return (
152
<View
153
style={[
0
0
0
154
px[4],
155
py[3],
156
+
r.lg,
157
layout.flex.row,
0
158
layout.flex.spaceBetween,
159
+
bg.neutral[900],
160
+
borders.width.thin,
161
+
borders.color.neutral[700],
162
]}
163
>
164
{/* Left side - Stream title and status */}
+58
-52
js/app/components/live-dashboard/livestream-panel/index.tsx
···
1
-
import { useLivestream, useToast, zero } from "@streamplace/components";
0
0
0
0
0
0
2
import ThumbnailSelector from "components/thumbnail-selector";
3
import ButtonSelector from "components/ui/button-selector";
4
import {
···
12
import { useEffect, useState } from "react";
13
import {
14
Platform,
15
-
Pressable,
16
ScrollView,
17
Text,
18
-
TextInput,
19
useWindowDimensions,
20
View,
21
} from "react-native";
22
import { useAppDispatch, useAppSelector } from "store/hooks";
23
24
-
const { flex, p, px, py, mt, gap, layout, bg, borders, text, r, w } = zero;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
25
const isWeb = Platform.OS === "web";
26
27
interface LivestreamPanelProps {
···
161
<View
162
style={[
163
flex.values[1],
164
-
bg.gray[800],
165
-
r[3],
166
borders.width.thin,
167
-
borders.color.gray[700],
168
layout.flex.column,
169
]}
170
>
···
172
style={[
173
layout.flex.row,
174
layout.flex.spaceBetween,
175
-
layout.flex.alignCenter,
176
-
p[4],
0
0
177
borders.bottom.width.thin,
178
-
borders.bottom.color.gray[700],
179
]}
180
>
181
<Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
182
-
Live Chat
183
</Text>
184
<ButtonSelector
185
values={[
186
{ label: "Create", value: "create" },
187
{ label: "Edit", value: "edit" },
188
]}
0
189
selectedValue={mode}
190
setSelectedValue={setSelectedMode}
191
disabledValues={livestream ? [] : ["edit"]}
192
/>
193
</View>
194
{mode === "edit" && (
195
-
<Text
196
-
style={[px[4], text.white, { fontSize: 20, fontWeight: "bold" }]}
197
-
>
198
Change your Current Livestream Title
199
</Text>
200
)}
201
{mode === "edit" && noLivestream ? (
202
<View style={[layout.flex.center, p[4]]}>
203
-
<Text style={[text.gray[400], { fontSize: 16 }]}>
204
No active livestream to edit. Start a livestream first!
205
</Text>
206
</View>
···
208
<View
209
style={[
210
{ flexDirection: useTwoColumns ? "row" : "column" },
211
-
useTwoColumns ? gap.row[12] : gap.column[4],
212
w.percent[100],
213
{ alignSelf: "center" },
214
p[4],
···
219
]}
220
>
221
{/* Left column: labels and fields */}
222
-
<View
223
-
style={[
224
-
flex.values[2],
225
-
{ minWidth: 0 },
226
-
gap.column[3],
227
-
w.percent[100],
228
-
]}
229
-
>
230
<View
231
style={[
232
layout.flex.row,
···
236
>
237
<Text
238
style={[
239
-
text.gray[300],
240
{ minWidth: 100, textAlign: "left", paddingBottom: 8 },
241
]}
242
>
···
260
>
261
<Text
262
style={[
263
-
text.gray[300],
264
{ minWidth: 100, textAlign: "left", paddingBottom: 8 },
265
]}
266
>
267
Title
268
</Text>
269
<View style={[flex.values[1]]}>
270
-
<TextInput
271
value={title}
272
onChangeText={setTitle}
273
style={[
274
p[2],
275
r[1],
276
-
bg.gray[900],
277
text.white,
278
w.percent[100],
279
{ minHeight: 100, fontSize: 16 },
···
288
style={[
289
layout.flex.row,
290
layout.flex.alignCenter,
0
291
w.percent[100],
292
-
{ marginTop: -16 },
293
]}
294
>
295
<View style={[flex.values[1]]}>
296
-
<Text style={[text.gray[400], { fontSize: 12 }]}>
297
Updating will not send out notifications to viewers or
298
create a new social media post.
299
</Text>
···
310
gap.column[4],
311
{ alignItems: "center" },
312
{ justifyContent: "flex-start" },
313
-
{ marginTop: 12 },
314
]}
315
>
316
<Text
···
328
</View>
329
</View>
330
)}
331
-
<View
0
332
style={[
0
0
0
333
w.percent[100],
334
{ alignItems: "center" },
335
-
mode === "edit" ? { marginTop: -16 } : mt[4],
336
]}
0
337
>
338
-
<Pressable
339
-
disabled={disabled}
340
-
style={[
341
-
bg.primary[500],
342
-
r[1],
343
-
px[4],
344
-
py[2],
345
-
w.percent[100],
346
-
{ alignItems: "center" },
347
-
{ opacity: disabled ? 0.5 : 1 },
348
-
]}
349
-
onPress={handleSubmit}
350
>
351
-
<Text
352
-
style={[text.white, { fontSize: 16, fontWeight: "bold" }]}
353
-
>
354
-
{buttonText}
355
-
</Text>
356
-
</Pressable>
357
-
</View>
358
</View>
359
)}
360
</View>
···
1
+
import {
2
+
Button,
3
+
Textarea,
4
+
useLivestream,
5
+
useToast,
6
+
zero,
7
+
} from "@streamplace/components";
8
import ThumbnailSelector from "components/thumbnail-selector";
9
import ButtonSelector from "components/ui/button-selector";
10
import {
···
18
import { useEffect, useState } from "react";
19
import {
20
Platform,
0
21
ScrollView,
22
Text,
0
23
useWindowDimensions,
24
View,
25
} from "react-native";
26
import { useAppDispatch, useAppSelector } from "store/hooks";
27
28
+
const {
29
+
flex,
30
+
p,
31
+
px,
32
+
py,
33
+
h,
34
+
gap,
35
+
layout,
36
+
bg,
37
+
borders,
38
+
mt,
39
+
pt,
40
+
pb,
41
+
text,
42
+
r,
43
+
w,
44
+
typography,
45
+
} = zero;
46
const isWeb = Platform.OS === "web";
47
48
interface LivestreamPanelProps {
···
182
<View
183
style={[
184
flex.values[1],
185
+
bg.neutral[900],
186
+
r.lg,
187
borders.width.thin,
188
+
borders.color.neutral[700],
189
layout.flex.column,
190
]}
191
>
···
193
style={[
194
layout.flex.row,
195
layout.flex.spaceBetween,
196
+
layout.flex.align.center,
197
+
px[4],
198
+
pt[4],
199
+
pb[4],
200
borders.bottom.width.thin,
201
+
borders.bottom.color.neutral[700],
202
]}
203
>
204
<Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
205
+
Stream settings
206
</Text>
207
<ButtonSelector
208
values={[
209
{ label: "Create", value: "create" },
210
{ label: "Edit", value: "edit" },
211
]}
212
+
style={[{ marginVertical: -2 }]}
213
selectedValue={mode}
214
setSelectedValue={setSelectedMode}
215
disabledValues={livestream ? [] : ["edit"]}
216
/>
217
</View>
218
{mode === "edit" && (
219
+
<Text style={[p[4], text.white, typography.universal["xl"]]}>
0
0
220
Change your Current Livestream Title
221
</Text>
222
)}
223
{mode === "edit" && noLivestream ? (
224
<View style={[layout.flex.center, p[4]]}>
225
+
<Text style={[text.neutral[400], { fontSize: 16 }]}>
226
No active livestream to edit. Start a livestream first!
227
</Text>
228
</View>
···
230
<View
231
style={[
232
{ flexDirection: useTwoColumns ? "row" : "column" },
233
+
useTwoColumns ? gap.row[12] : gap.all[8],
234
w.percent[100],
235
{ alignSelf: "center" },
236
p[4],
···
241
]}
242
>
243
{/* Left column: labels and fields */}
244
+
<View style={[{ minWidth: 0 }, gap.column[3], w.percent[100]]}>
0
0
0
0
0
0
0
245
<View
246
style={[
247
layout.flex.row,
···
251
>
252
<Text
253
style={[
254
+
text.neutral[300],
255
{ minWidth: 100, textAlign: "left", paddingBottom: 8 },
256
]}
257
>
···
275
>
276
<Text
277
style={[
278
+
text.neutral[300],
279
{ minWidth: 100, textAlign: "left", paddingBottom: 8 },
280
]}
281
>
282
Title
283
</Text>
284
<View style={[flex.values[1]]}>
285
+
<Textarea
286
value={title}
287
onChangeText={setTitle}
288
style={[
289
p[2],
290
r[1],
291
+
bg.neutral[800],
292
text.white,
293
w.percent[100],
294
{ minHeight: 100, fontSize: 16 },
···
303
style={[
304
layout.flex.row,
305
layout.flex.alignCenter,
306
+
mt[2],
307
w.percent[100],
0
308
]}
309
>
310
<View style={[flex.values[1]]}>
311
+
<Text style={[text.neutral[400], { fontSize: 12 }]}>
312
Updating will not send out notifications to viewers or
313
create a new social media post.
314
</Text>
···
325
gap.column[4],
326
{ alignItems: "center" },
327
{ justifyContent: "flex-start" },
328
+
h.percent[100],
329
]}
330
>
331
<Text
···
343
</View>
344
</View>
345
)}
346
+
<Button
347
+
disabled={disabled}
348
style={[
349
+
bg.primary[500],
350
+
r[1],
351
+
py[2],
352
w.percent[100],
353
{ alignItems: "center" },
354
+
{ opacity: disabled ? 0.5 : 1 },
355
]}
356
+
onPress={handleSubmit}
357
>
358
+
<Text
359
+
style={[text.white, { fontSize: 16, fontWeight: "bold" }]}
0
0
0
0
0
0
0
0
0
0
360
>
361
+
{buttonText}
362
+
</Text>
363
+
</Button>
0
0
0
0
364
</View>
365
)}
366
</View>
+57
-31
js/app/components/live-dashboard/stream-monitor.tsx
···
12
import { useSegmentTiming } from "../../hooks/useSegmentTiming";
13
import StreamScreen from "./live-selector";
14
15
-
const { flex, bg, r, borders, layout, p, text, w, h } = zero;
16
17
interface StreamMonitorProps {
18
userProfile?: any;
···
89
style={[
90
flex.values[2],
91
bg.gray[800],
92
-
r[3],
0
93
borders.width.thin,
94
-
borders.color.gray[700],
95
layout.flex.column,
96
]}
97
>
98
-
<View style={[flex.values[1], layout.flex.center, bg.gray[900]]}>
99
{isLive && userProfile ? (
100
isStreamVisible ? (
101
<Player src={userProfile.did} name={userProfile.handle} />
···
141
layout.flex.spaceBetween,
142
layout.flex.alignCenter,
143
p[4],
144
-
borders.bottom.width.thin,
145
-
borders.bottom.color.gray[700],
146
]}
147
>
148
-
<View style={[layout.flex.row, layout.flex.alignCenter, { gap: 12 }]}>
0
0
0
0
0
0
0
0
149
<Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
150
{ls?.record.title || "Stream Title"}
151
</Text>
152
-
{isLive && userProfile && (
153
-
<TouchableOpacity
154
-
onPress={() => setIsStreamVisible(!isStreamVisible)}
155
-
style={{
156
-
padding: 4,
157
-
borderRadius: 4,
158
-
backgroundColor: "rgba(255, 255, 255, 0.1)",
159
-
}}
160
-
>
161
-
{isStreamVisible ? (
162
-
<EyeOff size={16} color="#9ca3af" />
163
-
) : (
164
-
<Eye size={16} color="#9ca3af" />
165
-
)}
166
-
</TouchableOpacity>
167
-
)}
168
-
<View style={[w[2], h[2], r[1], bg[getConnectionColor()][500]]} />
169
-
<Text style={[text.gray[400], { fontSize: 14 }]}>
170
-
{isLive ? "LIVE" : "OFFLINE"}
171
-
</Text>
172
-
{isLive && segmentTiming.timeBetweenSegments && (
173
-
<Text style={[text.gray[500], { fontSize: 12 }]}>
174
-
{Math.round(segmentTiming.timeBetweenSegments)}ms
0
0
0
0
0
0
0
0
0
0
0
0
175
</Text>
176
-
)}
0
0
0
0
0
177
</View>
178
</View>
179
</View>
···
12
import { useSegmentTiming } from "../../hooks/useSegmentTiming";
13
import StreamScreen from "./live-selector";
14
15
+
const { flex, bg, r, borders, layout, p, text, w, h, mt } = zero;
16
17
interface StreamMonitorProps {
18
userProfile?: any;
···
89
style={[
90
flex.values[2],
91
bg.gray[800],
92
+
r.lg,
93
+
bg.neutral[900],
94
borders.width.thin,
95
+
borders.color.neutral[700],
96
layout.flex.column,
97
]}
98
>
99
+
<View style={[flex.values[1], layout.flex.center, bg.neutral[900]]}>
100
{isLive && userProfile ? (
101
isStreamVisible ? (
102
<Player src={userProfile.did} name={userProfile.handle} />
···
142
layout.flex.spaceBetween,
143
layout.flex.alignCenter,
144
p[4],
145
+
borders.top.width.thin,
146
+
borders.top.color.gray[700],
147
]}
148
>
149
+
<View
150
+
style={[
151
+
layout.flex.row,
152
+
layout.flex.spaceBetween,
153
+
layout.flex.alignCenter,
154
+
flex.grow[1],
155
+
{ gap: 12 },
156
+
]}
157
+
>
158
<Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
159
{ls?.record.title || "Stream Title"}
160
</Text>
161
+
<View
162
+
style={[
163
+
layout.flex.row,
164
+
layout.flex.justify.end,
165
+
layout.flex.align.start,
166
+
{ gap: 8, flexShrink: 0 },
167
+
]}
168
+
>
169
+
{isLive && userProfile && (
170
+
<TouchableOpacity
171
+
onPress={() => setIsStreamVisible(!isStreamVisible)}
172
+
style={{
173
+
padding: 4,
174
+
borderRadius: 4,
175
+
backgroundColor: "rgba(255, 255, 255, 0.1)",
176
+
}}
177
+
>
178
+
{isStreamVisible ? (
179
+
<EyeOff size={16} color="#9ca3af" />
180
+
) : (
181
+
<Eye size={16} color="#9ca3af" />
182
+
)}
183
+
</TouchableOpacity>
184
+
)}
185
+
<View
186
+
style={[
187
+
w[3],
188
+
h[3],
189
+
r.full,
190
+
{ marginTop: 3 },
191
+
bg[getConnectionColor()][500],
192
+
]}
193
+
/>
194
+
<Text style={[text.gray[400], { fontSize: 14 }]}>
195
+
{isLive ? "LIVE" : "OFFLINE"}
196
</Text>
197
+
{isLive && segmentTiming.timeBetweenSegments && (
198
+
<Text style={[text.gray[500], { fontSize: 12 }]}>
199
+
{Math.round(segmentTiming.timeBetweenSegments)}ms
200
+
</Text>
201
+
)}
202
+
</View>
203
</View>
204
</View>
205
</View>
+43
-32
js/app/components/ui/button-selector.tsx
···
1
-
import { Button, Text, XStack, YStack, YStackProps } from "tamagui";
0
0
0
0
0
0
0
0
0
2
3
export default function ButtonSelector({
4
text,
···
6
selectedValue,
7
setSelectedValue,
8
disabledValues,
0
9
...props
10
}: {
11
text?: string;
···
13
selectedValue: string;
14
setSelectedValue: (value: any) => void;
15
disabledValues?: string[];
16
-
} & YStackProps) {
0
17
return (
18
-
<YStack ai="flex-start" gap="$2" pt="$2" {...props}>
19
{text && (
20
-
<Text fontSize="$base" fontWeight="semibold">
21
{text}
22
</Text>
23
)}
24
-
<XStack
25
-
ai="center"
26
-
jc="space-around"
27
-
gap="$1"
28
-
w="100%"
29
-
bg="$background"
30
-
borderRadius="$xl"
31
>
32
-
{values.map(({ label, value }) => (
33
-
<Button
34
-
key={value}
35
-
onPress={() => setSelectedValue(value)}
36
-
f={1}
37
-
height="$2"
38
-
disabled={disabledValues?.includes(value)}
39
-
opacity={disabledValues?.includes(value) ? 0.5 : 1}
40
-
variant={selectedValue === value ? "outlined" : undefined}
41
-
>
42
-
<Text
43
-
color={
44
-
selectedValue === value
45
-
? "$color.foreground"
46
-
: "$color.mutedForeground"
47
-
}
0
0
48
>
49
{label}
50
-
</Text>
51
-
</Button>
52
-
))}
53
-
</XStack>
54
-
</YStack>
55
);
56
}
···
1
+
import {
2
+
Button,
3
+
Text,
4
+
useTheme,
5
+
View,
6
+
ViewProps,
7
+
zero,
8
+
} from "@streamplace/components";
9
+
10
+
const { gap, pt, w, bg, r, spacing, layout, colors } = zero;
11
12
export default function ButtonSelector({
13
text,
···
15
selectedValue,
16
setSelectedValue,
17
disabledValues,
18
+
style,
19
...props
20
}: {
21
text?: string;
···
23
selectedValue: string;
24
setSelectedValue: (value: any) => void;
25
disabledValues?: string[];
26
+
} & ViewProps) {
27
+
let theme = useTheme();
28
return (
29
+
<View align="start" style={[gap.all[2], style as any]} {...props}>
30
{text && (
31
+
<Text variant="body1" weight="semibold">
32
{text}
33
</Text>
34
)}
35
+
<View
36
+
direction="row"
37
+
align="center"
38
+
justify="around"
39
+
style={[gap.all[1], w.percent[100], r.full]}
0
0
40
>
41
+
{values.map(({ label, value }) => {
42
+
const isSelected = selectedValue === value;
43
+
const isDisabled = disabledValues?.includes(value);
44
+
45
+
return (
46
+
<Button
47
+
key={value}
48
+
onPress={() => setSelectedValue(value)}
49
+
variant={isSelected ? "outline" : "ghost"}
50
+
size="pill"
51
+
disabled={isDisabled}
52
+
style={[
53
+
{ flex: 1, maxHeight: 20 },
54
+
isSelected
55
+
? { backgroundColor: theme.theme.colors.primary }
56
+
: { backgroundColor: theme.theme.colors.secondary },
57
+
isDisabled && { opacity: 0.5 },
58
+
]}
59
>
60
{label}
61
+
</Button>
62
+
);
63
+
})}
64
+
</View>
65
+
</View>
66
);
67
}
+22
-14
js/components/src/components/chat/chat-message.tsx
···
7
import { Linking, View } from "react-native";
8
import { ChatMessageViewHydrated } from "streamplace";
9
import { RichtextSegment, segmentize } from "../../lib/facet";
10
-
import {
11
-
borders,
12
-
flex,
13
-
gap,
14
-
ml,
15
-
mr,
16
-
opacity,
17
-
pl,
18
-
w,
19
-
} from "../../lib/theme/atoms";
20
import { atoms, colors, layout } from "../ui";
21
22
interface Facet {
···
121
style={[
122
gap.all[2],
123
layout.flex.row,
124
-
w.percent[100],
125
borders.left.width.medium,
126
borders.left.color.gray[700],
127
ml[4],
···
129
opacity[80],
130
]}
131
>
132
-
<Text numberOfLines={1} style={[flex.shrink[1], mr[4]]}>
0
0
0
0
0
0
0
133
<Text
134
style={{
135
color: getRgbColor((item.replyTo.chatProfile as any).color),
···
149
</Text>
150
</View>
151
)}
152
-
<View style={[gap.all[2], layout.flex.row, w.percent[100]]}>
0
0
0
0
0
0
153
{showTime && (
154
<Text
155
style={{
···
160
{formatTime(item.record.createdAt)}
161
</Text>
162
)}
163
-
<Text weight="bold" color="default" style={[flex.shrink[1]]}>
0
0
0
0
164
<Text
165
style={[
166
{
···
7
import { Linking, View } from "react-native";
8
import { ChatMessageViewHydrated } from "streamplace";
9
import { RichtextSegment, segmentize } from "../../lib/facet";
10
+
import { borders, flex, gap, ml, mr, opacity, pl } from "../../lib/theme/atoms";
0
0
0
0
0
0
0
0
0
11
import { atoms, colors, layout } from "../ui";
12
13
interface Facet {
···
112
style={[
113
gap.all[2],
114
layout.flex.row,
115
+
{ minWidth: 0, maxWidth: "100%" },
116
borders.left.width.medium,
117
borders.left.color.gray[700],
118
ml[4],
···
120
opacity[80],
121
]}
122
>
123
+
<Text
124
+
numberOfLines={1}
125
+
style={[
126
+
flex.shrink[1],
127
+
mr[4],
128
+
{ minWidth: 0, overflow: "hidden" },
129
+
]}
130
+
>
131
<Text
132
style={{
133
color: getRgbColor((item.replyTo.chatProfile as any).color),
···
147
</Text>
148
</View>
149
)}
150
+
<View
151
+
style={[
152
+
gap.all[2],
153
+
layout.flex.row,
154
+
{ minWidth: 0, maxWidth: "100%" },
155
+
]}
156
+
>
157
{showTime && (
158
<Text
159
style={{
···
164
{formatTime(item.record.createdAt)}
165
</Text>
166
)}
167
+
<Text
168
+
weight="bold"
169
+
color="default"
170
+
style={[flex.shrink[1], { minWidth: 0, overflow: "hidden" }]}
171
+
>
172
<Text
173
style={[
174
{
+21
-6
js/components/src/components/chat/chat.tsx
···
17
useSetReplyToMessage,
18
View,
19
} from "../../";
20
-
import { bg, flex, px, py, w } from "../../lib/theme/atoms";
21
import { RenderChatMessage } from "./chat-message";
22
import { ModView } from "./mod-view";
23
···
87
padding: 1,
88
gap: 4,
89
zIndex: 10,
0
0
90
},
91
]}
92
>
···
184
style={[
185
py[1],
186
px[2],
187
-
{ position: "relative", borderRadius: 8 },
0
0
0
0
0
188
isHovered && bg.gray[950],
189
]}
190
onPointerEnter={handleHoverIn}
191
onPointerLeave={handleHoverOut}
192
>
193
-
<Pressable>
194
<RenderChatMessage item={item} />
195
</Pressable>
196
<ActionsBar
···
253
254
if (!chat)
255
return (
256
-
<View style={[flex.shrink[1]]}>
257
<Text>Loading chaat...</Text>
258
</View>
259
);
260
261
return (
262
-
<View style={[flex.shrink[1]].concat(propsStyle || [])}>
0
0
0
0
263
<FlatList
264
-
style={[flex.grow[1], flex.shrink[1], w.percent[100]]}
0
0
0
0
265
data={chat.slice(0, shownMessages)}
266
inverted={true}
267
keyExtractor={keyExtractor}
···
17
useSetReplyToMessage,
18
View,
19
} from "../../";
20
+
import { bg, flex, px, py } from "../../lib/theme/atoms";
21
import { RenderChatMessage } from "./chat-message";
22
import { ModView } from "./mod-view";
23
···
87
padding: 1,
88
gap: 4,
89
zIndex: 10,
90
+
maxWidth: 120,
91
+
flexShrink: 0,
92
},
93
]}
94
>
···
186
style={[
187
py[1],
188
px[2],
189
+
{
190
+
position: "relative",
191
+
borderRadius: 8,
192
+
minWidth: 0,
193
+
maxWidth: "100%",
194
+
},
195
isHovered && bg.gray[950],
196
]}
197
onPointerEnter={handleHoverIn}
198
onPointerLeave={handleHoverOut}
199
>
200
+
<Pressable style={[{ minWidth: 0, maxWidth: "100%" }]}>
201
<RenderChatMessage item={item} />
202
</Pressable>
203
<ActionsBar
···
260
261
if (!chat)
262
return (
263
+
<View style={[flex.shrink[1], { minWidth: 0, maxWidth: "100%" }]}>
264
<Text>Loading chaat...</Text>
265
</View>
266
);
267
268
return (
269
+
<View
270
+
style={[flex.shrink[1], { minWidth: 0, maxWidth: "100%" }].concat(
271
+
propsStyle || [],
272
+
)}
273
+
>
274
<FlatList
275
+
style={[
276
+
flex.grow[1],
277
+
flex.shrink[1],
278
+
{ minWidth: 0, maxWidth: "100%" },
279
+
]}
280
data={chat.slice(0, shownMessages)}
281
inverted={true}
282
keyExtractor={keyExtractor}
+1
-1
js/components/src/lib/theme/tokens.ts
···
427
428
export const borderRadius = {
429
none: 0,
430
-
sm: 4,
431
md: 8,
432
lg: 12,
433
xl: 16,
···
427
428
export const borderRadius = {
429
none: 0,
430
+
sm: 3,
431
md: 8,
432
lg: 12,
433
xl: 16,