kasunk99-livestream-core 0.3.15 → 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,14 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { ActivityIndicator, Animated, Dimensions, Keyboard,
|
|
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';
|
|
4
4
|
import { useViewerSocket } from '../hooks/useViewerSocket';
|
|
5
5
|
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
|
6
6
|
const CHAT_NAME_COLORS = ['#f97316', '#22c55e', '#3b82f6', '#eab308', '#ec4899', '#a855f7'];
|
|
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 up when keyboard is open — fixed offset works with both adjustResize + adjustNothing
|
|
11
|
-
const CHAT_BOTTOM_TYPING = BOTTOM_SAFE + 8;
|
|
12
10
|
let RTCViewComponent = null;
|
|
13
11
|
try {
|
|
14
12
|
const webrtc = require('react-native-webrtc');
|
|
@@ -36,27 +34,99 @@ 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
|
-
//
|
|
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;
|
|
46
56
|
const chatAnim = useRef(new Animated.Value(CHAT_BOTTOM_DEFAULT)).current;
|
|
47
|
-
//
|
|
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)
|
|
48
69
|
useEffect(() => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!isActive) {
|
|
126
|
+
Keyboard.dismiss();
|
|
127
|
+
setIsTyping(false);
|
|
128
|
+
}
|
|
129
|
+
}, [isActive]);
|
|
60
130
|
const seededFromRoomState = useMemo(() => {
|
|
61
131
|
const chat = roomState && typeof roomState === 'object'
|
|
62
132
|
? roomState.chat
|
|
@@ -70,6 +140,7 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
70
140
|
setChatInput('');
|
|
71
141
|
setIsTyping(false);
|
|
72
142
|
Keyboard.dismiss();
|
|
143
|
+
inputBottom.setValue(0);
|
|
73
144
|
chatAnim.setValue(CHAT_BOTTOM_DEFAULT);
|
|
74
145
|
return;
|
|
75
146
|
}
|
|
@@ -78,7 +149,7 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
78
149
|
setChatMessages([]);
|
|
79
150
|
setChatInput('');
|
|
80
151
|
}
|
|
81
|
-
}, [roomId, chatAnim]);
|
|
152
|
+
}, [roomId, inputBottom, chatAnim]);
|
|
82
153
|
useEffect(() => {
|
|
83
154
|
if (!roomId)
|
|
84
155
|
return;
|
|
@@ -193,7 +264,11 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
193
264
|
const text = chatInput.trim();
|
|
194
265
|
if (!text || !socket || !joined)
|
|
195
266
|
return;
|
|
267
|
+
// Clear input, close panel, dismiss keyboard — all before the emit
|
|
268
|
+
// so the button press completes synchronously before any state teardown.
|
|
196
269
|
setChatInput('');
|
|
270
|
+
setIsTyping(false);
|
|
271
|
+
Keyboard.dismiss();
|
|
197
272
|
socket.emit('chat-message', { text }, (res) => {
|
|
198
273
|
if (res?.error)
|
|
199
274
|
console.log('[viewer] chat send error', res.error);
|
|
@@ -201,8 +276,9 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
201
276
|
};
|
|
202
277
|
const hostLabel = stream.hostDisplayName || stream.title || 'Live';
|
|
203
278
|
const avatarLetter = hostLabel[0]?.toUpperCase() ?? 'L';
|
|
204
|
-
|
|
205
|
-
|
|
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" })] }) }))] }));
|
|
206
282
|
});
|
|
207
283
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
208
284
|
const styles = StyleSheet.create({
|
|
@@ -306,7 +382,6 @@ const styles = StyleSheet.create({
|
|
|
306
382
|
},
|
|
307
383
|
actionIcon: { fontSize: 22, color: '#fff' },
|
|
308
384
|
actionLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 11, fontWeight: '500' },
|
|
309
|
-
// Chat column — absolute, bottom animated between default and typing positions
|
|
310
385
|
chatColumn: {
|
|
311
386
|
position: 'absolute',
|
|
312
387
|
left: 12,
|
|
@@ -355,23 +430,29 @@ const styles = StyleSheet.create({
|
|
|
355
430
|
borderWidth: StyleSheet.hairlineWidth,
|
|
356
431
|
borderColor: 'rgba(255,255,255,0.18)',
|
|
357
432
|
},
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
// Modal input overlay
|
|
362
|
-
inputModalKAV: {
|
|
363
|
-
flex: 1,
|
|
433
|
+
typeTouchableFilled: {
|
|
434
|
+
backgroundColor: 'rgba(255,255,255,0.18)',
|
|
435
|
+
borderColor: 'rgba(255,255,255,0.30)',
|
|
364
436
|
},
|
|
365
|
-
|
|
366
|
-
|
|
437
|
+
typePlaceholder: {
|
|
438
|
+
color: 'rgba(255,255,255,0.50)',
|
|
439
|
+
fontSize: 14,
|
|
367
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
|
|
368
447
|
floatingInputWrap: {
|
|
448
|
+
position: 'absolute',
|
|
449
|
+
left: 0,
|
|
450
|
+
right: 0,
|
|
369
451
|
flexDirection: 'row',
|
|
370
452
|
alignItems: 'center',
|
|
371
453
|
backgroundColor: 'rgba(20,20,20,0.97)',
|
|
372
454
|
paddingHorizontal: 12,
|
|
373
455
|
paddingVertical: 8,
|
|
374
|
-
paddingBottom: BOTTOM_SAFE,
|
|
375
456
|
gap: 8,
|
|
376
457
|
borderTopWidth: StyleSheet.hairlineWidth,
|
|
377
458
|
borderTopColor: 'rgba(255,255,255,0.10)',
|
package/package.json
CHANGED