kasunk99-livestream-core 0.3.8 → 0.3.9
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;AAgBF;;GAEG;AACH,eAAO,MAAM,oBAAoB,uDA2X/B,CAAC"}
|
|
@@ -1,25 +1,25 @@
|
|
|
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
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Single full-screen viewer cell. When isActive, joins the stream room and consumes video/audio.
|
|
17
19
|
*/
|
|
18
|
-
export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream, isActive, index, }) {
|
|
20
|
+
export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream, isActive, index: _index, }) {
|
|
19
21
|
const roomId = isActive ? stream.roomId : null;
|
|
20
22
|
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
23
|
const viewerCount = liveViewerCount > 0 ? liveViewerCount : stream.viewerCount;
|
|
24
24
|
const hasVideo = producerList.some((p) => p.kind === 'video');
|
|
25
25
|
const streamObj = remoteStream;
|
|
@@ -35,41 +35,35 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
35
35
|
const showVideo = RTCViewComponent && remoteStream && !!streamURL && hasVideoTrack;
|
|
36
36
|
const [chatInput, setChatInput] = useState('');
|
|
37
37
|
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;
|
|
38
|
+
// iOS keyboard animation — Android handled automatically via windowSoftInputMode=adjustResize
|
|
39
|
+
const kbOffset = useRef(new Animated.Value(0)).current;
|
|
44
40
|
useEffect(() => {
|
|
45
41
|
if (Platform.OS !== 'ios')
|
|
46
|
-
return;
|
|
42
|
+
return;
|
|
47
43
|
const show = Keyboard.addListener('keyboardWillShow', (e) => {
|
|
48
|
-
Animated.timing(
|
|
49
|
-
toValue:
|
|
50
|
-
duration: e.duration ??
|
|
44
|
+
Animated.timing(kbOffset, {
|
|
45
|
+
toValue: e.endCoordinates.height,
|
|
46
|
+
duration: e.duration ?? 260,
|
|
51
47
|
useNativeDriver: false,
|
|
52
48
|
}).start();
|
|
53
49
|
});
|
|
54
50
|
const hide = Keyboard.addListener('keyboardWillHide', (e) => {
|
|
55
|
-
Animated.timing(
|
|
56
|
-
toValue:
|
|
57
|
-
duration: e.duration ??
|
|
51
|
+
Animated.timing(kbOffset, {
|
|
52
|
+
toValue: 0,
|
|
53
|
+
duration: e.duration ?? 220,
|
|
58
54
|
useNativeDriver: false,
|
|
59
55
|
}).start();
|
|
60
56
|
});
|
|
61
|
-
return () => {
|
|
62
|
-
|
|
63
|
-
hide.remove();
|
|
64
|
-
};
|
|
65
|
-
}, [chatBottomAnim]);
|
|
57
|
+
return () => { show.remove(); hide.remove(); };
|
|
58
|
+
}, [kbOffset]);
|
|
66
59
|
const seededRoomRef = useRef(null);
|
|
67
60
|
const chatListRef = useRef(null);
|
|
68
61
|
const seededFromRoomState = useMemo(() => {
|
|
69
|
-
const chat = roomState && typeof roomState === 'object'
|
|
62
|
+
const chat = roomState && typeof roomState === 'object'
|
|
63
|
+
? roomState.chat
|
|
64
|
+
: null;
|
|
70
65
|
return Array.isArray(chat) ? chat : [];
|
|
71
66
|
}, [roomState]);
|
|
72
|
-
// Reset local chat when switching rooms.
|
|
73
67
|
useEffect(() => {
|
|
74
68
|
if (!roomId) {
|
|
75
69
|
seededRoomRef.current = null;
|
|
@@ -83,7 +77,6 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
83
77
|
setChatInput('');
|
|
84
78
|
}
|
|
85
79
|
}, [roomId]);
|
|
86
|
-
// Seed chat history (once per room) from roomState.chat if present.
|
|
87
80
|
useEffect(() => {
|
|
88
81
|
if (!roomId)
|
|
89
82
|
return;
|
|
@@ -102,18 +95,18 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
102
95
|
const msg = m;
|
|
103
96
|
const displayName = typeof msg.displayName === 'string' ? msg.displayName : 'User';
|
|
104
97
|
const text = typeof msg.text === 'string' ? msg.text : '';
|
|
105
|
-
const timestamp = typeof msg.timestamp === 'number'
|
|
98
|
+
const timestamp = typeof msg.timestamp === 'number'
|
|
99
|
+
? msg.timestamp
|
|
100
|
+
: now - (seededFromRoomState.length - i) * 1000;
|
|
106
101
|
if (!text)
|
|
107
102
|
return null;
|
|
108
103
|
const id = `${roomId}-seed-${String(msg.peerId ?? i)}-${timestamp}-${i}`;
|
|
109
104
|
return { id, displayName, text, timestamp };
|
|
110
105
|
})
|
|
111
106
|
.filter(Boolean);
|
|
112
|
-
// Keep a reasonable amount of history, scrollable from the start.
|
|
113
107
|
return normalized;
|
|
114
108
|
});
|
|
115
109
|
}, [roomId, seededFromRoomState]);
|
|
116
|
-
// Live incoming chat messages.
|
|
117
110
|
useEffect(() => {
|
|
118
111
|
if (!socket || !roomId || !isActive)
|
|
119
112
|
return;
|
|
@@ -127,11 +120,7 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
127
120
|
if (!text)
|
|
128
121
|
return;
|
|
129
122
|
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
|
-
});
|
|
123
|
+
setChatMessages((prev) => [...prev, { id, displayName, text, timestamp }]);
|
|
135
124
|
};
|
|
136
125
|
const onViewerJoined = (msg) => {
|
|
137
126
|
const m = msg;
|
|
@@ -150,20 +139,16 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
150
139
|
socket.off('viewer-joined', onViewerJoined);
|
|
151
140
|
};
|
|
152
141
|
}, [isActive, roomId, socket]);
|
|
153
|
-
// Render messages in natural order (oldest -> newest).
|
|
154
142
|
const chatData = useMemo(() => chatMessages, [chatMessages]);
|
|
155
|
-
// Always scroll to the latest message at the bottom when chat loads/updates.
|
|
156
143
|
useEffect(() => {
|
|
157
144
|
if (!chatListRef.current || chatData.length === 0)
|
|
158
145
|
return;
|
|
159
146
|
try {
|
|
160
147
|
chatListRef.current.scrollToEnd({ animated: true });
|
|
161
148
|
}
|
|
162
|
-
catch {
|
|
163
|
-
// ignore scroll errors
|
|
164
|
-
}
|
|
149
|
+
catch { /* ignore */ }
|
|
165
150
|
}, [chatData.length]);
|
|
166
|
-
// System audio playback (Android only
|
|
151
|
+
// System audio playback (Android only)
|
|
167
152
|
useEffect(() => {
|
|
168
153
|
if (!socket || !joined || !isActive || Platform.OS !== 'android')
|
|
169
154
|
return;
|
|
@@ -198,185 +183,290 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
198
183
|
return CHAT_NAME_COLORS[0];
|
|
199
184
|
let hash = 0;
|
|
200
185
|
for (let i = 0; i < name.length; i += 1) {
|
|
201
|
-
// simple string hash
|
|
202
186
|
hash = (hash << 5) - hash + name.charCodeAt(i);
|
|
203
187
|
hash |= 0;
|
|
204
188
|
}
|
|
205
|
-
|
|
206
|
-
return CHAT_NAME_COLORS[idx];
|
|
189
|
+
return CHAT_NAME_COLORS[Math.abs(hash) % CHAT_NAME_COLORS.length];
|
|
207
190
|
};
|
|
208
|
-
const sendChat =
|
|
191
|
+
const sendChat = () => {
|
|
209
192
|
const text = chatInput.trim();
|
|
210
193
|
if (!text || !socket || !joined)
|
|
211
194
|
return;
|
|
212
195
|
setChatInput('');
|
|
213
196
|
socket.emit('chat-message', { text }, (res) => {
|
|
214
|
-
if (res?.error)
|
|
215
|
-
// eslint-disable-next-line no-console
|
|
197
|
+
if (res?.error)
|
|
216
198
|
console.log('[viewer] chat send error', res.error);
|
|
217
|
-
}
|
|
218
199
|
});
|
|
219
200
|
};
|
|
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" }) }))] }));
|
|
201
|
+
const hostLabel = stream.hostDisplayName || stream.title || 'Live';
|
|
202
|
+
const avatarLetter = hostLabel[0]?.toUpperCase() ?? 'L';
|
|
203
|
+
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(View, { style: styles.topGradient, pointerEvents: "none" }), _jsx(View, { style: styles.bottomGradient, pointerEvents: "none" }), _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
204
|
});
|
|
205
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
225
206
|
const styles = StyleSheet.create({
|
|
226
207
|
container: {
|
|
227
208
|
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',
|
|
209
|
+
backgroundColor: '#000',
|
|
240
210
|
},
|
|
211
|
+
// ── Video ─────────────────────────────────────────────────────────────────
|
|
241
212
|
rtcView: {
|
|
242
|
-
|
|
243
|
-
backgroundColor: '#
|
|
213
|
+
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
214
|
+
backgroundColor: '#000',
|
|
244
215
|
},
|
|
245
216
|
videoPlaceholder: {
|
|
246
|
-
|
|
217
|
+
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
247
218
|
alignItems: 'center',
|
|
248
219
|
justifyContent: 'center',
|
|
249
|
-
backgroundColor: '#
|
|
220
|
+
backgroundColor: '#0d0d0d',
|
|
221
|
+
gap: 12,
|
|
250
222
|
},
|
|
251
|
-
|
|
252
|
-
color: '#e5e5e5',
|
|
253
|
-
fontSize: 16,
|
|
254
|
-
marginVertical: 4,
|
|
255
|
-
},
|
|
256
|
-
errorText: {
|
|
223
|
+
placeholderError: {
|
|
257
224
|
color: '#ef4444',
|
|
225
|
+
fontSize: 13,
|
|
226
|
+
textAlign: 'center',
|
|
227
|
+
paddingHorizontal: 28,
|
|
258
228
|
},
|
|
259
|
-
|
|
260
|
-
color: '
|
|
261
|
-
fontSize: 14,
|
|
262
|
-
fontWeight: '700',
|
|
263
|
-
letterSpacing: 1,
|
|
264
|
-
marginBottom: 8,
|
|
265
|
-
},
|
|
266
|
-
hintText: {
|
|
267
|
-
color: '#737373',
|
|
229
|
+
placeholderHint: {
|
|
230
|
+
color: 'rgba(255,255,255,0.38)',
|
|
268
231
|
fontSize: 12,
|
|
269
|
-
marginTop: 4,
|
|
270
232
|
textAlign: 'center',
|
|
233
|
+
paddingHorizontal: 28,
|
|
271
234
|
},
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
marginTop: 8,
|
|
275
|
-
paddingHorizontal: 16,
|
|
276
|
-
},
|
|
277
|
-
overlay: {
|
|
235
|
+
// ── Gradient overlays ─────────────────────────────────────────────────────
|
|
236
|
+
topGradient: {
|
|
278
237
|
position: 'absolute',
|
|
279
238
|
top: 0,
|
|
280
239
|
left: 0,
|
|
281
240
|
right: 0,
|
|
241
|
+
height: 140,
|
|
242
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
243
|
+
},
|
|
244
|
+
bottomGradient: {
|
|
245
|
+
position: 'absolute',
|
|
282
246
|
bottom: 0,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
247
|
+
left: 0,
|
|
248
|
+
right: 0,
|
|
249
|
+
height: 260,
|
|
250
|
+
backgroundColor: 'rgba(0,0,0,0.38)',
|
|
286
251
|
},
|
|
287
|
-
|
|
288
|
-
|
|
252
|
+
// ── Top bar ───────────────────────────────────────────────────────────────
|
|
253
|
+
topBar: {
|
|
254
|
+
position: 'absolute',
|
|
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,
|
|
298
291
|
},
|
|
299
|
-
|
|
292
|
+
livePill: {
|
|
293
|
+
backgroundColor: '#ef4444',
|
|
294
|
+
borderRadius: 6,
|
|
295
|
+
paddingHorizontal: 7,
|
|
296
|
+
paddingVertical: 3,
|
|
297
|
+
flexShrink: 0,
|
|
298
|
+
},
|
|
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,
|
|
421
|
+
},
|
|
422
|
+
inputIconGlyph: {
|
|
423
|
+
fontSize: 21,
|
|
362
424
|
},
|
|
363
|
-
|
|
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,
|
|
369
432
|
},
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
433
|
+
sendBtn: {
|
|
434
|
+
width: 40,
|
|
435
|
+
height: 40,
|
|
436
|
+
borderRadius: 20,
|
|
437
|
+
backgroundColor: '#f97316',
|
|
438
|
+
alignItems: 'center',
|
|
439
|
+
justifyContent: 'center',
|
|
373
440
|
},
|
|
374
|
-
|
|
375
|
-
|
|
441
|
+
sendBtnOff: {
|
|
442
|
+
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
376
443
|
},
|
|
377
|
-
|
|
378
|
-
color: '#
|
|
379
|
-
fontSize:
|
|
444
|
+
sendIcon: {
|
|
445
|
+
color: '#fff',
|
|
446
|
+
fontSize: 15,
|
|
380
447
|
fontWeight: '700',
|
|
381
448
|
},
|
|
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,
|
|
464
|
+
fontWeight: '700',
|
|
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