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;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;AAuDF;;GAEG;AACH,eAAO,MAAM,oBAAoB,uDA0X/B,CAAC"}
@@ -1,25 +1,42 @@
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
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
- // 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;
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; // Android handled by the OS via adjustResize
59
+ return;
47
60
  const show = Keyboard.addListener('keyboardWillShow', (e) => {
48
- Animated.timing(chatBottomAnim, {
49
- toValue: 16 + e.endCoordinates.height,
50
- duration: e.duration ?? 250,
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(chatBottomAnim, {
56
- toValue: 16,
57
- duration: e.duration ?? 250,
68
+ Animated.timing(kbOffset, {
69
+ toValue: 0,
70
+ duration: e.duration ?? 220,
58
71
  useNativeDriver: false,
59
72
  }).start();
60
73
  });
61
- return () => {
62
- show.remove();
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' ? roomState.chat : null;
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' ? msg.timestamp : now - (seededFromRoomState.length - i) * 1000;
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 — receives AAC chunks from host during screen sharing)
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
- const idx = Math.abs(hash) % CHAT_NAME_COLORS.length;
206
- return CHAT_NAME_COLORS[idx];
206
+ return CHAT_NAME_COLORS[Math.abs(hash) % CHAT_NAME_COLORS.length];
207
207
  };
208
- const sendChat = async () => {
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
- 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" }) }))] }));
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: '#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',
226
+ backgroundColor: '#000',
240
227
  },
228
+ // ── Video ─────────────────────────────────────────────────────────────────
241
229
  rtcView: {
242
- ...StyleSheet.absoluteFillObject,
243
- backgroundColor: '#0a0a0a',
230
+ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
231
+ backgroundColor: '#000',
244
232
  },
245
233
  videoPlaceholder: {
246
- ...StyleSheet.absoluteFillObject,
234
+ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
247
235
  alignItems: 'center',
248
236
  justifyContent: 'center',
249
- backgroundColor: '#1a1a1a',
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
- liveBadge: {
240
+ placeholderError: {
260
241
  color: '#ef4444',
261
- fontSize: 14,
262
- fontWeight: '700',
263
- letterSpacing: 1,
264
- marginBottom: 8,
242
+ fontSize: 13,
243
+ textAlign: 'center',
244
+ paddingHorizontal: 28,
265
245
  },
266
- hintText: {
267
- color: '#737373',
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
- warnText: {
273
- color: '#f59e0b',
274
- marginTop: 8,
275
- paddingHorizontal: 16,
276
- },
277
- overlay: {
252
+ // ── Top bar ───────────────────────────────────────────────────────────────
253
+ topBar: {
278
254
  position: 'absolute',
279
- top: 0,
280
- left: 0,
281
- right: 0,
282
- bottom: 0,
283
- padding: 16,
284
- paddingBottom: 32,
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
- 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,
291
+ },
292
+ livePill: {
293
+ backgroundColor: '#ef4444',
294
+ borderRadius: 6,
295
+ paddingHorizontal: 7,
296
+ paddingVertical: 3,
297
+ flexShrink: 0,
298
298
  },
299
- stats: {
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,
362
421
  },
363
- chatInput: {
422
+ inputIconGlyph: {
423
+ fontSize: 21,
424
+ },
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,
432
+ },
433
+ sendBtn: {
434
+ width: 40,
435
+ height: 40,
436
+ borderRadius: 20,
437
+ backgroundColor: '#f97316',
438
+ alignItems: 'center',
439
+ justifyContent: 'center',
369
440
  },
370
- chatSendButton: {
371
- paddingHorizontal: 14,
372
- paddingVertical: 10,
441
+ sendBtnOff: {
442
+ backgroundColor: 'rgba(255,255,255,0.1)',
373
443
  },
374
- chatSendButtonDisabled: {
375
- opacity: 0.5,
444
+ sendIcon: {
445
+ color: '#fff',
446
+ fontSize: 15,
447
+ fontWeight: '700',
376
448
  },
377
- chatSendText: {
378
- color: '#f97316',
379
- fontSize: 18,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasunk99-livestream-core",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
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",