kasunk99-livestream-core 0.3.14 → 0.3.16

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;AAiB1E,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;AAiCF,eAAO,MAAM,oBAAoB,uDA8Z/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;AAU/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,uDAub/B,CAAC"}
@@ -7,6 +7,8 @@ const CHAT_NAME_COLORS = ['#f97316', '#22c55e', '#3b82f6', '#eab308', '#ec4899',
7
7
  const BOTTOM_SAFE = Platform.OS === 'ios' ? 28 : 10;
8
8
  const BOTTOM_BAR_H = 58;
9
9
  const CHAT_BOTTOM_DEFAULT = BOTTOM_SAFE + BOTTOM_BAR_H + 8;
10
+ // Chat shifts above the floating input bar when typing
11
+ const CHAT_BOTTOM_TYPING = BOTTOM_SAFE + 68;
10
12
  let RTCViewComponent = null;
11
13
  try {
12
14
  const webrtc = require('react-native-webrtc');
@@ -18,11 +20,6 @@ try {
18
20
  LinearGradient = require('expo-linear-gradient').LinearGradient;
19
21
  }
20
22
  catch { }
21
- let RNKCEvents = null;
22
- try {
23
- RNKCEvents = require('react-native-keyboard-controller').KeyboardEvents;
24
- }
25
- catch { }
26
23
  export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream, isActive, index: _index, }) {
27
24
  const roomId = isActive ? stream.roomId : null;
28
25
  const { joined, joining, error, producerList, roomState, remoteStream, remoteVideoStream, webrtcUnavailable, consumeError, socket, viewerCount: liveViewerCount, streamEnded, } = useViewerSocket(roomId);
@@ -45,27 +42,60 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
45
42
  const seededRoomRef = useRef(null);
46
43
  const chatListRef = useRef(null);
47
44
  const textInputRef = useRef(null);
48
- // Keyboard-driven animated positions
49
- const inputBottom = useRef(new Animated.Value(0)).current;
45
+ // inputBottom: Android stays fixed at BOTTOM_SAFE (adjustResize handles the rest).
46
+ // iOS animates to keyboard height.
47
+ const inputBottom = useRef(new Animated.Value(BOTTOM_SAFE)).current;
50
48
  const chatAnim = useRef(new Animated.Value(CHAT_BOTTOM_DEFAULT)).current;
51
- // Animate floating input and chat column with keyboard
49
+ // Android: adjustResize shrinks the container when the keyboard appears, so the
50
+ // input at bottom:BOTTOM_SAFE naturally ends up above the keyboard — no height math.
51
+ // Chat column animates up when typing starts (before keyboard appears) so it clears
52
+ // the floating input bar. keyboardDidHide resets everything.
53
+ //
54
+ // iOS: adjustResize does NOT apply. Keyboard covers content. We must use
55
+ // keyboardWillShow to push both the input bar and chat above the keyboard.
52
56
  useEffect(() => {
53
- const animate = (toInput, toChat, dur, onDone) => {
54
- Animated.parallel([
55
- Animated.timing(inputBottom, { toValue: toInput, duration: dur, useNativeDriver: false }),
56
- Animated.timing(chatAnim, { toValue: toChat, duration: dur, useNativeDriver: false }),
57
- ]).start(() => onDone?.());
58
- };
59
- if (RNKCEvents) {
60
- const s1 = RNKCEvents.addListener('keyboardWillShow', (e) => animate(e.height, e.height + 60, e.duration || 250));
61
- const s2 = RNKCEvents.addListener('keyboardWillHide', (e) => animate(0, CHAT_BOTTOM_DEFAULT, e.duration || 250, () => setIsTyping(false)));
62
- return () => { s1.remove(); s2.remove(); };
57
+ if (Platform.OS === 'android') {
58
+ const sub = Keyboard.addListener('keyboardDidHide', () => {
59
+ setIsTyping(false);
60
+ chatAnim.setValue(CHAT_BOTTOM_DEFAULT);
61
+ });
62
+ return () => sub.remove();
63
63
  }
64
- // Standard Keyboard API fallback
65
- const sub1 = Keyboard.addListener((Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'), (e) => animate(e.endCoordinates.height, e.endCoordinates.height + 60, e.duration || 250));
66
- const sub2 = Keyboard.addListener((Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'), (e) => animate(0, CHAT_BOTTOM_DEFAULT, e.duration || 200, () => setIsTyping(false)));
67
- return () => { sub1.remove(); sub2.remove(); };
64
+ // iOS animate with keyboard
65
+ const s1 = Keyboard.addListener('keyboardWillShow', (e) => {
66
+ const h = e.endCoordinates.height;
67
+ const dur = e.duration || 250;
68
+ Animated.parallel([
69
+ Animated.timing(inputBottom, { toValue: h, duration: dur, useNativeDriver: false }),
70
+ Animated.timing(chatAnim, { toValue: h + 60, duration: dur, useNativeDriver: false }),
71
+ ]).start();
72
+ });
73
+ const s2 = Keyboard.addListener('keyboardWillHide', (e) => {
74
+ const dur = e.duration || 250;
75
+ Animated.parallel([
76
+ Animated.timing(inputBottom, { toValue: BOTTOM_SAFE, duration: dur, useNativeDriver: false }),
77
+ Animated.timing(chatAnim, { toValue: CHAT_BOTTOM_DEFAULT, duration: dur, useNativeDriver: false }),
78
+ ]).start(() => setIsTyping(false));
79
+ });
80
+ return () => { s1.remove(); s2.remove(); };
68
81
  }, [inputBottom, chatAnim]);
82
+ // Animate chat up immediately when isTyping flips (Android: before keyboard appears)
83
+ useEffect(() => {
84
+ if (Platform.OS !== 'android')
85
+ return;
86
+ Animated.timing(chatAnim, {
87
+ toValue: isTyping ? CHAT_BOTTOM_TYPING : CHAT_BOTTOM_DEFAULT,
88
+ duration: 200,
89
+ useNativeDriver: false,
90
+ }).start();
91
+ }, [isTyping, chatAnim]);
92
+ // Dismiss keyboard and reset when stream becomes inactive
93
+ useEffect(() => {
94
+ if (!isActive) {
95
+ Keyboard.dismiss();
96
+ setIsTyping(false);
97
+ }
98
+ }, [isActive]);
69
99
  const seededFromRoomState = useMemo(() => {
70
100
  const chat = roomState && typeof roomState === 'object'
71
101
  ? roomState.chat
@@ -79,7 +109,7 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
79
109
  setChatInput('');
80
110
  setIsTyping(false);
81
111
  Keyboard.dismiss();
82
- inputBottom.setValue(0);
112
+ inputBottom.setValue(BOTTOM_SAFE);
83
113
  chatAnim.setValue(CHAT_BOTTOM_DEFAULT);
84
114
  return;
85
115
  }
@@ -211,7 +241,7 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
211
241
  };
212
242
  const hostLabel = stream.hostDisplayName || stream.title || 'Live';
213
243
  const avatarLetter = hostLabel[0]?.toUpperCase() ?? 'L';
214
- 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 }))] })), 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", keyboardDismissMode: "on-drag", 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, onPress: () => { if (joined && !joining && !error)
244
+ 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 }))] })), 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, onPress: () => { if (joined && !joining && !error)
215
245
  setIsTyping(true); }, activeOpacity: 0.7, children: _jsx(Text, { style: styles.typePlaceholder, children: 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, (!chatInput.trim() || !joined) && styles.sendBtnOff], onPress: sendChat, disabled: !joined || !chatInput.trim(), 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" })] }) }))] }));
216
246
  });
217
247
  // ─────────────────────────────────────────────────────────────────────────────
@@ -316,7 +346,6 @@ const styles = StyleSheet.create({
316
346
  },
317
347
  actionIcon: { fontSize: 22, color: '#fff' },
318
348
  actionLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 11, fontWeight: '500' },
319
- // Chat column — absolute, bottom driven by animation
320
349
  chatColumn: {
321
350
  position: 'absolute',
322
351
  left: 12,
@@ -346,7 +375,6 @@ const styles = StyleSheet.create({
346
375
  chatUsername: { fontWeight: '700' },
347
376
  chatMsg: { color: '#e5e5e5', fontWeight: '400' },
348
377
  joinText: { color: 'rgba(255,255,255,0.48)', fontSize: 12, fontStyle: 'italic' },
349
- // Bottom action bar
350
378
  bottomBar: {
351
379
  position: 'absolute',
352
380
  bottom: BOTTOM_SAFE,
@@ -368,14 +396,13 @@ const styles = StyleSheet.create({
368
396
  typePlaceholder: { color: 'rgba(255,255,255,0.50)', fontSize: 14 },
369
397
  bottomIconBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center' },
370
398
  bottomIconGlyph: { fontSize: 22 },
371
- // Floating input bar — sits directly above keyboard, animated
372
399
  floatingInputWrap: {
373
400
  position: 'absolute',
374
401
  left: 0,
375
402
  right: 0,
376
403
  flexDirection: 'row',
377
404
  alignItems: 'center',
378
- backgroundColor: 'rgba(20,20,20,0.96)',
405
+ backgroundColor: 'rgba(20,20,20,0.97)',
379
406
  paddingHorizontal: 12,
380
407
  paddingVertical: 8,
381
408
  gap: 8,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasunk99-livestream-core",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
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",