kasunk99-livestream-core 0.3.36 → 0.3.38
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;AAmBvF,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;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CAChD,CAAC;AAmEF,eAAO,MAAM,oBAAoB,uDAoe/B,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { AppState, ActivityIndicator, Keyboard, KeyboardAvoidingView as RNKeyboardAvoidingView, NativeModules, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
|
|
2
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Animated, AppState, ActivityIndicator, Keyboard, KeyboardAvoidingView as RNKeyboardAvoidingView, NativeModules, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
|
|
4
4
|
import { useViewerSocket } from '../hooks/useViewerSocket';
|
|
5
5
|
const CHAT_NAME_COLORS = ['#f97316', '#22c55e', '#3b82f6', '#eab308', '#ec4899', '#a855f7'];
|
|
6
6
|
const BOTTOM_SAFE = Platform.OS === 'ios' ? 28 : 10;
|
|
@@ -43,7 +43,7 @@ try {
|
|
|
43
43
|
catch { }
|
|
44
44
|
export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream, isActive, index: _index, onLeave, onJoinStatusChange, }) {
|
|
45
45
|
const roomId = isActive ? stream.roomId : null;
|
|
46
|
-
const { joined, joining, error, producerList, roomState, remoteStream, remoteVideoStream, webrtcUnavailable, consumeError, socket, viewerCount: liveViewerCount, streamEnded, } = useViewerSocket(roomId);
|
|
46
|
+
const { joined, joining, error, producerList, roomState, remoteStream, remoteVideoStream, webrtcUnavailable, consumeError, socket, viewerCount: liveViewerCount, likeCount, streamEnded, emitLike, } = useViewerSocket(roomId);
|
|
47
47
|
const viewerCount = liveViewerCount > 0 ? liveViewerCount : stream.viewerCount;
|
|
48
48
|
const hasVideo = producerList.some((p) => p.kind === 'video');
|
|
49
49
|
const streamObj = remoteStream;
|
|
@@ -62,6 +62,20 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
62
62
|
const [chatMessages, setChatMessages] = useState([]);
|
|
63
63
|
// isTyping controls whether we show the TextInput bar or the pill bar
|
|
64
64
|
const [isTyping, setIsTyping] = useState(false);
|
|
65
|
+
const [floatingHearts, setFloatingHearts] = useState([]);
|
|
66
|
+
const heartIdRef = useRef(0);
|
|
67
|
+
const handleLike = useCallback(() => {
|
|
68
|
+
if (!joined)
|
|
69
|
+
return;
|
|
70
|
+
emitLike();
|
|
71
|
+
const id = heartIdRef.current++;
|
|
72
|
+
const anim = new Animated.Value(0);
|
|
73
|
+
const dx = (Math.random() - 0.5) * 36;
|
|
74
|
+
setFloatingHearts((prev) => [...prev, { id, anim, dx }]);
|
|
75
|
+
Animated.timing(anim, { toValue: 1, duration: 900, useNativeDriver: true }).start(() => {
|
|
76
|
+
setFloatingHearts((prev) => prev.filter((h) => h.id !== id));
|
|
77
|
+
});
|
|
78
|
+
}, [joined, emitLike]);
|
|
65
79
|
// Incremented every time the app returns to foreground — forces RTCView to remount,
|
|
66
80
|
// which restarts the Android SurfaceView renderer that freezes during background.
|
|
67
81
|
const [videoKey, setVideoKey] = useState(0);
|
|
@@ -254,16 +268,25 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
254
268
|
const avatarLetter = hostLabel[0]?.toUpperCase() ?? 'L';
|
|
255
269
|
const hasUnsent = chatInput.trim().length > 0;
|
|
256
270
|
const canType = joined && !joining && !error;
|
|
257
|
-
return (_jsxs(KeyboardAvoidingView, { style: styles.container, behavior: "padding", automaticOffset: true, children: [showVideo && RTCViewComponent && streamURL ? (_jsx(RTCViewComponent, { streamURL: streamURL, stream: displayStream, style: StyleSheet.absoluteFillObject, objectFit: "cover", mirror: false, pointerEvents: "none" }, `rtc-${trackCount}-${streamURL}-${videoKey}`)) : (_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.topRight, pointerEvents: "box-none", children: [_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() : '—' })] }), onLeave ? (_jsx(TouchableOpacity, { style: styles.closeBtn, onPress: onLeave, hitSlop: { top: 8, bottom: 8, left: 8, right: 8 }, activeOpacity: 0.7, children: IoniconsComponent
|
|
271
|
+
return (_jsxs(KeyboardAvoidingView, { style: styles.container, behavior: "padding", automaticOffset: true, children: [showVideo && RTCViewComponent && streamURL ? (_jsx(RTCViewComponent, { streamURL: streamURL, stream: displayStream, style: StyleSheet.absoluteFillObject, objectFit: "cover", mirror: false, pointerEvents: "none" }, `rtc-${trackCount}-${streamURL}-${videoKey}`)) : (_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.topRight, pointerEvents: "box-none", children: [_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() : '—' })] }), _jsxs(View, { style: styles.viewerChip, pointerEvents: "none", children: [_jsx(Text, { style: styles.viewerEye, children: "\u2665" }), _jsx(Text, { style: styles.viewerCount, children: likeCount >= 1000 ? `${(likeCount / 1000).toFixed(1)}K` : String(likeCount) })] }), onLeave ? (_jsx(TouchableOpacity, { style: styles.closeBtn, onPress: onLeave, hitSlop: { top: 8, bottom: 8, left: 8, right: 8 }, activeOpacity: 0.7, children: IoniconsComponent
|
|
258
272
|
? _jsx(IoniconsComponent, { name: "close", size: 20, color: "#fff" })
|
|
259
|
-
: _jsx(Text, { style: { color: '#fff', fontSize: 18, fontWeight: '700' }, children: "\u00D7" }) })) : null] })] }), _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))) }),
|
|
273
|
+
: _jsx(Text, { style: { color: '#fff', fontSize: 18, fontWeight: '700' }, children: "\u00D7" }) })) : null] })] }), _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))) }), _jsx(View, { style: styles.heartsContainer, pointerEvents: "none", children: floatingHearts.map(({ id, anim, dx }) => (_jsx(Animated.Text, { style: [
|
|
274
|
+
styles.floatingHeart,
|
|
275
|
+
{
|
|
276
|
+
transform: [
|
|
277
|
+
{ translateX: dx },
|
|
278
|
+
{ translateY: anim.interpolate({ inputRange: [0, 1], outputRange: [0, -150] }) },
|
|
279
|
+
],
|
|
280
|
+
opacity: anim.interpolate({ inputRange: [0, 0.65, 1], outputRange: [1, 0.85, 0] }),
|
|
281
|
+
},
|
|
282
|
+
], children: "\u2665" }, id))) }), isTyping ? (_jsxs(View, { style: styles.inputBar, children: [_jsx(TextInput, { ref: textInputRef, style: styles.textInput, value: chatInput, onChangeText: (text) => { chatInputRef.current = text; setChatInput(text); }, placeholder: "Say something...", placeholderTextColor: "rgba(255,255,255,0.40)", editable: canType, onSubmitEditing: sendChat, returnKeyType: "send", submitBehavior: "submit", autoFocus: true }), RNGHTouchable ? (_jsx(RNGHTouchable, { onPress: sendChat, activeOpacity: 0.75, style: [styles.sendBtn, (!hasUnsent || !joined) && styles.sendBtnOff], hitSlop: { top: 8, right: 8, bottom: 8, left: 8 }, children: IoniconsComponent
|
|
260
283
|
? _jsx(IoniconsComponent, { name: "send", size: 18, color: "#fff" })
|
|
261
284
|
: _jsx(Text, { style: styles.sendIcon, children: "\u27A4" }) })) : (_jsx(Pressable, { style: [styles.sendBtn, (!hasUnsent || !joined) && styles.sendBtnOff], onPressIn: sendChat, hitSlop: { top: 8, right: 8, bottom: 8, left: 8 }, children: IoniconsComponent
|
|
262
285
|
? _jsx(IoniconsComponent, { name: "send", size: 18, color: "#fff" })
|
|
263
286
|
: _jsx(Text, { style: styles.sendIcon, children: "\u27A4" }) }))] })) : (
|
|
264
287
|
// Pill bar — tapping opens the input bar.
|
|
265
288
|
_jsxs(View, { style: styles.bottomBar, children: [_jsx(TouchableOpacity, { style: [styles.typeTouchable, hasUnsent && styles.typeTouchableFilled], onPress: () => { if (canType)
|
|
266
|
-
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.
|
|
289
|
+
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.likeBtn, onPress: handleLike, activeOpacity: 0.75, children: _jsx(Text, { style: [styles.bottomIconGlyph, styles.heartIcon], children: "\u2665" }) })] })), 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" })] }) }))] }));
|
|
267
290
|
});
|
|
268
291
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
269
292
|
const styles = StyleSheet.create({
|
|
@@ -444,6 +467,18 @@ const styles = StyleSheet.create({
|
|
|
444
467
|
typePlaceholderFilled: { color: '#fff' },
|
|
445
468
|
bottomIconBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center' },
|
|
446
469
|
bottomIconGlyph: { fontSize: 22 },
|
|
470
|
+
likeBtn: { alignItems: 'center', justifyContent: 'center', minWidth: 40 },
|
|
471
|
+
likeCountText: { color: 'rgba(255,255,255,0.85)', fontSize: 10, fontWeight: '600', marginTop: 1 },
|
|
472
|
+
heartsContainer: {
|
|
473
|
+
position: 'absolute',
|
|
474
|
+
right: 12,
|
|
475
|
+
bottom: 56 + BOTTOM_SAFE,
|
|
476
|
+
width: 48,
|
|
477
|
+
height: 160,
|
|
478
|
+
alignItems: 'center',
|
|
479
|
+
justifyContent: 'flex-end',
|
|
480
|
+
},
|
|
481
|
+
floatingHeart: { position: 'absolute', bottom: 0, color: '#ef4444', fontSize: 26, fontWeight: '700' },
|
|
447
482
|
endedOverlay: {
|
|
448
483
|
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
449
484
|
backgroundColor: 'rgba(0,0,0,0.80)',
|
|
@@ -15,6 +15,8 @@ type ViewerSocketState = {
|
|
|
15
15
|
consumeRetryKey: number;
|
|
16
16
|
/** Real-time viewer count, updated via viewer-count-updated socket event. */
|
|
17
17
|
viewerCount: number;
|
|
18
|
+
/** Unique-user like count, updated via like-count-updated socket event. */
|
|
19
|
+
likeCount: number;
|
|
18
20
|
/** True once the host disconnects — triggers the "stream ended" overlay. */
|
|
19
21
|
streamEnded: boolean;
|
|
20
22
|
};
|
|
@@ -24,6 +26,7 @@ type ViewerSocketState = {
|
|
|
24
26
|
*/
|
|
25
27
|
export declare function useViewerSocket(roomId: string | null): ViewerSocketState & {
|
|
26
28
|
socket: Socket | null;
|
|
29
|
+
emitLike: () => void;
|
|
27
30
|
};
|
|
28
31
|
export {};
|
|
29
32
|
//# sourceMappingURL=useViewerSocket.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useViewerSocket.d.ts","sourceRoot":"","sources":["../../src/hooks/useViewerSocket.ts"],"names":[],"mappings":"AACA,OAAO,EAAM,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7D,KAAK,iBAAiB,GAAG;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,iBAAiB,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,iBAAiB,GAAG;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"useViewerSocket.d.ts","sourceRoot":"","sources":["../../src/hooks/useViewerSocket.ts"],"names":[],"mappings":"AACA,OAAO,EAAM,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7D,KAAK,iBAAiB,GAAG;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,iBAAiB,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,iBAAiB,GAAG;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAAE,CAijB1H"}
|
|
@@ -21,6 +21,7 @@ export function useViewerSocket(roomId) {
|
|
|
21
21
|
consumeError: null,
|
|
22
22
|
consumeRetryKey: 0,
|
|
23
23
|
viewerCount: 0,
|
|
24
|
+
likeCount: 0,
|
|
24
25
|
streamEnded: false,
|
|
25
26
|
});
|
|
26
27
|
const socketRef = useRef(null);
|
|
@@ -63,6 +64,7 @@ export function useViewerSocket(roomId) {
|
|
|
63
64
|
consumeError: null,
|
|
64
65
|
consumeRetryKey: 0,
|
|
65
66
|
viewerCount: 0,
|
|
67
|
+
likeCount: 0,
|
|
66
68
|
streamEnded: false,
|
|
67
69
|
});
|
|
68
70
|
consumeRetryCountRef.current = 0;
|
|
@@ -162,6 +164,7 @@ export function useViewerSocket(roomId) {
|
|
|
162
164
|
}
|
|
163
165
|
const roomState = res.roomState ?? null;
|
|
164
166
|
const initialViewerCount = typeof roomState?.viewerCount === 'number' ? roomState.viewerCount : 0;
|
|
167
|
+
const initialLikeCount = typeof roomState?.likeCount === 'number' ? roomState.likeCount : 0;
|
|
165
168
|
setState((prev) => ({
|
|
166
169
|
...prev,
|
|
167
170
|
joining: false,
|
|
@@ -171,6 +174,7 @@ export function useViewerSocket(roomId) {
|
|
|
171
174
|
rtpCapabilities: res.rtpCapabilities ?? null,
|
|
172
175
|
error: null,
|
|
173
176
|
viewerCount: initialViewerCount,
|
|
177
|
+
likeCount: initialLikeCount,
|
|
174
178
|
}));
|
|
175
179
|
});
|
|
176
180
|
});
|
|
@@ -203,6 +207,12 @@ export function useViewerSocket(roomId) {
|
|
|
203
207
|
setState((prev) => ({ ...prev, viewerCount: payload.viewerCount }));
|
|
204
208
|
}
|
|
205
209
|
});
|
|
210
|
+
// Like count: server deduplicates by user identity and broadcasts the unique count.
|
|
211
|
+
socket.on('like-count-updated', (payload) => {
|
|
212
|
+
if (typeof payload?.likeCount === 'number') {
|
|
213
|
+
setState((prev) => ({ ...prev, likeCount: payload.likeCount }));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
206
216
|
socket.on('peer-left', (payload) => {
|
|
207
217
|
if (payload?.isHost) {
|
|
208
218
|
setState((prev) => ({ ...prev, streamEnded: true, producerList: [] }));
|
|
@@ -500,8 +510,13 @@ export function useViewerSocket(roomId) {
|
|
|
500
510
|
cancelled = true;
|
|
501
511
|
};
|
|
502
512
|
}, [roomId, state.remoteStream, state.producerList]);
|
|
513
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
514
|
+
const emitLike = useCallback(() => {
|
|
515
|
+
socketRef.current?.emit('like');
|
|
516
|
+
}, []);
|
|
503
517
|
return {
|
|
504
518
|
...state,
|
|
505
519
|
socket: socketRef.current,
|
|
520
|
+
emitLike,
|
|
506
521
|
};
|
|
507
522
|
}
|
package/package.json
CHANGED