kasunk99-livestream-core 0.3.18 → 0.3.19

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,KAAkE,MAAM,OAAO,CAAC;AAkBvF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAQ/C,KAAK,yBAAyB,GAAG;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAwBF,eAAO,MAAM,oBAAoB,uDAqe/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;AAM/C,KAAK,yBAAyB,GAAG;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAwBF,eAAO,MAAM,oBAAoB,uDAoZ/B,CAAC"}
@@ -1,12 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { ActivityIndicator, Animated, Dimensions, Keyboard, NativeModules, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
2
+ import { memo, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { ActivityIndicator, Keyboard, KeyboardAvoidingView, NativeModules, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
4
4
  import { useViewerSocket } from '../hooks/useViewerSocket';
5
- const SCREEN_HEIGHT = Dimensions.get('window').height;
6
5
  const CHAT_NAME_COLORS = ['#f97316', '#22c55e', '#3b82f6', '#eab308', '#ec4899', '#a855f7'];
7
6
  const BOTTOM_SAFE = Platform.OS === 'ios' ? 28 : 10;
8
7
  const BOTTOM_BAR_H = 58;
9
- const CHAT_BOTTOM_DEFAULT = BOTTOM_SAFE + BOTTOM_BAR_H + 8;
10
8
  let RTCViewComponent = null;
11
9
  try {
12
10
  const webrtc = require('react-native-webrtc');
@@ -34,97 +32,20 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
34
32
  ? displayStreamObj.toURL()
35
33
  : undefined;
36
34
  const showVideo = RTCViewComponent && remoteStream && !!streamURL && hasVideoTrack;
37
- // chatInput persists when keyboard closes without sending cleared only on send
35
+ // chatInput persists when keyboard is dismissed without sending (cleared only on send)
38
36
  const [chatInput, setChatInput] = useState('');
39
37
  const [chatMessages, setChatMessages] = useState([]);
40
- // isTyping: user has opened the input panel
38
+ // isTyping controls whether we show the TextInput bar or the pill bar
41
39
  const [isTyping, setIsTyping] = useState(false);
42
40
  const seededRoomRef = useRef(null);
43
41
  const chatListRef = useRef(null);
44
42
  const textInputRef = useRef(null);
45
- // Cell layout tracking used to compensate for adjustResize shrinking the cell.
46
- // Initialise baseCellH with the same estimate LiveStreamFeed uses for viewportHeight
47
- // so the gap formula works correctly even before onCellLayout fires.
48
- const baseCellH = useRef(SCREEN_HEIGHT - 120);
49
- const currCellH = useRef(SCREEN_HEIGHT - 120);
50
- const onCellLayout = useCallback((e) => {
51
- const h = Math.round(e.nativeEvent.layout.height);
52
- currCellH.current = h;
53
- if (h > baseCellH.current)
54
- baseCellH.current = h;
55
- }, []);
56
- // Animated values
57
- const inputBottom = useRef(new Animated.Value(0)).current;
58
- const chatAnim = useRef(new Animated.Value(CHAT_BOTTOM_DEFAULT)).current;
59
- // Remember last offset so re-open positions instantly instead of jumping
60
- const lastOffsetRef = useRef(0);
61
- // Compute the correct bottom offset for the floating input.
62
- //
63
- // The cell sits inside a tab navigator whose height is less than the full window.
64
- // `kbH` from the keyboard event is measured from the bottom of the WINDOW, not the
65
- // bottom of the CELL. We subtract `cellBottomGap` (window bottom → cell bottom) to
66
- // get the offset relative to the cell.
67
- //
68
- // If adjustResize has also shrunk the cell, we subtract that too (avoids double-offset).
69
- const computeOffset = useCallback((kbH) => {
70
- const base = baseCellH.current;
71
- const cur = currCellH.current;
72
- const cellBottomGap = Math.max(0, SCREEN_HEIGHT - base); // tab-nav area below cell
73
- const shrink = Math.max(0, base - cur); // adjustResize shrink (if any)
74
- return Math.max(BOTTOM_SAFE, kbH - cellBottomGap - shrink);
75
- }, []);
76
- // Pre-position the input immediately when typing starts (uses last known offset).
77
- // Keyboard listeners own the reset when isTyping becomes false.
78
- useEffect(() => {
79
- if (isTyping && lastOffsetRef.current > 0) {
80
- inputBottom.setValue(lastOffsetRef.current);
81
- chatAnim.setValue(lastOffsetRef.current + 60);
82
- }
83
- }, [isTyping, inputBottom, chatAnim]);
84
- // Keyboard event listeners — platform-specific
43
+ // When keyboard hides (back button, swipe, or after Keyboard.dismiss()), close the input panel.
85
44
  useEffect(() => {
86
- if (Platform.OS === 'android') {
87
- // keyboardDidShow fires AFTER keyboard animation + AFTER adjustResize layout.
88
- // currCellH.current has the post-resize value at this point.
89
- const s1 = Keyboard.addListener('keyboardDidShow', (e) => {
90
- const offset = computeOffset(e.endCoordinates.height);
91
- lastOffsetRef.current = offset;
92
- // Short animation so input slides into position rather than hard-jumping
93
- Animated.parallel([
94
- Animated.timing(inputBottom, { toValue: offset, duration: 120, useNativeDriver: false }),
95
- Animated.timing(chatAnim, { toValue: offset + 60, duration: 120, useNativeDriver: false }),
96
- ]).start();
97
- });
98
- const s2 = Keyboard.addListener('keyboardDidHide', () => {
99
- // Only close typing if it wasn't already closed by sendChat
100
- setIsTyping((prev) => {
101
- if (prev) {
102
- inputBottom.setValue(0);
103
- chatAnim.setValue(CHAT_BOTTOM_DEFAULT);
104
- }
105
- return false;
106
- });
107
- });
108
- return () => { s1.remove(); s2.remove(); };
109
- }
110
- // iOS — keyboardWillShow fires before keyboard so we can animate in sync
111
- const s1 = Keyboard.addListener('keyboardWillShow', (e) => {
112
- const offset = computeOffset(e.endCoordinates.height);
113
- lastOffsetRef.current = offset;
114
- Animated.parallel([
115
- Animated.timing(inputBottom, { toValue: offset, duration: e.duration || 250, useNativeDriver: false }),
116
- Animated.timing(chatAnim, { toValue: offset + 60, duration: e.duration || 250, useNativeDriver: false }),
117
- ]).start();
118
- });
119
- const s2 = Keyboard.addListener('keyboardWillHide', (e) => {
120
- Animated.parallel([
121
- Animated.timing(inputBottom, { toValue: 0, duration: e.duration || 250, useNativeDriver: false }),
122
- Animated.timing(chatAnim, { toValue: CHAT_BOTTOM_DEFAULT, duration: e.duration || 250, useNativeDriver: false }),
123
- ]).start(() => setIsTyping(false));
124
- });
125
- return () => { s1.remove(); s2.remove(); };
126
- }, [inputBottom, chatAnim, computeOffset]);
127
- // Dismiss keyboard when stream becomes inactive
45
+ const sub = Keyboard.addListener('keyboardDidHide', () => setIsTyping(false));
46
+ return () => sub.remove();
47
+ }, []);
48
+ // Dismiss input when stream becomes inactive (e.g. user swipes to next stream)
128
49
  useEffect(() => {
129
50
  if (!isActive) {
130
51
  Keyboard.dismiss();
@@ -144,8 +65,6 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
144
65
  setChatInput('');
145
66
  setIsTyping(false);
146
67
  Keyboard.dismiss();
147
- inputBottom.setValue(0);
148
- chatAnim.setValue(CHAT_BOTTOM_DEFAULT);
149
68
  return;
150
69
  }
151
70
  if (seededRoomRef.current !== roomId) {
@@ -153,7 +72,7 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
153
72
  setChatMessages([]);
154
73
  setChatInput('');
155
74
  }
156
- }, [roomId, inputBottom, chatAnim]);
75
+ }, [roomId]);
157
76
  useEffect(() => {
158
77
  if (!roomId)
159
78
  return;
@@ -268,17 +187,25 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
268
187
  const text = chatInput.trim();
269
188
  if (!text || !socket || !joined)
270
189
  return;
271
- // Emit FIRST so the message reaches the server before any cleanup.
272
- // keyboardDidHide will set isTyping=false and reset animations.
190
+ // Emit first state teardown must not precede the network call.
273
191
  socket.emit('chat-message', { text });
274
192
  setChatInput('');
275
- Keyboard.dismiss();
193
+ Keyboard.dismiss(); // triggers keyboardDidHide → setIsTyping(false)
276
194
  };
277
195
  const hostLabel = stream.hostDisplayName || stream.title || 'Live';
278
196
  const avatarLetter = hostLabel[0]?.toUpperCase() ?? 'L';
279
197
  const hasUnsent = chatInput.trim().length > 0;
280
- return (_jsxs(View, { style: styles.container, onLayout: onCellLayout, 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 }))] })), LinearGradient ? (_jsx(LinearGradient, { colors: ['rgba(0,0,0,0.70)', 'rgba(0,0,0,0.0)', 'rgba(0,0,0,0.0)', 'rgba(0,0,0,0.78)'], locations: [0, 0.28, 0.48, 1], style: styles.gradientOverlay, pointerEvents: "none" })) : null, _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() : '—' })] })] }), !isTyping && (_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" })] })] })), _jsx(Animated.View, { style: [styles.chatColumn, { bottom: chatAnim }], pointerEvents: "box-none", children: _jsx(ScrollView, { ref: chatListRef, contentContainerStyle: styles.chatContent, showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "always", 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))) }) }), !isTyping && (_jsxs(View, { style: styles.bottomBar, children: [_jsx(TouchableOpacity, { style: [styles.typeTouchable, hasUnsent && styles.typeTouchableFilled], onPress: () => { if (joined && !joining && !error)
281
- setIsTyping(true); }, activeOpacity: 0.7, children: _jsx(Text, { style: [styles.typePlaceholder, hasUnsent && styles.typePlaceholderFilled], numberOfLines: 1, children: hasUnsent ? chatInput : (joined && !joining ? 'Say something...' : 'Connecting...') }) }), _jsx(TouchableOpacity, { style: styles.bottomIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.bottomIconGlyph, children: "\u263A" }) }), _jsx(TouchableOpacity, { style: styles.bottomIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.bottomIconGlyph, children: "\uD83C\uDF39" }) }), _jsx(TouchableOpacity, { style: styles.bottomIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.bottomIconGlyph, children: "\uD83C\uDF81" }) }), _jsx(TouchableOpacity, { style: styles.bottomIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.bottomIconGlyph, children: "\u2197" }) })] })), isTyping && (_jsxs(Animated.View, { style: [styles.floatingInputWrap, { bottom: inputBottom }], children: [_jsx(TextInput, { ref: textInputRef, style: styles.floatingTextInput, value: chatInput, onChangeText: setChatInput, placeholder: "Say something...", placeholderTextColor: "rgba(255,255,255,0.40)", editable: joined && !joining && !error, onSubmitEditing: sendChat, returnKeyType: "send", submitBehavior: "submit", autoFocus: true }), _jsx(TouchableOpacity, { style: [styles.floatingSendBtn, (!hasUnsent || !joined) && styles.sendBtnOff], onPress: sendChat, disabled: !joined || !hasUnsent, activeOpacity: 0.75, 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" })] }) }))] }));
198
+ const canType = joined && !joining && !error;
199
+ return (
200
+ // KeyboardAvoidingView is the root. It measures its own screen rect, computes the exact
201
+ // overlap with the system keyboard, and shrinks its content area accordingly.
202
+ // No manual offset formulas or layout listeners are needed.
203
+ _jsxs(KeyboardAvoidingView, { style: styles.container, behavior: Platform.OS === 'ios' ? 'padding' : 'height', children: [showVideo && RTCViewComponent && streamURL ? (_jsx(RTCViewComponent, { streamURL: streamURL, stream: displayStream, style: StyleSheet.absoluteFillObject, objectFit: "cover", mirror: false }, `rtc-${trackCount}-${streamURL}`)) : (_jsxs(View, { style: [StyleSheet.absoluteFillObject, 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 }))] })), LinearGradient ? (_jsx(LinearGradient, { colors: ['rgba(0,0,0,0.70)', 'rgba(0,0,0,0.0)', 'rgba(0,0,0,0.0)', 'rgba(0,0,0,0.78)'], locations: [0, 0.28, 0.48, 1], style: StyleSheet.absoluteFillObject, pointerEvents: "none" })) : null, _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() : '—' })] })] }), !isTyping && (_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" })] })] })), _jsx(View, { style: styles.spacer, pointerEvents: "none" }), _jsx(ScrollView, { ref: chatListRef, style: styles.chatList, contentContainerStyle: styles.chatContent, showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "always", 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))) }), isTyping ? (
204
+ // Input bar — KAV lifts this to sit flush against the keyboard top.
205
+ _jsxs(View, { style: styles.inputBar, children: [_jsx(TextInput, { ref: textInputRef, style: styles.textInput, value: chatInput, onChangeText: setChatInput, placeholder: "Say something...", placeholderTextColor: "rgba(255,255,255,0.40)", editable: canType, onSubmitEditing: sendChat, returnKeyType: "send", submitBehavior: "submit", autoFocus: true }), _jsx(TouchableOpacity, { style: [styles.sendBtn, (!hasUnsent || !joined) && styles.sendBtnOff], onPress: sendChat, disabled: !joined || !hasUnsent, activeOpacity: 0.75, children: _jsx(Text, { style: styles.sendIcon, children: "\u25B6" }) })] })) : (
206
+ // Pill bar — tapping opens the input bar.
207
+ _jsxs(View, { style: styles.bottomBar, children: [_jsx(TouchableOpacity, { style: [styles.typeTouchable, hasUnsent && styles.typeTouchableFilled], onPress: () => { if (canType)
208
+ setIsTyping(true); }, activeOpacity: 0.7, children: _jsx(Text, { style: [styles.typePlaceholder, hasUnsent && styles.typePlaceholderFilled], numberOfLines: 1, children: hasUnsent ? chatInput : (joined && !joining ? 'Say something...' : 'Connecting...') }) }), _jsx(TouchableOpacity, { style: styles.bottomIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.bottomIconGlyph, children: "\u263A" }) }), _jsx(TouchableOpacity, { style: styles.bottomIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.bottomIconGlyph, children: "\uD83C\uDF39" }) }), _jsx(TouchableOpacity, { style: styles.bottomIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.bottomIconGlyph, children: "\uD83C\uDF81" }) }), _jsx(TouchableOpacity, { style: styles.bottomIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.bottomIconGlyph, children: "\u2197" }) })] })), 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" })] }) }))] }));
282
209
  });
283
210
  // ─────────────────────────────────────────────────────────────────────────────
284
211
  const styles = StyleSheet.create({
@@ -286,12 +213,7 @@ const styles = StyleSheet.create({
286
213
  flex: 1,
287
214
  backgroundColor: '#000',
288
215
  },
289
- rtcView: {
290
- position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
291
- backgroundColor: '#000',
292
- },
293
216
  videoPlaceholder: {
294
- position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
295
217
  alignItems: 'center',
296
218
  justifyContent: 'center',
297
219
  backgroundColor: '#0d0d0d',
@@ -309,9 +231,6 @@ const styles = StyleSheet.create({
309
231
  textAlign: 'center',
310
232
  paddingHorizontal: 28,
311
233
  },
312
- gradientOverlay: {
313
- position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
314
- },
315
234
  topBar: {
316
235
  position: 'absolute',
317
236
  top: 10,
@@ -382,13 +301,15 @@ const styles = StyleSheet.create({
382
301
  },
383
302
  actionIcon: { fontSize: 22, color: '#fff' },
384
303
  actionLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 11, fontWeight: '500' },
385
- chatColumn: {
386
- position: 'absolute',
387
- left: 12,
304
+ // Flex spacer — fills remaining vertical space, pushes chat + bars to bottom
305
+ spacer: { flex: 1 },
306
+ // Chat list — normal flow block, grows upward from input bar
307
+ chatList: {
308
+ marginLeft: 12,
388
309
  width: '66%',
389
- maxHeight: Math.round(SCREEN_HEIGHT * 0.30),
310
+ maxHeight: 200,
390
311
  },
391
- chatContent: { paddingBottom: 2 },
312
+ chatContent: { paddingBottom: 4 },
392
313
  chatBubble: {
393
314
  alignSelf: 'flex-start',
394
315
  marginBottom: 5,
@@ -411,53 +332,19 @@ const styles = StyleSheet.create({
411
332
  chatUsername: { fontWeight: '700' },
412
333
  chatMsg: { color: '#e5e5e5', fontWeight: '400' },
413
334
  joinText: { color: 'rgba(255,255,255,0.48)', fontSize: 12, fontStyle: 'italic' },
414
- // Bottom action bar
415
- bottomBar: {
416
- position: 'absolute',
417
- bottom: BOTTOM_SAFE,
418
- left: 12,
419
- right: 12,
420
- flexDirection: 'row',
421
- alignItems: 'center',
422
- gap: 6,
423
- },
424
- typeTouchable: {
425
- flex: 1,
426
- backgroundColor: 'rgba(255,255,255,0.12)',
427
- borderRadius: 20,
428
- paddingHorizontal: 14,
429
- paddingVertical: 10,
430
- borderWidth: StyleSheet.hairlineWidth,
431
- borderColor: 'rgba(255,255,255,0.18)',
432
- },
433
- typeTouchableFilled: {
434
- backgroundColor: 'rgba(255,255,255,0.18)',
435
- borderColor: 'rgba(255,255,255,0.30)',
436
- },
437
- typePlaceholder: {
438
- color: 'rgba(255,255,255,0.50)',
439
- fontSize: 14,
440
- },
441
- typePlaceholderFilled: {
442
- color: '#fff',
443
- },
444
- bottomIconBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center' },
445
- bottomIconGlyph: { fontSize: 22 },
446
- // Floating input — absolute, bottom driven by Animated.Value
447
- floatingInputWrap: {
448
- position: 'absolute',
449
- left: 0,
450
- right: 0,
335
+ // Input bar — normal flow, KAV lifts it above keyboard automatically
336
+ inputBar: {
451
337
  flexDirection: 'row',
452
338
  alignItems: 'center',
453
339
  backgroundColor: 'rgba(20,20,20,0.97)',
454
340
  paddingHorizontal: 12,
455
- paddingVertical: 8,
341
+ paddingTop: 8,
342
+ paddingBottom: BOTTOM_SAFE + 8,
456
343
  gap: 8,
457
344
  borderTopWidth: StyleSheet.hairlineWidth,
458
345
  borderTopColor: 'rgba(255,255,255,0.10)',
459
346
  },
460
- floatingTextInput: {
347
+ textInput: {
461
348
  flex: 1,
462
349
  backgroundColor: 'rgba(255,255,255,0.10)',
463
350
  borderRadius: 20,
@@ -467,7 +354,7 @@ const styles = StyleSheet.create({
467
354
  fontSize: 14,
468
355
  minHeight: 40,
469
356
  },
470
- floatingSendBtn: {
357
+ sendBtn: {
471
358
  width: 40,
472
359
  height: 40,
473
360
  borderRadius: 20,
@@ -477,6 +364,32 @@ const styles = StyleSheet.create({
477
364
  },
478
365
  sendBtnOff: { backgroundColor: 'rgba(255,255,255,0.12)' },
479
366
  sendIcon: { color: '#fff', fontSize: 15, fontWeight: '700' },
367
+ // Pill bar — normal flow, visible when keyboard is closed
368
+ bottomBar: {
369
+ flexDirection: 'row',
370
+ alignItems: 'center',
371
+ paddingHorizontal: 12,
372
+ paddingBottom: BOTTOM_SAFE,
373
+ paddingTop: 8,
374
+ gap: 6,
375
+ },
376
+ typeTouchable: {
377
+ flex: 1,
378
+ backgroundColor: 'rgba(255,255,255,0.12)',
379
+ borderRadius: 20,
380
+ paddingHorizontal: 14,
381
+ paddingVertical: 10,
382
+ borderWidth: StyleSheet.hairlineWidth,
383
+ borderColor: 'rgba(255,255,255,0.18)',
384
+ },
385
+ typeTouchableFilled: {
386
+ backgroundColor: 'rgba(255,255,255,0.18)',
387
+ borderColor: 'rgba(255,255,255,0.30)',
388
+ },
389
+ typePlaceholder: { color: 'rgba(255,255,255,0.50)', fontSize: 14 },
390
+ typePlaceholderFilled: { color: '#fff' },
391
+ bottomIconBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center' },
392
+ bottomIconGlyph: { fontSize: 22 },
480
393
  endedOverlay: {
481
394
  position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
482
395
  backgroundColor: 'rgba(0,0,0,0.80)',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasunk99-livestream-core",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
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",