kasunk99-livestream-core 0.3.30 → 0.3.32

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,9 +1,15 @@
1
1
  import React from 'react';
2
+ type LiveStreamFeedProps = {
3
+ isScreenFocused?: boolean;
4
+ onLeave?: () => void;
5
+ onJoinStatusChange?: (joined: boolean) => void;
6
+ };
2
7
  /**
3
8
  * TikTok-style vertical feed: one stream per full-screen item.
4
9
  * Height is measured from the actual container so it fills exactly to the
5
10
  * navigation bar on every device — no hardcoded offsets.
6
11
  * Scrolling snaps per-item (next/previous stream only).
7
12
  */
8
- export declare function LiveStreamFeed(): React.ReactElement;
13
+ export declare function LiveStreamFeed({ isScreenFocused, onLeave, onJoinStatusChange, }?: LiveStreamFeedProps): React.ReactElement;
14
+ export {};
9
15
  //# sourceMappingURL=LiveStreamFeed.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LiveStreamFeed.d.ts","sourceRoot":"","sources":["../../src/components/LiveStreamFeed.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwC,MAAM,OAAO,CAAC;AAiB7D;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,KAAK,CAAC,YAAY,CAgInD"}
1
+ {"version":3,"file":"LiveStreamFeed.d.ts","sourceRoot":"","sources":["../../src/components/LiveStreamFeed.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwC,MAAM,OAAO,CAAC;AAiB7D,KAAK,mBAAmB,GAAG;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC7B,eAAe,EACf,OAAO,EACP,kBAAkB,GACnB,GAAE,mBAAwB,GAAG,KAAK,CAAC,YAAY,CAsI/C"}
@@ -9,7 +9,7 @@ import { useLiveStreams } from '../hooks/useLiveStreams';
9
9
  * navigation bar on every device — no hardcoded offsets.
10
10
  * Scrolling snaps per-item (next/previous stream only).
11
11
  */
12
- export function LiveStreamFeed() {
12
+ export function LiveStreamFeed({ isScreenFocused, onLeave, onJoinStatusChange, } = {}) {
13
13
  const { streams, loading, error, refetch } = useLiveStreams();
14
14
  const [activeIndex, setActiveIndex] = useState(0);
15
15
  const [refreshing, setRefreshing] = useState(false);
@@ -38,7 +38,7 @@ export function LiveStreamFeed() {
38
38
  const index = Math.round(e.nativeEvent.contentOffset.y / viewportHeight);
39
39
  setActiveIndex(Math.max(0, Math.min(index, streams.length - 1)));
40
40
  }, [streams.length, viewportHeight]);
41
- const renderItem = useCallback(({ item, index }) => (_jsx(View, { style: { height: viewportHeight, width: '100%' }, children: _jsx(LiveStreamViewerItem, { stream: item, isActive: index === activeIndex, index: index }) })), [activeIndex, viewportHeight]);
41
+ const renderItem = useCallback(({ item, index }) => (_jsx(View, { style: { height: viewportHeight, width: '100%' }, children: _jsx(LiveStreamViewerItem, { stream: item, isActive: index === activeIndex && (isScreenFocused ?? true), index: index, onLeave: onLeave, onJoinStatusChange: index === activeIndex ? onJoinStatusChange : undefined }) })), [activeIndex, viewportHeight, isScreenFocused, onLeave, onJoinStatusChange]);
42
42
  const keyExtractor = useCallback((item) => item.roomId, []);
43
43
  const getItemLayout = useCallback((_, index) => ({
44
44
  length: viewportHeight,
@@ -4,6 +4,8 @@ type LiveStreamViewerItemProps = {
4
4
  stream: LiveStreamInfo;
5
5
  isActive: boolean;
6
6
  index: number;
7
+ onLeave?: () => void;
8
+ onJoinStatusChange?: (joined: boolean) => void;
7
9
  };
8
10
  export declare const LiveStreamViewerItem: React.NamedExoticComponent<LiveStreamViewerItemProps>;
9
11
  export {};
@@ -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;AAM/C,KAAK,yBAAyB,GAAG;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAiDF,eAAO,MAAM,oBAAoB,uDA0X/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;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,uDAyZ/B,CAAC"}
@@ -23,6 +23,17 @@ try {
23
23
  IoniconsComponent = vi.Ionicons;
24
24
  }
25
25
  catch { }
26
+ // RNGH TouchableOpacity — fires at the native gesture layer (GestureHandlerRootView is
27
+ // outermost in _layout.tsx, above KeyboardProvider), so it claims the touch before
28
+ // RNKC's InputMethodManager intercept drops it. This is why onPressIn/Pressable fail
29
+ // but this component works for the send button when the keyboard is open.
30
+ let RNGHTouchable = null;
31
+ try {
32
+ const rgh = require('react-native-gesture-handler');
33
+ if (rgh.TouchableOpacity)
34
+ RNGHTouchable = rgh.TouchableOpacity;
35
+ }
36
+ catch { }
26
37
  let KeyboardAvoidingView = RNKeyboardAvoidingView;
27
38
  try {
28
39
  const rnkc = require('react-native-keyboard-controller');
@@ -30,7 +41,7 @@ try {
30
41
  KeyboardAvoidingView = rnkc.KeyboardAvoidingView;
31
42
  }
32
43
  catch { }
33
- export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream, isActive, index: _index, }) {
44
+ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream, isActive, index: _index, onLeave, onJoinStatusChange, }) {
34
45
  const roomId = isActive ? stream.roomId : null;
35
46
  const { joined, joining, error, producerList, roomState, remoteStream, remoteVideoStream, webrtcUnavailable, consumeError, socket, viewerCount: liveViewerCount, streamEnded, } = useViewerSocket(roomId);
36
47
  const viewerCount = liveViewerCount > 0 ? liveViewerCount : stream.viewerCount;
@@ -60,6 +71,9 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
60
71
  const sub = Keyboard.addListener('keyboardDidHide', () => setIsTyping(false));
61
72
  return () => sub.remove();
62
73
  }, []);
74
+ useEffect(() => {
75
+ onJoinStatusChange?.(joined);
76
+ }, [joined, onJoinStatusChange]);
63
77
  // Dismiss input when stream becomes inactive (e.g. user swipes to next stream)
64
78
  useEffect(() => {
65
79
  if (!isActive) {
@@ -211,9 +225,13 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
211
225
  const avatarLetter = hostLabel[0]?.toUpperCase() ?? 'L';
212
226
  const hasUnsent = chatInput.trim().length > 0;
213
227
  const canType = joined && !joining && !error;
214
- 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}`)) : (_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() : '—' })] })] }), _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 ? (_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 }), _jsx(Pressable, { style: [styles.sendBtn, (!hasUnsent || !joined) && styles.sendBtnOff], onPressIn: sendChat, hitSlop: { top: 8, right: 8, bottom: 8, left: 8 }, children: IoniconsComponent
228
+ 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}`)) : (_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: "box-none", children: [onLeave ? (_jsx(TouchableOpacity, { style: styles.closeBtn, onPress: onLeave, hitSlop: { top: 8, bottom: 8, left: 8, right: 8 }, activeOpacity: 0.7, children: IoniconsComponent
229
+ ? _jsx(IoniconsComponent, { name: "close", size: 20, color: "#fff" })
230
+ : _jsx(Text, { style: { color: '#fff', fontSize: 18, fontWeight: '700' }, children: "\u00D7" }) })) : null, _jsx(View, { style: styles.avatar, pointerEvents: "none", children: _jsx(Text, { style: styles.avatarLetter, children: avatarLetter }) }), _jsx(Text, { style: styles.hostName, numberOfLines: 1, pointerEvents: "none", children: hostLabel }), _jsx(View, { style: styles.livePill, pointerEvents: "none", 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() : '—' })] })] }), _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 ? (_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
231
+ ? _jsx(IoniconsComponent, { name: "send", size: 18, color: "#fff" })
232
+ : _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
215
233
  ? _jsx(IoniconsComponent, { name: "send", size: 18, color: "#fff" })
216
- : _jsx(Text, { style: styles.sendIcon, children: "\u27A4" }) })] })) : (
234
+ : _jsx(Text, { style: styles.sendIcon, children: "\u27A4" }) }))] })) : (
217
235
  // Pill bar — tapping opens the input bar.
218
236
  _jsxs(View, { style: styles.bottomBar, children: [_jsx(TouchableOpacity, { style: [styles.typeTouchable, hasUnsent && styles.typeTouchableFilled], onPress: () => { if (canType)
219
237
  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, 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" })] }) }))] }));
@@ -259,6 +277,15 @@ const styles = StyleSheet.create({
259
277
  marginRight: 8,
260
278
  overflow: 'hidden',
261
279
  },
280
+ closeBtn: {
281
+ width: 32,
282
+ height: 32,
283
+ borderRadius: 16,
284
+ backgroundColor: 'rgba(0,0,0,0.45)',
285
+ alignItems: 'center',
286
+ justifyContent: 'center',
287
+ flexShrink: 0,
288
+ },
262
289
  avatar: {
263
290
  width: 36,
264
291
  height: 36,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasunk99-livestream-core",
3
- "version": "0.3.30",
3
+ "version": "0.3.32",
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",