kasunk99-livestream-core 0.3.8 → 0.3.10
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LiveStreamViewerItem.d.ts","sourceRoot":"","sources":["../../src/components/LiveStreamViewerItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqD,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"LiveStreamViewerItem.d.ts","sourceRoot":"","sources":["../../src/components/LiveStreamViewerItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqD,MAAM,OAAO,CAAC;AAgB1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAO/C,KAAK,yBAAyB,GAAG;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAuDF;;GAEG;AACH,eAAO,MAAM,oBAAoB,uDA0X/B,CAAC"}
|
|
@@ -1,25 +1,42 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { Animated, Dimensions, Keyboard, NativeModules, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
|
|
3
|
+
import { ActivityIndicator, Animated, Dimensions, Keyboard, NativeModules, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
|
|
4
4
|
import { useViewerSocket } from '../hooks/useViewerSocket';
|
|
5
|
-
const
|
|
5
|
+
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
|
6
6
|
const CHAT_NAME_COLORS = ['#f97316', '#22c55e', '#3b82f6', '#eab308', '#ec4899', '#a855f7'];
|
|
7
|
+
// iOS needs bottom safe-area padding; Android auto-adjusts via adjustResize
|
|
8
|
+
const BOTTOM_SAFE = Platform.OS === 'ios' ? 28 : 10;
|
|
7
9
|
let RTCViewComponent = null;
|
|
8
10
|
try {
|
|
9
11
|
const webrtc = require('react-native-webrtc');
|
|
10
12
|
RTCViewComponent = webrtc.RTCView;
|
|
11
13
|
}
|
|
12
14
|
catch {
|
|
13
|
-
// react-native-webrtc not available
|
|
15
|
+
// react-native-webrtc not available in Expo Go
|
|
16
|
+
}
|
|
17
|
+
// Renders a multi-step gradient (dark → transparent or vice-versa) using stacked Views.
|
|
18
|
+
// Smoothstep interpolation makes the transition indistinguishable from a real CSS gradient.
|
|
19
|
+
const GRADIENT_STEPS = 14;
|
|
20
|
+
function SmoothGradient({ direction, height, maxAlpha = 0.65, }) {
|
|
21
|
+
const stepH = height / GRADIENT_STEPS;
|
|
22
|
+
return (_jsx(View, { pointerEvents: "none", style: [
|
|
23
|
+
{ position: 'absolute', left: 0, right: 0, height },
|
|
24
|
+
direction === 'top' ? { top: 0 } : { bottom: 0 },
|
|
25
|
+
], children: Array.from({ length: GRADIENT_STEPS }, (_, i) => {
|
|
26
|
+
const t = i / (GRADIENT_STEPS - 1);
|
|
27
|
+
const smooth = t * t * (3 - 2 * t); // smoothstep: S-curve between 0 and 1
|
|
28
|
+
const alpha = direction === 'top'
|
|
29
|
+
? maxAlpha * (1 - smooth) // opaque at i=0 (top edge), clear at i=last
|
|
30
|
+
: maxAlpha * smooth; // clear at i=0, opaque at i=last (bottom edge)
|
|
31
|
+
return (_jsx(View, { style: { height: stepH, backgroundColor: `rgba(0,0,0,${alpha.toFixed(3)})` } }, i));
|
|
32
|
+
}) }));
|
|
14
33
|
}
|
|
15
34
|
/**
|
|
16
35
|
* Single full-screen viewer cell. When isActive, joins the stream room and consumes video/audio.
|
|
17
36
|
*/
|
|
18
|
-
export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream, isActive, index, }) {
|
|
37
|
+
export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream, isActive, index: _index, }) {
|
|
19
38
|
const roomId = isActive ? stream.roomId : null;
|
|
20
39
|
const { joined, joining, error, producerList, roomState, remoteStream, remoteVideoStream, webrtcUnavailable, consumeError, socket, viewerCount: liveViewerCount, streamEnded, } = useViewerSocket(roomId);
|
|
21
|
-
// Prefer real-time socket count (updated on every join/leave event);
|
|
22
|
-
// fall back to the HTTP-polled value from the feed for the initial render.
|
|
23
40
|
const viewerCount = liveViewerCount > 0 ? liveViewerCount : stream.viewerCount;
|
|
24
41
|
const hasVideo = producerList.some((p) => p.kind === 'video');
|
|
25
42
|
const streamObj = remoteStream;
|
|
@@ -35,41 +52,35 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
35
52
|
const showVideo = RTCViewComponent && remoteStream && !!streamURL && hasVideoTrack;
|
|
36
53
|
const [chatInput, setChatInput] = useState('');
|
|
37
54
|
const [chatMessages, setChatMessages] = useState([]);
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
// animation so the panel moves in perfect sync with the keyboard.
|
|
41
|
-
// Android: windowSoftInputMode="adjustResize" shrinks the layout automatically,
|
|
42
|
-
// so no JS offset is needed (adding one would create a double gap).
|
|
43
|
-
const chatBottomAnim = useRef(new Animated.Value(16)).current;
|
|
55
|
+
// iOS keyboard animation — Android handled automatically via windowSoftInputMode=adjustResize
|
|
56
|
+
const kbOffset = useRef(new Animated.Value(0)).current;
|
|
44
57
|
useEffect(() => {
|
|
45
58
|
if (Platform.OS !== 'ios')
|
|
46
|
-
return;
|
|
59
|
+
return;
|
|
47
60
|
const show = Keyboard.addListener('keyboardWillShow', (e) => {
|
|
48
|
-
Animated.timing(
|
|
49
|
-
toValue:
|
|
50
|
-
duration: e.duration ??
|
|
61
|
+
Animated.timing(kbOffset, {
|
|
62
|
+
toValue: e.endCoordinates.height,
|
|
63
|
+
duration: e.duration ?? 260,
|
|
51
64
|
useNativeDriver: false,
|
|
52
65
|
}).start();
|
|
53
66
|
});
|
|
54
67
|
const hide = Keyboard.addListener('keyboardWillHide', (e) => {
|
|
55
|
-
Animated.timing(
|
|
56
|
-
toValue:
|
|
57
|
-
duration: e.duration ??
|
|
68
|
+
Animated.timing(kbOffset, {
|
|
69
|
+
toValue: 0,
|
|
70
|
+
duration: e.duration ?? 220,
|
|
58
71
|
useNativeDriver: false,
|
|
59
72
|
}).start();
|
|
60
73
|
});
|
|
61
|
-
return () => {
|
|
62
|
-
|
|
63
|
-
hide.remove();
|
|
64
|
-
};
|
|
65
|
-
}, [chatBottomAnim]);
|
|
74
|
+
return () => { show.remove(); hide.remove(); };
|
|
75
|
+
}, [kbOffset]);
|
|
66
76
|
const seededRoomRef = useRef(null);
|
|
67
77
|
const chatListRef = useRef(null);
|
|
68
78
|
const seededFromRoomState = useMemo(() => {
|
|
69
|
-
const chat = roomState && typeof roomState === 'object'
|
|
79
|
+
const chat = roomState && typeof roomState === 'object'
|
|
80
|
+
? roomState.chat
|
|
81
|
+
: null;
|
|
70
82
|
return Array.isArray(chat) ? chat : [];
|
|
71
83
|
}, [roomState]);
|
|
72
|
-
// Reset local chat when switching rooms.
|
|
73
84
|
useEffect(() => {
|
|
74
85
|
if (!roomId) {
|
|
75
86
|
seededRoomRef.current = null;
|
|
@@ -83,7 +94,6 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
83
94
|
setChatInput('');
|
|
84
95
|
}
|
|
85
96
|
}, [roomId]);
|
|
86
|
-
// Seed chat history (once per room) from roomState.chat if present.
|
|
87
97
|
useEffect(() => {
|
|
88
98
|
if (!roomId)
|
|
89
99
|
return;
|
|
@@ -102,18 +112,18 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
102
112
|
const msg = m;
|
|
103
113
|
const displayName = typeof msg.displayName === 'string' ? msg.displayName : 'User';
|
|
104
114
|
const text = typeof msg.text === 'string' ? msg.text : '';
|
|
105
|
-
const timestamp = typeof msg.timestamp === 'number'
|
|
115
|
+
const timestamp = typeof msg.timestamp === 'number'
|
|
116
|
+
? msg.timestamp
|
|
117
|
+
: now - (seededFromRoomState.length - i) * 1000;
|
|
106
118
|
if (!text)
|
|
107
119
|
return null;
|
|
108
120
|
const id = `${roomId}-seed-${String(msg.peerId ?? i)}-${timestamp}-${i}`;
|
|
109
121
|
return { id, displayName, text, timestamp };
|
|
110
122
|
})
|
|
111
123
|
.filter(Boolean);
|
|
112
|
-
// Keep a reasonable amount of history, scrollable from the start.
|
|
113
124
|
return normalized;
|
|
114
125
|
});
|
|
115
126
|
}, [roomId, seededFromRoomState]);
|
|
116
|
-
// Live incoming chat messages.
|
|
117
127
|
useEffect(() => {
|
|
118
128
|
if (!socket || !roomId || !isActive)
|
|
119
129
|
return;
|
|
@@ -127,11 +137,7 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
127
137
|
if (!text)
|
|
128
138
|
return;
|
|
129
139
|
const id = `${roomId}-${String(m.peerId ?? 'peer')}-${timestamp}-${text.slice(0, 8)}`;
|
|
130
|
-
setChatMessages((prev) => {
|
|
131
|
-
const next = [...prev, { id, displayName, text, timestamp }];
|
|
132
|
-
// Allow full history to be scrollable.
|
|
133
|
-
return next;
|
|
134
|
-
});
|
|
140
|
+
setChatMessages((prev) => [...prev, { id, displayName, text, timestamp }]);
|
|
135
141
|
};
|
|
136
142
|
const onViewerJoined = (msg) => {
|
|
137
143
|
const m = msg;
|
|
@@ -150,20 +156,16 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
150
156
|
socket.off('viewer-joined', onViewerJoined);
|
|
151
157
|
};
|
|
152
158
|
}, [isActive, roomId, socket]);
|
|
153
|
-
// Render messages in natural order (oldest -> newest).
|
|
154
159
|
const chatData = useMemo(() => chatMessages, [chatMessages]);
|
|
155
|
-
// Always scroll to the latest message at the bottom when chat loads/updates.
|
|
156
160
|
useEffect(() => {
|
|
157
161
|
if (!chatListRef.current || chatData.length === 0)
|
|
158
162
|
return;
|
|
159
163
|
try {
|
|
160
164
|
chatListRef.current.scrollToEnd({ animated: true });
|
|
161
165
|
}
|
|
162
|
-
catch {
|
|
163
|
-
// ignore scroll errors
|
|
164
|
-
}
|
|
166
|
+
catch { /* ignore */ }
|
|
165
167
|
}, [chatData.length]);
|
|
166
|
-
// System audio playback (Android only
|
|
168
|
+
// System audio playback (Android only)
|
|
167
169
|
useEffect(() => {
|
|
168
170
|
if (!socket || !joined || !isActive || Platform.OS !== 'android')
|
|
169
171
|
return;
|
|
@@ -198,185 +200,273 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
198
200
|
return CHAT_NAME_COLORS[0];
|
|
199
201
|
let hash = 0;
|
|
200
202
|
for (let i = 0; i < name.length; i += 1) {
|
|
201
|
-
// simple string hash
|
|
202
203
|
hash = (hash << 5) - hash + name.charCodeAt(i);
|
|
203
204
|
hash |= 0;
|
|
204
205
|
}
|
|
205
|
-
|
|
206
|
-
return CHAT_NAME_COLORS[idx];
|
|
206
|
+
return CHAT_NAME_COLORS[Math.abs(hash) % CHAT_NAME_COLORS.length];
|
|
207
207
|
};
|
|
208
|
-
const sendChat =
|
|
208
|
+
const sendChat = () => {
|
|
209
209
|
const text = chatInput.trim();
|
|
210
210
|
if (!text || !socket || !joined)
|
|
211
211
|
return;
|
|
212
212
|
setChatInput('');
|
|
213
213
|
socket.emit('chat-message', { text }, (res) => {
|
|
214
|
-
if (res?.error)
|
|
215
|
-
// eslint-disable-next-line no-console
|
|
214
|
+
if (res?.error)
|
|
216
215
|
console.log('[viewer] chat send error', res.error);
|
|
217
|
-
}
|
|
218
216
|
});
|
|
219
217
|
};
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
], children: _jsx(Text, { style: styles.chatSendText, children: "\u27A4" }) })] })] }) })] }), streamEnded && (_jsx(View, { style: styles.streamEndedOverlay, children: _jsx(Text, { style: styles.streamEndedText, children: "Live stream ended" }) }))] }));
|
|
218
|
+
const hostLabel = stream.hostDisplayName || stream.title || 'Live';
|
|
219
|
+
const avatarLetter = hostLabel[0]?.toUpperCase() ?? 'L';
|
|
220
|
+
return (_jsxs(View, { style: styles.container, children: [showVideo && RTCViewComponent && streamURL ? (_jsx(RTCViewComponent, { streamURL: streamURL, stream: displayStream, style: styles.rtcView, objectFit: "cover", mirror: false }, `rtc-${trackCount}-${streamURL}`)) : (_jsxs(View, { style: styles.videoPlaceholder, children: [joining && (_jsx(ActivityIndicator, { size: "large", color: "rgba(255,255,255,0.6)" })), !joining && error && (_jsx(Text, { style: styles.placeholderError, children: error })), joined && !joining && !error && hasVideo && !remoteStream && (_jsx(ActivityIndicator, { size: "large", color: "rgba(255,255,255,0.45)" })), joined && !joining && !error && hasVideo && webrtcUnavailable && (_jsx(Text, { style: styles.placeholderHint, children: "Video needs a development build" })), joined && !joining && !error && hasVideo && consumeError && (_jsx(Text, { style: styles.placeholderError, children: consumeError }))] })), _jsx(SmoothGradient, { direction: "top", height: 170, maxAlpha: 0.62 }), _jsx(SmoothGradient, { direction: "bottom", height: 300, maxAlpha: 0.68 }), _jsxs(View, { style: styles.topBar, pointerEvents: "box-none", children: [_jsxs(View, { style: styles.hostRow, pointerEvents: "none", children: [_jsx(View, { style: styles.avatar, children: _jsx(Text, { style: styles.avatarLetter, children: avatarLetter }) }), _jsx(Text, { style: styles.hostName, numberOfLines: 1, children: hostLabel }), _jsx(View, { style: styles.livePill, children: _jsx(Text, { style: styles.livePillText, children: "LIVE" }) })] }), _jsxs(View, { style: styles.viewerChip, pointerEvents: "none", children: [_jsx(Text, { style: styles.viewerEye, children: "\uD83D\uDC41" }), _jsx(Text, { style: styles.viewerCount, children: viewerCount > 0 ? viewerCount.toLocaleString() : '—' })] })] }), _jsxs(View, { style: styles.rightColumn, pointerEvents: "box-none", children: [_jsxs(TouchableOpacity, { style: styles.actionBtn, activeOpacity: 0.75, children: [_jsx(View, { style: styles.actionCircle, children: _jsx(Text, { style: styles.actionIcon, children: "\u2665" }) }), _jsx(Text, { style: styles.actionLabel, children: "Like" })] }), _jsxs(TouchableOpacity, { style: styles.actionBtn, activeOpacity: 0.75, children: [_jsx(View, { style: styles.actionCircle, children: _jsx(Text, { style: styles.actionIcon, children: "\uD83C\uDF81" }) }), _jsx(Text, { style: styles.actionLabel, children: "Gift" })] }), _jsxs(TouchableOpacity, { style: styles.actionBtn, activeOpacity: 0.75, children: [_jsx(View, { style: styles.actionCircle, children: _jsx(Text, { style: styles.actionIcon, children: "\u2197" }) }), _jsx(Text, { style: styles.actionLabel, children: "Share" })] })] }), _jsxs(Animated.View, { style: [styles.bottomPanel, { bottom: kbOffset }], pointerEvents: "box-none", children: [_jsx(View, { style: styles.chatColumn, pointerEvents: "box-none", children: _jsx(ScrollView, { ref: chatListRef, contentContainerStyle: styles.chatContent, showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "always", keyboardDismissMode: "none", children: chatData.map((item) => item.displayName ? (_jsx(View, { style: styles.chatBubble, children: _jsxs(Text, { style: styles.chatLine, numberOfLines: 3, children: [_jsxs(Text, { style: [styles.chatUsername, { color: getNameColor(item.displayName) }], children: [item.displayName, ' '] }), _jsx(Text, { style: styles.chatMsg, children: item.text })] }) }, item.id)) : (_jsx(View, { style: styles.joinBubble, children: _jsx(Text, { style: styles.joinText, children: item.text }) }, item.id))) }) }), _jsxs(View, { style: styles.inputBar, children: [_jsx(TouchableOpacity, { style: styles.inputIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.inputIconGlyph, children: "\u263A" }) }), _jsx(TextInput, { style: styles.textInput, value: chatInput, onChangeText: setChatInput, placeholder: joined ? 'Say something...' : 'Connecting...', placeholderTextColor: "rgba(255,255,255,0.35)", editable: joined && !joining && !error, onSubmitEditing: sendChat, returnKeyType: "send", blurOnSubmit: false }), _jsx(TouchableOpacity, { style: styles.inputIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.inputIconGlyph, children: "\uD83C\uDF39" }) }), _jsx(TouchableOpacity, { style: [styles.sendBtn, (!chatInput.trim() || !joined) && styles.sendBtnOff], onPress: sendChat, activeOpacity: 0.75, disabled: !joined || !chatInput.trim(), children: _jsx(Text, { style: styles.sendIcon, children: "\u25B6" }) })] })] }), streamEnded && (_jsx(View, { style: styles.endedOverlay, children: _jsxs(View, { style: styles.endedCard, children: [_jsx(Text, { style: styles.endedTitle, children: "Stream ended" }), _jsx(Text, { style: styles.endedSub, children: "The host has ended this live stream" })] }) }))] }));
|
|
224
221
|
});
|
|
222
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
225
223
|
const styles = StyleSheet.create({
|
|
226
224
|
container: {
|
|
227
225
|
flex: 1,
|
|
228
|
-
backgroundColor: '#
|
|
229
|
-
},
|
|
230
|
-
streamEndedOverlay: {
|
|
231
|
-
...StyleSheet.absoluteFillObject,
|
|
232
|
-
backgroundColor: 'rgba(0,0,0,0.82)',
|
|
233
|
-
alignItems: 'center',
|
|
234
|
-
justifyContent: 'center',
|
|
235
|
-
},
|
|
236
|
-
streamEndedText: {
|
|
237
|
-
color: '#ffffff',
|
|
238
|
-
fontSize: 20,
|
|
239
|
-
fontWeight: '600',
|
|
226
|
+
backgroundColor: '#000',
|
|
240
227
|
},
|
|
228
|
+
// ── Video ─────────────────────────────────────────────────────────────────
|
|
241
229
|
rtcView: {
|
|
242
|
-
|
|
243
|
-
backgroundColor: '#
|
|
230
|
+
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
231
|
+
backgroundColor: '#000',
|
|
244
232
|
},
|
|
245
233
|
videoPlaceholder: {
|
|
246
|
-
|
|
234
|
+
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
247
235
|
alignItems: 'center',
|
|
248
236
|
justifyContent: 'center',
|
|
249
|
-
backgroundColor: '#
|
|
250
|
-
|
|
251
|
-
placeholderText: {
|
|
252
|
-
color: '#e5e5e5',
|
|
253
|
-
fontSize: 16,
|
|
254
|
-
marginVertical: 4,
|
|
255
|
-
},
|
|
256
|
-
errorText: {
|
|
257
|
-
color: '#ef4444',
|
|
237
|
+
backgroundColor: '#0d0d0d',
|
|
238
|
+
gap: 12,
|
|
258
239
|
},
|
|
259
|
-
|
|
240
|
+
placeholderError: {
|
|
260
241
|
color: '#ef4444',
|
|
261
|
-
fontSize:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
marginBottom: 8,
|
|
242
|
+
fontSize: 13,
|
|
243
|
+
textAlign: 'center',
|
|
244
|
+
paddingHorizontal: 28,
|
|
265
245
|
},
|
|
266
|
-
|
|
267
|
-
color: '
|
|
246
|
+
placeholderHint: {
|
|
247
|
+
color: 'rgba(255,255,255,0.38)',
|
|
268
248
|
fontSize: 12,
|
|
269
|
-
marginTop: 4,
|
|
270
249
|
textAlign: 'center',
|
|
250
|
+
paddingHorizontal: 28,
|
|
271
251
|
},
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
marginTop: 8,
|
|
275
|
-
paddingHorizontal: 16,
|
|
276
|
-
},
|
|
277
|
-
overlay: {
|
|
252
|
+
// ── Top bar ───────────────────────────────────────────────────────────────
|
|
253
|
+
topBar: {
|
|
278
254
|
position: 'absolute',
|
|
279
|
-
top:
|
|
280
|
-
left:
|
|
281
|
-
right:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
backgroundColor: 'transparent',
|
|
286
|
-
},
|
|
287
|
-
topMeta: {
|
|
288
|
-
marginTop: 6,
|
|
255
|
+
top: 46,
|
|
256
|
+
left: 14,
|
|
257
|
+
right: 14,
|
|
258
|
+
flexDirection: 'row',
|
|
259
|
+
alignItems: 'center',
|
|
260
|
+
justifyContent: 'space-between',
|
|
289
261
|
},
|
|
290
|
-
|
|
262
|
+
hostRow: {
|
|
291
263
|
flexDirection: 'row',
|
|
292
264
|
alignItems: 'center',
|
|
265
|
+
gap: 8,
|
|
266
|
+
flex: 1,
|
|
267
|
+
marginRight: 8,
|
|
268
|
+
overflow: 'hidden',
|
|
293
269
|
},
|
|
294
|
-
|
|
295
|
-
|
|
270
|
+
avatar: {
|
|
271
|
+
width: 36,
|
|
272
|
+
height: 36,
|
|
273
|
+
borderRadius: 18,
|
|
274
|
+
backgroundColor: 'rgba(255,255,255,0.15)',
|
|
275
|
+
borderWidth: 1.5,
|
|
276
|
+
borderColor: 'rgba(255,255,255,0.3)',
|
|
277
|
+
alignItems: 'center',
|
|
278
|
+
justifyContent: 'center',
|
|
279
|
+
flexShrink: 0,
|
|
280
|
+
},
|
|
281
|
+
avatarLetter: {
|
|
282
|
+
color: '#fff',
|
|
283
|
+
fontSize: 15,
|
|
284
|
+
fontWeight: '700',
|
|
285
|
+
},
|
|
286
|
+
hostName: {
|
|
287
|
+
color: '#fff',
|
|
296
288
|
fontSize: 14,
|
|
297
289
|
fontWeight: '600',
|
|
290
|
+
flex: 1,
|
|
291
|
+
},
|
|
292
|
+
livePill: {
|
|
293
|
+
backgroundColor: '#ef4444',
|
|
294
|
+
borderRadius: 6,
|
|
295
|
+
paddingHorizontal: 7,
|
|
296
|
+
paddingVertical: 3,
|
|
297
|
+
flexShrink: 0,
|
|
298
298
|
},
|
|
299
|
-
|
|
299
|
+
livePillText: {
|
|
300
|
+
color: '#fff',
|
|
301
|
+
fontSize: 11,
|
|
302
|
+
fontWeight: '700',
|
|
303
|
+
letterSpacing: 0.8,
|
|
304
|
+
},
|
|
305
|
+
viewerChip: {
|
|
300
306
|
flexDirection: 'row',
|
|
301
307
|
alignItems: 'center',
|
|
302
|
-
|
|
308
|
+
backgroundColor: 'rgba(0,0,0,0.42)',
|
|
309
|
+
borderRadius: 20,
|
|
310
|
+
paddingHorizontal: 10,
|
|
311
|
+
paddingVertical: 5,
|
|
312
|
+
gap: 5,
|
|
313
|
+
flexShrink: 0,
|
|
303
314
|
},
|
|
304
|
-
|
|
305
|
-
|
|
315
|
+
viewerEye: { fontSize: 11 },
|
|
316
|
+
viewerCount: {
|
|
317
|
+
color: 'rgba(255,255,255,0.88)',
|
|
306
318
|
fontSize: 12,
|
|
319
|
+
fontWeight: '500',
|
|
307
320
|
},
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
},
|
|
311
|
-
chatPanel: {
|
|
321
|
+
// ── Right action column ───────────────────────────────────────────────────
|
|
322
|
+
rightColumn: {
|
|
312
323
|
position: 'absolute',
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
324
|
+
right: 12,
|
|
325
|
+
bottom: 92,
|
|
326
|
+
alignItems: 'center',
|
|
327
|
+
gap: 14,
|
|
316
328
|
},
|
|
317
|
-
|
|
318
|
-
|
|
329
|
+
actionBtn: {
|
|
330
|
+
alignItems: 'center',
|
|
331
|
+
gap: 5,
|
|
319
332
|
},
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
borderRadius:
|
|
333
|
+
actionCircle: {
|
|
334
|
+
width: 48,
|
|
335
|
+
height: 48,
|
|
336
|
+
borderRadius: 24,
|
|
337
|
+
backgroundColor: 'rgba(255,255,255,0.14)',
|
|
338
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
339
|
+
borderColor: 'rgba(255,255,255,0.2)',
|
|
340
|
+
alignItems: 'center',
|
|
341
|
+
justifyContent: 'center',
|
|
342
|
+
},
|
|
343
|
+
actionIcon: {
|
|
344
|
+
fontSize: 22,
|
|
345
|
+
color: '#fff',
|
|
346
|
+
},
|
|
347
|
+
actionLabel: {
|
|
348
|
+
color: 'rgba(255,255,255,0.7)',
|
|
349
|
+
fontSize: 11,
|
|
350
|
+
fontWeight: '500',
|
|
351
|
+
},
|
|
352
|
+
// ── Bottom panel ──────────────────────────────────────────────────────────
|
|
353
|
+
bottomPanel: {
|
|
354
|
+
position: 'absolute',
|
|
355
|
+
left: 0,
|
|
356
|
+
right: 0,
|
|
324
357
|
paddingHorizontal: 12,
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
358
|
+
paddingBottom: BOTTOM_SAFE,
|
|
359
|
+
gap: 8,
|
|
360
|
+
},
|
|
361
|
+
// Chat messages
|
|
362
|
+
chatColumn: {
|
|
363
|
+
width: '66%',
|
|
364
|
+
maxHeight: Math.round(SCREEN_HEIGHT * 0.30),
|
|
365
|
+
},
|
|
366
|
+
chatContent: {
|
|
367
|
+
paddingBottom: 2,
|
|
368
|
+
},
|
|
369
|
+
chatBubble: {
|
|
332
370
|
alignSelf: 'flex-start',
|
|
333
|
-
|
|
334
|
-
|
|
371
|
+
marginBottom: 5,
|
|
372
|
+
backgroundColor: 'rgba(0,0,0,0.40)',
|
|
373
|
+
borderRadius: 14,
|
|
374
|
+
paddingHorizontal: 10,
|
|
375
|
+
paddingVertical: 5,
|
|
376
|
+
maxWidth: '100%',
|
|
335
377
|
},
|
|
336
|
-
|
|
337
|
-
|
|
378
|
+
joinBubble: {
|
|
379
|
+
alignSelf: 'flex-start',
|
|
380
|
+
marginBottom: 5,
|
|
381
|
+
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
382
|
+
borderRadius: 14,
|
|
383
|
+
paddingHorizontal: 10,
|
|
384
|
+
paddingVertical: 4,
|
|
385
|
+
maxWidth: '100%',
|
|
338
386
|
},
|
|
339
387
|
chatLine: {
|
|
340
|
-
color: '#e5e5e5',
|
|
341
388
|
fontSize: 13,
|
|
342
389
|
lineHeight: 18,
|
|
343
|
-
marginBottom: 4,
|
|
344
390
|
},
|
|
345
|
-
|
|
346
|
-
color: '#fafafa',
|
|
391
|
+
chatUsername: {
|
|
347
392
|
fontWeight: '700',
|
|
348
393
|
},
|
|
349
|
-
|
|
394
|
+
chatMsg: {
|
|
350
395
|
color: '#e5e5e5',
|
|
351
396
|
fontWeight: '400',
|
|
352
397
|
},
|
|
353
|
-
|
|
354
|
-
|
|
398
|
+
joinText: {
|
|
399
|
+
color: 'rgba(255,255,255,0.48)',
|
|
400
|
+
fontSize: 12,
|
|
401
|
+
fontStyle: 'italic',
|
|
402
|
+
},
|
|
403
|
+
// Input bar
|
|
404
|
+
inputBar: {
|
|
355
405
|
flexDirection: 'row',
|
|
356
406
|
alignItems: 'center',
|
|
407
|
+
backgroundColor: 'rgba(18,18,18,0.75)',
|
|
357
408
|
borderRadius: 999,
|
|
358
|
-
|
|
359
|
-
borderWidth: 1,
|
|
409
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
360
410
|
borderColor: 'rgba(255,255,255,0.12)',
|
|
361
|
-
|
|
411
|
+
paddingHorizontal: 4,
|
|
412
|
+
paddingVertical: 4,
|
|
413
|
+
gap: 2,
|
|
414
|
+
},
|
|
415
|
+
inputIconBtn: {
|
|
416
|
+
width: 40,
|
|
417
|
+
height: 40,
|
|
418
|
+
alignItems: 'center',
|
|
419
|
+
justifyContent: 'center',
|
|
420
|
+
borderRadius: 20,
|
|
362
421
|
},
|
|
363
|
-
|
|
422
|
+
inputIconGlyph: {
|
|
423
|
+
fontSize: 21,
|
|
424
|
+
},
|
|
425
|
+
textInput: {
|
|
364
426
|
flex: 1,
|
|
365
|
-
|
|
366
|
-
paddingVertical: 10,
|
|
367
|
-
color: '#fafafa',
|
|
427
|
+
color: '#fff',
|
|
368
428
|
fontSize: 14,
|
|
429
|
+
paddingHorizontal: 6,
|
|
430
|
+
paddingVertical: Platform.OS === 'ios' ? 10 : 6,
|
|
431
|
+
minHeight: 40,
|
|
432
|
+
},
|
|
433
|
+
sendBtn: {
|
|
434
|
+
width: 40,
|
|
435
|
+
height: 40,
|
|
436
|
+
borderRadius: 20,
|
|
437
|
+
backgroundColor: '#f97316',
|
|
438
|
+
alignItems: 'center',
|
|
439
|
+
justifyContent: 'center',
|
|
369
440
|
},
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
paddingVertical: 10,
|
|
441
|
+
sendBtnOff: {
|
|
442
|
+
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
373
443
|
},
|
|
374
|
-
|
|
375
|
-
|
|
444
|
+
sendIcon: {
|
|
445
|
+
color: '#fff',
|
|
446
|
+
fontSize: 15,
|
|
447
|
+
fontWeight: '700',
|
|
376
448
|
},
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
449
|
+
// ── Stream ended ──────────────────────────────────────────────────────────
|
|
450
|
+
endedOverlay: {
|
|
451
|
+
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
452
|
+
backgroundColor: 'rgba(0,0,0,0.80)',
|
|
453
|
+
alignItems: 'center',
|
|
454
|
+
justifyContent: 'center',
|
|
455
|
+
},
|
|
456
|
+
endedCard: {
|
|
457
|
+
alignItems: 'center',
|
|
458
|
+
gap: 10,
|
|
459
|
+
paddingHorizontal: 36,
|
|
460
|
+
},
|
|
461
|
+
endedTitle: {
|
|
462
|
+
color: '#fff',
|
|
463
|
+
fontSize: 22,
|
|
380
464
|
fontWeight: '700',
|
|
381
465
|
},
|
|
466
|
+
endedSub: {
|
|
467
|
+
color: 'rgba(255,255,255,0.5)',
|
|
468
|
+
fontSize: 14,
|
|
469
|
+
textAlign: 'center',
|
|
470
|
+
lineHeight: 20,
|
|
471
|
+
},
|
|
382
472
|
});
|
package/package.json
CHANGED