kasunk99-livestream-core 0.3.16 → 0.3.17
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,
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { ActivityIndicator, Animated, Dimensions, Keyboard, NativeModules, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
|
|
4
4
|
import { useViewerSocket } from '../hooks/useViewerSocket';
|
|
5
5
|
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
|
@@ -7,8 +7,6 @@ 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;
|
|
12
10
|
let RTCViewComponent = null;
|
|
13
11
|
try {
|
|
14
12
|
const webrtc = require('react-native-webrtc');
|
|
@@ -36,60 +34,93 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
36
34
|
? displayStreamObj.toURL()
|
|
37
35
|
: undefined;
|
|
38
36
|
const showVideo = RTCViewComponent && remoteStream && !!streamURL && hasVideoTrack;
|
|
37
|
+
// chatInput persists when keyboard closes without sending — cleared only on send
|
|
39
38
|
const [chatInput, setChatInput] = useState('');
|
|
40
39
|
const [chatMessages, setChatMessages] = useState([]);
|
|
40
|
+
// isTyping: user has opened the input panel
|
|
41
41
|
const [isTyping, setIsTyping] = useState(false);
|
|
42
42
|
const seededRoomRef = useRef(null);
|
|
43
43
|
const chatListRef = useRef(null);
|
|
44
44
|
const textInputRef = useRef(null);
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
const
|
|
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;
|
|
48
56
|
const chatAnim = useRef(new Animated.Value(CHAT_BOTTOM_DEFAULT)).current;
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
// the
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
56
81
|
useEffect(() => {
|
|
57
82
|
if (Platform.OS === 'android') {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
});
|
|
61
103
|
});
|
|
62
|
-
return () =>
|
|
104
|
+
return () => { s1.remove(); s2.remove(); };
|
|
63
105
|
}
|
|
64
|
-
// iOS — animate
|
|
106
|
+
// iOS — keyboardWillShow fires before keyboard so we can animate in sync
|
|
65
107
|
const s1 = Keyboard.addListener('keyboardWillShow', (e) => {
|
|
66
|
-
const
|
|
67
|
-
|
|
108
|
+
const offset = computeOffset(e.endCoordinates.height);
|
|
109
|
+
lastOffsetRef.current = offset;
|
|
68
110
|
Animated.parallel([
|
|
69
|
-
Animated.timing(inputBottom, { toValue:
|
|
70
|
-
Animated.timing(chatAnim, { toValue:
|
|
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 }),
|
|
71
113
|
]).start();
|
|
72
114
|
});
|
|
73
115
|
const s2 = Keyboard.addListener('keyboardWillHide', (e) => {
|
|
74
|
-
const dur = e.duration || 250;
|
|
75
116
|
Animated.parallel([
|
|
76
|
-
Animated.timing(inputBottom, { toValue:
|
|
77
|
-
Animated.timing(chatAnim, { toValue: CHAT_BOTTOM_DEFAULT, duration:
|
|
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 }),
|
|
78
119
|
]).start(() => setIsTyping(false));
|
|
79
120
|
});
|
|
80
121
|
return () => { s1.remove(); s2.remove(); };
|
|
81
|
-
}, [inputBottom, chatAnim]);
|
|
82
|
-
//
|
|
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
|
|
122
|
+
}, [inputBottom, chatAnim, computeOffset]);
|
|
123
|
+
// Dismiss keyboard when stream becomes inactive
|
|
93
124
|
useEffect(() => {
|
|
94
125
|
if (!isActive) {
|
|
95
126
|
Keyboard.dismiss();
|
|
@@ -109,7 +140,7 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
109
140
|
setChatInput('');
|
|
110
141
|
setIsTyping(false);
|
|
111
142
|
Keyboard.dismiss();
|
|
112
|
-
inputBottom.setValue(
|
|
143
|
+
inputBottom.setValue(0);
|
|
113
144
|
chatAnim.setValue(CHAT_BOTTOM_DEFAULT);
|
|
114
145
|
return;
|
|
115
146
|
}
|
|
@@ -233,7 +264,11 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
233
264
|
const text = chatInput.trim();
|
|
234
265
|
if (!text || !socket || !joined)
|
|
235
266
|
return;
|
|
267
|
+
// Clear input, close panel, dismiss keyboard — all before the emit
|
|
268
|
+
// so the button press completes synchronously before any state teardown.
|
|
236
269
|
setChatInput('');
|
|
270
|
+
setIsTyping(false);
|
|
271
|
+
Keyboard.dismiss();
|
|
237
272
|
socket.emit('chat-message', { text }, (res) => {
|
|
238
273
|
if (res?.error)
|
|
239
274
|
console.log('[viewer] chat send error', res.error);
|
|
@@ -241,8 +276,9 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
241
276
|
};
|
|
242
277
|
const hostLabel = stream.hostDisplayName || stream.title || 'Live';
|
|
243
278
|
const avatarLetter = hostLabel[0]?.toUpperCase() ?? 'L';
|
|
244
|
-
|
|
245
|
-
|
|
279
|
+
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" })] }) }))] }));
|
|
246
282
|
});
|
|
247
283
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
248
284
|
const styles = StyleSheet.create({
|
|
@@ -375,6 +411,7 @@ const styles = StyleSheet.create({
|
|
|
375
411
|
chatUsername: { fontWeight: '700' },
|
|
376
412
|
chatMsg: { color: '#e5e5e5', fontWeight: '400' },
|
|
377
413
|
joinText: { color: 'rgba(255,255,255,0.48)', fontSize: 12, fontStyle: 'italic' },
|
|
414
|
+
// Bottom action bar
|
|
378
415
|
bottomBar: {
|
|
379
416
|
position: 'absolute',
|
|
380
417
|
bottom: BOTTOM_SAFE,
|
|
@@ -393,9 +430,20 @@ const styles = StyleSheet.create({
|
|
|
393
430
|
borderWidth: StyleSheet.hairlineWidth,
|
|
394
431
|
borderColor: 'rgba(255,255,255,0.18)',
|
|
395
432
|
},
|
|
396
|
-
|
|
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
|
+
},
|
|
397
444
|
bottomIconBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center' },
|
|
398
445
|
bottomIconGlyph: { fontSize: 22 },
|
|
446
|
+
// Floating input — absolute, bottom driven by Animated.Value
|
|
399
447
|
floatingInputWrap: {
|
|
400
448
|
position: 'absolute',
|
|
401
449
|
left: 0,
|
package/package.json
CHANGED