kasunk99-livestream-core 0.3.17 → 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,uDAme/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,93 +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
- const baseCellH = useRef(0); // largest height seen = keyboard-free baseline
47
- const currCellH = useRef(0); // current cell height (may shrink with adjustResize)
48
- const onCellLayout = useCallback((e) => {
49
- const h = Math.round(e.nativeEvent.layout.height);
50
- currCellH.current = h;
51
- if (h > baseCellH.current)
52
- baseCellH.current = h;
53
- }, []);
54
- // Animated values
55
- const inputBottom = useRef(new Animated.Value(0)).current;
56
- const chatAnim = useRef(new Animated.Value(CHAT_BOTTOM_DEFAULT)).current;
57
- // Remember last offset so re-open positions instantly instead of jumping
58
- const lastOffsetRef = useRef(0);
59
- // Compute the correct bottom offset for the floating input.
60
- // If adjustResize has shrunk the cell (shrink > 0), subtract that from keyboard height
61
- // so we don't double-offset. If cell hasn't shrunk, use full keyboard height.
62
- const computeOffset = useCallback((kbH) => {
63
- const base = baseCellH.current || SCREEN_HEIGHT;
64
- const cur = currCellH.current || base;
65
- const shrink = Math.max(0, base - cur);
66
- return Math.max(BOTTOM_SAFE, kbH - shrink);
67
- }, []);
68
- // Pre-position the input immediately when typing starts (uses last known offset)
69
- useEffect(() => {
70
- if (isTyping && lastOffsetRef.current > 0) {
71
- inputBottom.setValue(lastOffsetRef.current);
72
- chatAnim.setValue(lastOffsetRef.current + 60);
73
- }
74
- else if (!isTyping) {
75
- // Only reset if keyboard is not currently shown (let keyboard events handle active state)
76
- inputBottom.setValue(0);
77
- chatAnim.setValue(CHAT_BOTTOM_DEFAULT);
78
- }
79
- }, [isTyping, inputBottom, chatAnim]);
80
- // Keyboard event listeners — platform-specific
43
+ // When keyboard hides (back button, swipe, or after Keyboard.dismiss()), close the input panel.
81
44
  useEffect(() => {
82
- if (Platform.OS === 'android') {
83
- // keyboardDidShow fires AFTER keyboard animation + AFTER adjustResize layout.
84
- // currCellH.current has the post-resize value at this point.
85
- const s1 = Keyboard.addListener('keyboardDidShow', (e) => {
86
- const offset = computeOffset(e.endCoordinates.height);
87
- lastOffsetRef.current = offset;
88
- // Short animation so input slides into position rather than hard-jumping
89
- Animated.parallel([
90
- Animated.timing(inputBottom, { toValue: offset, duration: 120, useNativeDriver: false }),
91
- Animated.timing(chatAnim, { toValue: offset + 60, duration: 120, useNativeDriver: false }),
92
- ]).start();
93
- });
94
- const s2 = Keyboard.addListener('keyboardDidHide', () => {
95
- // Only close typing if it wasn't already closed by sendChat
96
- setIsTyping((prev) => {
97
- if (prev) {
98
- inputBottom.setValue(0);
99
- chatAnim.setValue(CHAT_BOTTOM_DEFAULT);
100
- }
101
- return false;
102
- });
103
- });
104
- return () => { s1.remove(); s2.remove(); };
105
- }
106
- // iOS — keyboardWillShow fires before keyboard so we can animate in sync
107
- const s1 = Keyboard.addListener('keyboardWillShow', (e) => {
108
- const offset = computeOffset(e.endCoordinates.height);
109
- lastOffsetRef.current = offset;
110
- Animated.parallel([
111
- Animated.timing(inputBottom, { toValue: offset, duration: e.duration || 250, useNativeDriver: false }),
112
- Animated.timing(chatAnim, { toValue: offset + 60, duration: e.duration || 250, useNativeDriver: false }),
113
- ]).start();
114
- });
115
- const s2 = Keyboard.addListener('keyboardWillHide', (e) => {
116
- Animated.parallel([
117
- Animated.timing(inputBottom, { toValue: 0, duration: e.duration || 250, useNativeDriver: false }),
118
- Animated.timing(chatAnim, { toValue: CHAT_BOTTOM_DEFAULT, duration: e.duration || 250, useNativeDriver: false }),
119
- ]).start(() => setIsTyping(false));
120
- });
121
- return () => { s1.remove(); s2.remove(); };
122
- }, [inputBottom, chatAnim, computeOffset]);
123
- // 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)
124
49
  useEffect(() => {
125
50
  if (!isActive) {
126
51
  Keyboard.dismiss();
@@ -140,8 +65,6 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
140
65
  setChatInput('');
141
66
  setIsTyping(false);
142
67
  Keyboard.dismiss();
143
- inputBottom.setValue(0);
144
- chatAnim.setValue(CHAT_BOTTOM_DEFAULT);
145
68
  return;
146
69
  }
147
70
  if (seededRoomRef.current !== roomId) {
@@ -149,7 +72,7 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
149
72
  setChatMessages([]);
150
73
  setChatInput('');
151
74
  }
152
- }, [roomId, inputBottom, chatAnim]);
75
+ }, [roomId]);
153
76
  useEffect(() => {
154
77
  if (!roomId)
155
78
  return;
@@ -264,21 +187,25 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
264
187
  const text = chatInput.trim();
265
188
  if (!text || !socket || !joined)
266
189
  return;
267
- // Clear input, close panel, dismiss keyboard all before the emit
268
- // so the button press completes synchronously before any state teardown.
190
+ // Emit first state teardown must not precede the network call.
191
+ socket.emit('chat-message', { text });
269
192
  setChatInput('');
270
- setIsTyping(false);
271
- Keyboard.dismiss();
272
- socket.emit('chat-message', { text }, (res) => {
273
- if (res?.error)
274
- console.log('[viewer] chat send error', res.error);
275
- });
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.17",
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",