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;AAe1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAK/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,uDAgX/B,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, Fragment as _Fragment } from "react/jsx-runtime";
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 { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
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 (e.g. Expo Go)
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
- // Animated bottom offset for the chat panel.
39
- // iOS: keyboard fires willShow/willHide with exact duration — we mirror that
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; // Android handled by the OS via adjustResize
42
+ return;
47
43
  const show = Keyboard.addListener('keyboardWillShow', (e) => {
48
- Animated.timing(chatBottomAnim, {
49
- toValue: 16 + e.endCoordinates.height,
50
- duration: e.duration ?? 250,
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(chatBottomAnim, {
56
- toValue: 16,
57
- duration: e.duration ?? 250,
51
+ Animated.timing(kbOffset, {
52
+ toValue: 0,
53
+ duration: e.duration ?? 220,
58
54
  useNativeDriver: false,
59
55
  }).start();
60
56
  });
61
- return () => {
62
- show.remove();
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' ? roomState.chat : null;
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' ? msg.timestamp : now - (seededFromRoomState.length - i) * 1000;
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 — receives AAC chunks from host during screen sharing)
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
- const idx = Math.abs(hash) % CHAT_NAME_COLORS.length;
206
- return CHAT_NAME_COLORS[idx];
189
+ return CHAT_NAME_COLORS[Math.abs(hash) % CHAT_NAME_COLORS.length];
207
190
  };
208
- const sendChat = async () => {
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
- 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(Text, { style: styles.placeholderText, children: "Joining..." }), error && _jsx(Text, { style: [styles.placeholderText, styles.errorText], children: error }), joined && !joining && !error && (_jsxs(_Fragment, { children: [_jsx(Text, { style: styles.liveBadge, children: "LIVE" }), _jsx(Text, { style: styles.placeholderText, children: stream.roomId }), hasVideo && webrtcUnavailable && (_jsxs(Text, { style: [styles.hintText, styles.warnText], children: ["Video needs a development build.", '\n', "Run: npx expo run:android"] })), hasVideo && consumeError && (_jsx(Text, { style: [styles.hintText, styles.errorText], children: consumeError })), hasVideo && !remoteStream && !webrtcUnavailable && !consumeError && (_jsx(Text, { style: styles.hintText, children: "Loading video..." })), !hasVideo && joined && producerList.length === 0 && (_jsxs(Text, { style: [styles.hintText, styles.warnText], children: ["Host has not started camera yet.", '\n', "Use a web client to stream, or wait for host to produce."] })), hasVideo && remoteStream && !RTCViewComponent && (_jsx(Text, { style: styles.hintText, children: "Video received (use dev build to see it)" })), hasVideo && remoteStream && RTCViewComponent && !hasVideoTrack && (_jsx(Text, { style: [styles.hintText, styles.warnText], children: "Audio only (no video track yet)" })), hasVideo && remoteStream && hasVideoTrack && !streamURL && (_jsx(Text, { style: [styles.hintText, styles.warnText], children: "Video track received but cannot display (need dev build with react-native-webrtc)" }))] }))] })), _jsxs(View, { style: styles.overlay, children: [_jsxs(View, { style: styles.topMeta, children: [_jsx(View, { style: styles.row, children: _jsxs(Text, { style: styles.roomId, numberOfLines: 1, children: ["LIVE \u00B7 ", stream.hostDisplayName || stream.title || stream.roomId] }) }), _jsx(View, { style: styles.stats, children: _jsxs(Text, { style: styles.statsText, children: ["\uD83D\uDC41 ", viewerCount] }) })] }), _jsx(Animated.View, { style: [styles.chatPanel, { bottom: chatBottomAnim }], pointerEvents: "box-none", children: _jsxs(ScrollView, { scrollEnabled: false, keyboardShouldPersistTaps: "always", contentContainerStyle: styles.chatStack, children: [_jsx(View, { style: styles.chatListWrapper, children: _jsx(ScrollView, { ref: chatListRef, contentContainerStyle: styles.chatListContent, showsVerticalScrollIndicator: false, keyboardDismissMode: "on-drag", keyboardShouldPersistTaps: "always", nestedScrollEnabled: true, children: chatData.map((item) => (_jsxs(Text, { style: styles.chatLine, numberOfLines: 2, children: [_jsx(Text, { style: [styles.chatName, { color: getNameColor(item.displayName) }], children: item.displayName }), _jsxs(Text, { style: styles.chatText, children: [" ", item.text] })] }, item.id))) }) }), _jsxs(View, { style: styles.chatInputRow, children: [_jsx(TextInput, { style: styles.chatInput, value: chatInput, onChangeText: setChatInput, placeholder: joined ? 'Say something...' : 'Connecting...', placeholderTextColor: "#9ca3af", editable: joined && !joining && !error, onSubmitEditing: sendChat, returnKeyType: "send" }), _jsx(TouchableOpacity, { onPress: sendChat, disabled: !joined || joining || !!error || !chatInput.trim(), style: [
221
- styles.chatSendButton,
222
- (!joined || joining || !!error || !chatInput.trim()) && styles.chatSendButtonDisabled,
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: '#0a0a0a',
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
- ...StyleSheet.absoluteFillObject,
243
- backgroundColor: '#0a0a0a',
213
+ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
214
+ backgroundColor: '#000',
244
215
  },
245
216
  videoPlaceholder: {
246
- ...StyleSheet.absoluteFillObject,
217
+ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
247
218
  alignItems: 'center',
248
219
  justifyContent: 'center',
249
- backgroundColor: '#1a1a1a',
220
+ backgroundColor: '#0d0d0d',
221
+ gap: 12,
250
222
  },
251
- placeholderText: {
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
- liveBadge: {
260
- color: '#ef4444',
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
- warnText: {
273
- color: '#f59e0b',
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
- padding: 16,
284
- paddingBottom: 32,
285
- backgroundColor: 'transparent',
247
+ left: 0,
248
+ right: 0,
249
+ height: 260,
250
+ backgroundColor: 'rgba(0,0,0,0.38)',
286
251
  },
287
- topMeta: {
288
- marginTop: 6,
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
- row: {
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
- roomId: {
295
- color: '#fafafa',
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
- stats: {
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
- marginTop: 4,
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
- statsText: {
305
- color: '#a3a3a3',
315
+ viewerEye: { fontSize: 11 },
316
+ viewerCount: {
317
+ color: 'rgba(255,255,255,0.88)',
306
318
  fontSize: 12,
319
+ fontWeight: '500',
307
320
  },
308
- statsTextSpacer: {
309
- marginLeft: 8,
310
- },
311
- chatPanel: {
321
+ // ── Right action column ───────────────────────────────────────────────────
322
+ rightColumn: {
312
323
  position: 'absolute',
313
- left: 16,
314
- right: 16,
315
- // bottom is driven by chatBottomAnim (starts at 16, follows keyboard on iOS)
324
+ right: 12,
325
+ bottom: 92,
326
+ alignItems: 'center',
327
+ gap: 14,
316
328
  },
317
- chatStack: {
318
- justifyContent: 'flex-end',
329
+ actionBtn: {
330
+ alignItems: 'center',
331
+ gap: 5,
319
332
  },
320
- chatListWrapper: {
321
- // Limit chat area to below mid-screen (roughly bottom 1/3–1/2 of stream).
322
- maxHeight: Math.round(SCREEN_HEIGHT * 0.35),
323
- borderRadius: 14,
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
- paddingTop: 12,
326
- paddingBottom: 4,
327
- // Subtle transparent background so chat is readable over video.
328
- backgroundColor: 'rgba(0,0,0,0.35)',
329
- borderWidth: 1,
330
- borderColor: 'rgba(255,255,255,0.08)',
331
- // Wrap chats in a narrower column like TikTok.
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
- width: '70%',
334
- overflow: 'hidden',
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
- chatListContent: {
337
- paddingBottom: 4,
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
- chatName: {
346
- color: '#fafafa',
391
+ chatUsername: {
347
392
  fontWeight: '700',
348
393
  },
349
- chatText: {
394
+ chatMsg: {
350
395
  color: '#e5e5e5',
351
396
  fontWeight: '400',
352
397
  },
353
- chatInputRow: {
354
- marginTop: 10,
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
- overflow: 'hidden',
359
- borderWidth: 1,
409
+ borderWidth: StyleSheet.hairlineWidth,
360
410
  borderColor: 'rgba(255,255,255,0.12)',
361
- backgroundColor: 'rgba(0,0,0,0.45)',
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
- chatInput: {
425
+ textInput: {
364
426
  flex: 1,
365
- paddingHorizontal: 14,
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
- chatSendButton: {
371
- paddingHorizontal: 14,
372
- paddingVertical: 10,
433
+ sendBtn: {
434
+ width: 40,
435
+ height: 40,
436
+ borderRadius: 20,
437
+ backgroundColor: '#f97316',
438
+ alignItems: 'center',
439
+ justifyContent: 'center',
373
440
  },
374
- chatSendButtonDisabled: {
375
- opacity: 0.5,
441
+ sendBtnOff: {
442
+ backgroundColor: 'rgba(255,255,255,0.1)',
376
443
  },
377
- chatSendText: {
378
- color: '#f97316',
379
- fontSize: 18,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasunk99-livestream-core",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "Reusable livestream viewer/host module for React Native (Expo) — mediasoup + Socket.IO",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",