kasunk99-livestream-core 0.3.18 → 0.3.20

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;AAiB1E,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,uDAob/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, StatusBar, 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,103 +32,53 @@ 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();
131
52
  setIsTyping(false);
132
53
  }
133
54
  }, [isActive]);
55
+ // On Android, override adjustResize → adjustNothing while this stream is active.
56
+ //
57
+ // Why: the FlatList cell has a fixed height (set by LiveStreamFeed's viewportHeight state).
58
+ // With adjustResize the system shrinks the window, which eventually propagates to the cell,
59
+ // but by the time KeyboardAvoidingView fires the cell height has already changed — so KAV
60
+ // sees zero overlap with the keyboard and doesn't lift the content at all.
61
+ //
62
+ // With adjustNothing the window is untouched; KAV measures the exact overlap between its
63
+ // own screen rect and the keyboard rect, reduces its height by that amount, and the input
64
+ // bar ends up flush against the keyboard top with zero manual calculation.
65
+ //
66
+ // keyboardVerticalOffset corrects for the status bar: KAV's onLayout returns a y offset
67
+ // relative to its parent (= 0, top of the cell), but keyboardFrame.screenY is absolute
68
+ // screen coords. Without the offset KAV under-lifts by exactly StatusBar.currentHeight.
69
+ useEffect(() => {
70
+ if (Platform.OS !== 'android' || !isActive)
71
+ return;
72
+ let cleanup;
73
+ try {
74
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
75
+ const rnkc = require('react-native-keyboard-controller');
76
+ rnkc.KeyboardController.setInputMode(rnkc.AndroidSoftInputModes.SOFT_INPUT_ADJUST_NOTHING);
77
+ cleanup = () => rnkc.KeyboardController.setDefaultMode();
78
+ }
79
+ catch { /* RNKC unavailable — falls back to adjustResize with partial lift */ }
80
+ return cleanup;
81
+ }, [isActive]);
134
82
  const seededFromRoomState = useMemo(() => {
135
83
  const chat = roomState && typeof roomState === 'object'
136
84
  ? roomState.chat
@@ -144,8 +92,6 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
144
92
  setChatInput('');
145
93
  setIsTyping(false);
146
94
  Keyboard.dismiss();
147
- inputBottom.setValue(0);
148
- chatAnim.setValue(CHAT_BOTTOM_DEFAULT);
149
95
  return;
150
96
  }
151
97
  if (seededRoomRef.current !== roomId) {
@@ -153,7 +99,7 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
153
99
  setChatMessages([]);
154
100
  setChatInput('');
155
101
  }
156
- }, [roomId, inputBottom, chatAnim]);
102
+ }, [roomId]);
157
103
  useEffect(() => {
158
104
  if (!roomId)
159
105
  return;
@@ -268,17 +214,25 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
268
214
  const text = chatInput.trim();
269
215
  if (!text || !socket || !joined)
270
216
  return;
271
- // Emit FIRST so the message reaches the server before any cleanup.
272
- // keyboardDidHide will set isTyping=false and reset animations.
217
+ // Emit first state teardown must not precede the network call.
273
218
  socket.emit('chat-message', { text });
274
219
  setChatInput('');
275
- Keyboard.dismiss();
220
+ Keyboard.dismiss(); // triggers keyboardDidHide → setIsTyping(false)
276
221
  };
277
222
  const hostLabel = stream.hostDisplayName || stream.title || 'Live';
278
223
  const avatarLetter = hostLabel[0]?.toUpperCase() ?? 'L';
279
224
  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" })] }) }))] }));
225
+ const canType = joined && !joining && !error;
226
+ return (
227
+ // KeyboardAvoidingView is the root. It measures its own screen rect, computes the exact
228
+ // overlap with the system keyboard, and shrinks its content area accordingly.
229
+ // No manual offset formulas or layout listeners are needed.
230
+ _jsxs(KeyboardAvoidingView, { style: styles.container, behavior: Platform.OS === 'ios' ? 'padding' : 'height', keyboardVerticalOffset: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0, 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 ? (
231
+ // Input bar — KAV lifts this to sit flush against the keyboard top.
232
+ _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" }) })] })) : (
233
+ // Pill bar — tapping opens the input bar.
234
+ _jsxs(View, { style: styles.bottomBar, children: [_jsx(TouchableOpacity, { style: [styles.typeTouchable, hasUnsent && styles.typeTouchableFilled], onPress: () => { if (canType)
235
+ 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
236
  });
283
237
  // ─────────────────────────────────────────────────────────────────────────────
284
238
  const styles = StyleSheet.create({
@@ -286,12 +240,7 @@ const styles = StyleSheet.create({
286
240
  flex: 1,
287
241
  backgroundColor: '#000',
288
242
  },
289
- rtcView: {
290
- position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
291
- backgroundColor: '#000',
292
- },
293
243
  videoPlaceholder: {
294
- position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
295
244
  alignItems: 'center',
296
245
  justifyContent: 'center',
297
246
  backgroundColor: '#0d0d0d',
@@ -309,9 +258,6 @@ const styles = StyleSheet.create({
309
258
  textAlign: 'center',
310
259
  paddingHorizontal: 28,
311
260
  },
312
- gradientOverlay: {
313
- position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
314
- },
315
261
  topBar: {
316
262
  position: 'absolute',
317
263
  top: 10,
@@ -382,13 +328,15 @@ const styles = StyleSheet.create({
382
328
  },
383
329
  actionIcon: { fontSize: 22, color: '#fff' },
384
330
  actionLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 11, fontWeight: '500' },
385
- chatColumn: {
386
- position: 'absolute',
387
- left: 12,
331
+ // Flex spacer — fills remaining vertical space, pushes chat + bars to bottom
332
+ spacer: { flex: 1 },
333
+ // Chat list — normal flow block, grows upward from input bar
334
+ chatList: {
335
+ marginLeft: 12,
388
336
  width: '66%',
389
- maxHeight: Math.round(SCREEN_HEIGHT * 0.30),
337
+ maxHeight: 200,
390
338
  },
391
- chatContent: { paddingBottom: 2 },
339
+ chatContent: { paddingBottom: 4 },
392
340
  chatBubble: {
393
341
  alignSelf: 'flex-start',
394
342
  marginBottom: 5,
@@ -411,53 +359,19 @@ const styles = StyleSheet.create({
411
359
  chatUsername: { fontWeight: '700' },
412
360
  chatMsg: { color: '#e5e5e5', fontWeight: '400' },
413
361
  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,
362
+ // Input bar — normal flow, KAV lifts it above keyboard automatically
363
+ inputBar: {
451
364
  flexDirection: 'row',
452
365
  alignItems: 'center',
453
366
  backgroundColor: 'rgba(20,20,20,0.97)',
454
367
  paddingHorizontal: 12,
455
- paddingVertical: 8,
368
+ paddingTop: 8,
369
+ paddingBottom: BOTTOM_SAFE + 8,
456
370
  gap: 8,
457
371
  borderTopWidth: StyleSheet.hairlineWidth,
458
372
  borderTopColor: 'rgba(255,255,255,0.10)',
459
373
  },
460
- floatingTextInput: {
374
+ textInput: {
461
375
  flex: 1,
462
376
  backgroundColor: 'rgba(255,255,255,0.10)',
463
377
  borderRadius: 20,
@@ -467,7 +381,7 @@ const styles = StyleSheet.create({
467
381
  fontSize: 14,
468
382
  minHeight: 40,
469
383
  },
470
- floatingSendBtn: {
384
+ sendBtn: {
471
385
  width: 40,
472
386
  height: 40,
473
387
  borderRadius: 20,
@@ -477,6 +391,32 @@ const styles = StyleSheet.create({
477
391
  },
478
392
  sendBtnOff: { backgroundColor: 'rgba(255,255,255,0.12)' },
479
393
  sendIcon: { color: '#fff', fontSize: 15, fontWeight: '700' },
394
+ // Pill bar — normal flow, visible when keyboard is closed
395
+ bottomBar: {
396
+ flexDirection: 'row',
397
+ alignItems: 'center',
398
+ paddingHorizontal: 12,
399
+ paddingBottom: BOTTOM_SAFE,
400
+ paddingTop: 8,
401
+ gap: 6,
402
+ },
403
+ typeTouchable: {
404
+ flex: 1,
405
+ backgroundColor: 'rgba(255,255,255,0.12)',
406
+ borderRadius: 20,
407
+ paddingHorizontal: 14,
408
+ paddingVertical: 10,
409
+ borderWidth: StyleSheet.hairlineWidth,
410
+ borderColor: 'rgba(255,255,255,0.18)',
411
+ },
412
+ typeTouchableFilled: {
413
+ backgroundColor: 'rgba(255,255,255,0.18)',
414
+ borderColor: 'rgba(255,255,255,0.30)',
415
+ },
416
+ typePlaceholder: { color: 'rgba(255,255,255,0.50)', fontSize: 14 },
417
+ typePlaceholderFilled: { color: '#fff' },
418
+ bottomIconBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center' },
419
+ bottomIconGlyph: { fontSize: 22 },
480
420
  endedOverlay: {
481
421
  position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
482
422
  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.20",
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",