@amityco/react-native-social-uikit 4.0.0-a9cb7bb.0 → 4.0.0-b024948b.0

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.
Files changed (47) hide show
  1. package/lib/commonjs/v4/PublicApi/Components/AmityPostEngagementActionsComponent/Components/DetailStyle.js +1 -1
  2. package/lib/commonjs/v4/PublicApi/Components/AmityPostEngagementActionsComponent/Components/DetailStyle.js.map +1 -1
  3. package/lib/commonjs/v4/PublicApi/Components/AmityPostEngagementActionsComponent/Components/FeedStyle.js +2 -1
  4. package/lib/commonjs/v4/PublicApi/Components/AmityPostEngagementActionsComponent/Components/FeedStyle.js.map +1 -1
  5. package/lib/commonjs/v4/PublicApi/Pages/AmityCreateLivestreamPage/AmityCreateLivestreamPage.js +11 -0
  6. package/lib/commonjs/v4/PublicApi/Pages/AmityCreateLivestreamPage/AmityCreateLivestreamPage.js.map +1 -1
  7. package/lib/commonjs/v4/PublicApi/Pages/AmityLivestreamPlayerPage/AmityLivestreamPlayerPage.js +94 -68
  8. package/lib/commonjs/v4/PublicApi/Pages/AmityLivestreamPlayerPage/AmityLivestreamPlayerPage.js.map +1 -1
  9. package/lib/commonjs/v4/PublicApi/Pages/AmityLivestreamPlayerPage/styles.js +2 -1
  10. package/lib/commonjs/v4/PublicApi/Pages/AmityLivestreamPlayerPage/styles.js.map +1 -1
  11. package/lib/commonjs/v4/component/RenderTextWithMention/RenderTextWithMention.js +16 -9
  12. package/lib/commonjs/v4/component/RenderTextWithMention/RenderTextWithMention.js.map +1 -1
  13. package/lib/commonjs/v4/constants/index.js +2 -1
  14. package/lib/commonjs/v4/constants/index.js.map +1 -1
  15. package/lib/commonjs/v4/enum/roomStatus.js +1 -1
  16. package/lib/module/v4/PublicApi/Components/AmityPostEngagementActionsComponent/Components/DetailStyle.js +1 -1
  17. package/lib/module/v4/PublicApi/Components/AmityPostEngagementActionsComponent/Components/DetailStyle.js.map +1 -1
  18. package/lib/module/v4/PublicApi/Components/AmityPostEngagementActionsComponent/Components/FeedStyle.js +2 -1
  19. package/lib/module/v4/PublicApi/Components/AmityPostEngagementActionsComponent/Components/FeedStyle.js.map +1 -1
  20. package/lib/module/v4/PublicApi/Pages/AmityCreateLivestreamPage/AmityCreateLivestreamPage.js +11 -0
  21. package/lib/module/v4/PublicApi/Pages/AmityCreateLivestreamPage/AmityCreateLivestreamPage.js.map +1 -1
  22. package/lib/module/v4/PublicApi/Pages/AmityLivestreamPlayerPage/AmityLivestreamPlayerPage.js +95 -69
  23. package/lib/module/v4/PublicApi/Pages/AmityLivestreamPlayerPage/AmityLivestreamPlayerPage.js.map +1 -1
  24. package/lib/module/v4/PublicApi/Pages/AmityLivestreamPlayerPage/styles.js +2 -1
  25. package/lib/module/v4/PublicApi/Pages/AmityLivestreamPlayerPage/styles.js.map +1 -1
  26. package/lib/module/v4/component/RenderTextWithMention/RenderTextWithMention.js +17 -10
  27. package/lib/module/v4/component/RenderTextWithMention/RenderTextWithMention.js.map +1 -1
  28. package/lib/module/v4/constants/index.js +1 -0
  29. package/lib/module/v4/constants/index.js.map +1 -1
  30. package/lib/module/v4/enum/roomStatus.js +1 -1
  31. package/lib/typescript/src/v4/PublicApi/Pages/AmityCreateLivestreamPage/AmityCreateLivestreamPage.d.ts.map +1 -1
  32. package/lib/typescript/src/v4/PublicApi/Pages/AmityLivestreamPlayerPage/AmityLivestreamPlayerPage.d.ts.map +1 -1
  33. package/lib/typescript/src/v4/PublicApi/Pages/AmityLivestreamPlayerPage/styles.d.ts +1 -0
  34. package/lib/typescript/src/v4/PublicApi/Pages/AmityLivestreamPlayerPage/styles.d.ts.map +1 -1
  35. package/lib/typescript/src/v4/component/RenderTextWithMention/RenderTextWithMention.d.ts.map +1 -1
  36. package/lib/typescript/src/v4/constants/index.d.ts +1 -0
  37. package/lib/typescript/src/v4/constants/index.d.ts.map +1 -1
  38. package/lib/typescript/src/v4/enum/roomStatus.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/v4/PublicApi/Components/AmityPostEngagementActionsComponent/Components/DetailStyle.tsx +1 -1
  41. package/src/v4/PublicApi/Components/AmityPostEngagementActionsComponent/Components/FeedStyle.tsx +1 -1
  42. package/src/v4/PublicApi/Pages/AmityCreateLivestreamPage/AmityCreateLivestreamPage.tsx +15 -1
  43. package/src/v4/PublicApi/Pages/AmityLivestreamPlayerPage/AmityLivestreamPlayerPage.tsx +155 -87
  44. package/src/v4/PublicApi/Pages/AmityLivestreamPlayerPage/styles.ts +1 -0
  45. package/src/v4/component/RenderTextWithMention/RenderTextWithMention.tsx +16 -10
  46. package/src/v4/constants/index.ts +3 -0
  47. package/src/v4/enum/roomStatus.ts +1 -1
@@ -1,9 +1,8 @@
1
1
  import React, { useEffect, useState, useRef } from 'react';
2
- import { View, TouchableOpacity } from 'react-native';
2
+ import { View, TouchableOpacity, Platform } from 'react-native';
3
3
  import { useStyles } from './styles';
4
4
  import LiveStreamEndThumbnail from '../../../component/LivestreamContent/LivestreamEndedThumbnail';
5
5
  import { SvgXml } from 'react-native-svg';
6
- import { RoomRepository } from '@amityco/ts-sdk-react-native';
7
6
  import { close } from '../../../assets/icons';
8
7
  import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
9
8
  import { NativeStackNavigationProp } from '@react-navigation/native-stack';
@@ -20,6 +19,7 @@ import {
20
19
  usePostSubscription,
21
20
  useRoomSubscription,
22
21
  } from '../../../../v4/hook/index';
22
+ import { RoomRepository } from '@amityco/ts-sdk-react-native';
23
23
 
24
24
  function AmityLiveStreamPlayerPage() {
25
25
  const { styles, theme } = useStyles();
@@ -29,18 +29,21 @@ function AmityLiveStreamPlayerPage() {
29
29
 
30
30
  const [reconnecting, setReconnecting] = useState(false);
31
31
  const [room, setRoom] = useState<Amity.Room | null>(null);
32
+ const [videoError, setVideoError] = useState(false);
32
33
  const [error, setError] = useState<Error | null>(null);
34
+ const [, setPlayerInitialized] = useState(false);
35
+ const [videoKey, setVideoKey] = useState(0);
36
+ const [isPaused, setIsPaused] = useState(false);
33
37
  const [wasLive, setWasLive] = useState(false);
34
- const [showEndThumbnail, setShowEndThumbnail] = useState(false);
35
- const { roomId, post } = route.params;
36
- const { client } = useAuth();
37
- const videoRef = useRef<any>(null);
38
- const isProgrammaticDismiss = useRef(false);
39
38
 
39
+ const { roomId, post } = route.params;
40
40
  const { subscribedPost } = usePostSubscription(post?.postId);
41
-
42
41
  useRoomSubscription({ room });
43
42
 
43
+ const { client } = useAuth();
44
+ const videoRef = useRef<any>(null);
45
+ const isStreamEnding = useRef(false);
46
+
44
47
  useEffect(() => {
45
48
  const unsubscribe = RoomRepository.getRoom(
46
49
  roomId,
@@ -54,22 +57,21 @@ function AmityLiveStreamPlayerPage() {
54
57
  }, [roomId]);
55
58
 
56
59
  useEffect(() => {
57
- if (room?.isDeleted || subscribedPost?.isDeleted) {
58
- navigation.replace('PostDetail', { postId: subscribedPost?.postId });
60
+ if (room?.status === RoomStatus.live) {
61
+ setWasLive(true);
59
62
  }
60
- }, [room?.isDeleted, subscribedPost, navigation]);
63
+ }, [room?.status]);
61
64
 
62
65
  useEffect(() => {
63
- const isTerminated =
64
- room?.moderation?.terminateLabels &&
65
- room?.moderation?.terminateLabels?.length > 0;
66
- const isLiveOrEnded =
67
- room?.status === RoomStatus.live || room?.status === RoomStatus.ended;
68
-
69
- if (isLiveOrEnded && isTerminated) {
70
- navigation.replace('LivestreamTerminated', { type: 'viewer' });
66
+ if (room?.isDeleted || subscribedPost?.isDeleted) {
67
+ navigation.replace('PostDetail', { postId: subscribedPost?.postId });
71
68
  }
72
- }, [room?.moderation?.terminateLabels, room?.status, navigation]);
69
+ }, [
70
+ navigation,
71
+ room?.isDeleted,
72
+ subscribedPost?.postId,
73
+ subscribedPost?.isDeleted,
74
+ ]);
73
75
 
74
76
  useEffect(() => {
75
77
  const unsubscribe = NetInfo.addEventListener((state) => {
@@ -78,44 +80,94 @@ function AmityLiveStreamPlayerPage() {
78
80
  return () => unsubscribe();
79
81
  }, []);
80
82
 
81
- // Track if user was watching live
82
83
  useEffect(() => {
83
- if (room?.status === RoomStatus.live) {
84
- setWasLive(true);
85
- }
86
- }, [room?.status]);
84
+ if (!room?.status) return;
87
85
 
88
- // Dismiss fullscreen player when stream ends
89
- useEffect(() => {
90
- const shouldShowEndThumbnail =
91
- room?.status === RoomStatus.ended ||
92
- (room?.status === RoomStatus.recorded && wasLive);
93
-
94
- if (shouldShowEndThumbnail && videoRef.current) {
95
- isProgrammaticDismiss.current = true;
96
- videoRef.current?.dismissFullscreenPlayer();
97
- // Delay showing end thumbnail to allow fullscreen dismiss to complete
86
+ const shouldEnd =
87
+ room.status === RoomStatus.ended ||
88
+ (room.status === RoomStatus.recorded && wasLive);
89
+
90
+ if (!shouldEnd || isStreamEnding.current) return;
91
+
92
+ isStreamEnding.current = true;
93
+ setIsPaused(true);
94
+
95
+ if (Platform.OS === 'ios') {
96
+ // iOS: ONLY dismiss fullscreen. DO NOT touch key. DO NOT unmount.
97
+ requestAnimationFrame(() => {
98
+ videoRef.current?.dismissFullscreenPlayer?.();
99
+ });
100
+ } else {
101
+ // Android: HARD destroy
98
102
  setTimeout(() => {
99
- setShowEndThumbnail(true);
100
- }, 300);
101
- } else if (shouldShowEndThumbnail && !videoRef.current) {
102
- // If video ref is already null, show end thumbnail immediately
103
- setShowEndThumbnail(true);
104
- } else if (!shouldShowEndThumbnail) {
105
- setShowEndThumbnail(false);
103
+ setVideoKey((prev) => prev + 1);
104
+ }, 50);
106
105
  }
107
106
  }, [room?.status, wasLive]);
108
107
 
109
- // Start in fullscreen mode on iOS
110
108
  useEffect(() => {
111
- if (videoRef.current && room && room.status !== RoomStatus.ended) {
112
- const timer = setTimeout(() => {
113
- videoRef.current?.presentFullscreenPlayer();
114
- }, 100);
115
- return () => clearTimeout(timer);
109
+ let timer: ReturnType<typeof setTimeout> | null = null;
110
+
111
+ if (
112
+ videoRef.current &&
113
+ room &&
114
+ room.status === RoomStatus.live &&
115
+ !videoError &&
116
+ Platform.OS === 'ios'
117
+ ) {
118
+ const isTerminated =
119
+ room?.moderation?.terminateLabels &&
120
+ room?.moderation?.terminateLabels?.length > 0;
121
+
122
+ if (!isTerminated) {
123
+ timer = setTimeout(() => {
124
+ videoRef.current?.presentFullscreenPlayer();
125
+ }, 100);
126
+ }
127
+ }
128
+
129
+ return () => {
130
+ if (timer !== null) {
131
+ clearTimeout(timer);
132
+ }
133
+ };
134
+ }, [room, videoError]);
135
+
136
+ const isTerminated =
137
+ room?.moderation?.terminateLabels &&
138
+ room?.moderation?.terminateLabels?.length > 0;
139
+
140
+ const shouldShowEndThumbnail =
141
+ room?.status === RoomStatus.ended ||
142
+ (room?.status === RoomStatus.recorded && wasLive) ||
143
+ isTerminated;
144
+
145
+ useEffect(() => {
146
+ if (!room?.status) return;
147
+
148
+ const shouldEnd =
149
+ room.status === RoomStatus.ended ||
150
+ (room.status === RoomStatus.recorded && wasLive);
151
+
152
+ if (!shouldEnd || isStreamEnding.current) return;
153
+
154
+ isStreamEnding.current = true;
155
+ setIsPaused(true);
156
+
157
+ if (Platform.OS === 'ios') {
158
+ // iOS: just dismiss fullscreen, DO NOT destroy immediately
159
+ if (videoRef.current) {
160
+ try {
161
+ videoRef.current.dismissFullscreenPlayer?.();
162
+ } catch {}
163
+ }
164
+ } else {
165
+ // Android: HARD destroy
166
+ setTimeout(() => {
167
+ setVideoKey((prev) => prev + 1);
168
+ }, 50);
116
169
  }
117
- return undefined;
118
- }, [room]);
170
+ }, [room?.status, wasLive]);
119
171
 
120
172
  if (!room || error) {
121
173
  return (
@@ -129,21 +181,19 @@ function AmityLiveStreamPlayerPage() {
129
181
  navigation.goBack();
130
182
  };
131
183
 
132
- const handleFullscreenDismiss = () => {
133
- // Only navigate back if user manually dismissed, not programmatically
134
- if (!isProgrammaticDismiss.current) {
135
- closePlayer();
136
- }
137
- isProgrammaticDismiss.current = false;
138
- };
184
+ const videoUrl =
185
+ room.status === RoomStatus.recorded
186
+ ? room.recordedPlaybackInfos[0]?.url
187
+ : room.livePlaybackUrl;
139
188
 
140
189
  return (
141
190
  <SafeAreaView style={styles.container}>
142
- {showEndThumbnail ? (
191
+ {shouldShowEndThumbnail ? (
143
192
  <>
144
193
  <View style={styles.steamEndContainer}>
145
194
  <LiveStreamEndThumbnail />
146
195
  </View>
196
+
147
197
  <TouchableOpacity style={styles.closeButton} onPress={closePlayer}>
148
198
  <SvgXml
149
199
  xml={close()}
@@ -165,35 +215,53 @@ function AmityLiveStreamPlayerPage() {
165
215
  </View>
166
216
  </View>
167
217
  )}
168
- <Video
169
- ref={videoRef}
170
- source={{
171
- uri:
172
- room.status === RoomStatus.recorded
173
- ? room.recordedPlaybackInfos[0]?.url
174
- : room.livePlaybackUrl,
175
- headers: {
176
- Authorization: `Bearer ${client.token.accessToken}`,
177
- },
178
-
179
- type: 'm3u8',
180
- }}
181
- style={styles.container}
182
- resizeMode="contain"
183
- controls={room.status === RoomStatus.recorded}
184
- fullscreen={true}
185
- fullscreenOrientation="landscape"
186
- paused={false}
187
- muted={false}
188
- volume={1.0}
189
- audioOutput="speaker"
190
- playInBackground={false}
191
- playWhenInactive={false}
192
- onError={(e) => {
193
- console.log('Video Player Error: ', e);
194
- }}
195
- onFullscreenPlayerDidDismiss={handleFullscreenDismiss}
196
- />
218
+
219
+ {!shouldShowEndThumbnail && (
220
+ <Video
221
+ key={
222
+ Platform.OS === 'android' ? `${videoUrl}-${videoKey}` : videoUrl
223
+ }
224
+ ref={videoRef}
225
+ source={{
226
+ uri:
227
+ room.status === RoomStatus.recorded
228
+ ? room.recordedPlaybackInfos[0]?.url
229
+ : room.livePlaybackUrl,
230
+ headers: {
231
+ Authorization: `Bearer ${client.token.accessToken}`,
232
+ },
233
+ }}
234
+ style={styles.container}
235
+ resizeMode="contain"
236
+ controls={room?.status === RoomStatus.recorded && !wasLive}
237
+ fullscreen={
238
+ Platform.OS === 'ios' && room.status !== RoomStatus.recorded
239
+ }
240
+ fullscreenOrientation="landscape"
241
+ paused={isPaused}
242
+ muted={false}
243
+ volume={1.0}
244
+ audioOutput="speaker"
245
+ playInBackground={false}
246
+ playWhenInactive={false}
247
+ repeat={false}
248
+ onLoad={() => {
249
+ setPlayerInitialized(true);
250
+ if (room.status === RoomStatus.recorded) {
251
+ setIsPaused(false);
252
+ }
253
+ }}
254
+ onError={(e) => {
255
+ console.log('Video Error: ', e);
256
+ setVideoError(true);
257
+ }}
258
+ onFullscreenPlayerDidDismiss={() => {
259
+ if (Platform.OS === 'ios') {
260
+ closePlayer();
261
+ }
262
+ }}
263
+ />
264
+ )}
197
265
  </View>
198
266
  )}
199
267
 
@@ -17,6 +17,7 @@ export const useStyles = () => {
17
17
  steamEndContainer: {
18
18
  flex: 1,
19
19
  justifyContent: 'center',
20
+ backgroundColor: '#000000',
20
21
  },
21
22
  indicator: {
22
23
  left: 16,
@@ -1,4 +1,4 @@
1
- import { Text, Linking } from 'react-native';
1
+ import { Text, Linking, TouchableOpacity } from 'react-native';
2
2
  import { useStyles } from './styles';
3
3
  import React, { memo, useCallback } from 'react';
4
4
  import { useNavigation } from '@react-navigation/native';
@@ -6,6 +6,7 @@ import { IMentionPosition } from '../../types/type';
6
6
  import { NativeStackNavigationProp } from '@react-navigation/native-stack';
7
7
  import { RootStackParamList } from '../../routes/RouteParamList';
8
8
  import ReadMore from '@fawazahmed/react-native-read-more';
9
+ import { URL_REGEX } from '../../../v4/constants';
9
10
 
10
11
  interface IrenderTextWithMention {
11
12
  mentionPositionArr: IMentionPosition[];
@@ -28,10 +29,9 @@ const RenderTextWithMention: React.FC<IrenderTextWithMention> = ({
28
29
  const navigation =
29
30
  useNavigation() as NativeStackNavigationProp<RootStackParamList>;
30
31
  const linkArr = useCallback((text: string): LinkInfo[] => {
31
- const urlRegex = /(https?:\/\/|www\.)[^\s]+/g;
32
32
  const links: LinkInfo[] = [];
33
33
  let match;
34
- while ((match = urlRegex.exec(text)) !== null) {
34
+ while ((match = URL_REGEX.exec(text)) !== null) {
35
35
  links.push({
36
36
  link: match[0],
37
37
  index: match.index,
@@ -54,8 +54,14 @@ const RenderTextWithMention: React.FC<IrenderTextWithMention> = ({
54
54
  },
55
55
  [navigation]
56
56
  );
57
- const handleLinkClick = useCallback((url: string) => {
58
- Linking.openURL(url);
57
+ const handleLinkClick = useCallback(async (url: string) => {
58
+ try {
59
+ const hasProtocol = /^(https?|ftp|mailto):/.test(url);
60
+ const formattedUrl = hasProtocol ? url : `https://${url}`;
61
+ await Linking.openURL(formattedUrl);
62
+ } catch (error) {
63
+ console.warn('Failed to open URL:', error);
64
+ }
59
65
  }, []);
60
66
 
61
67
  const handleOnClick = useCallback(
@@ -88,14 +94,14 @@ const RenderTextWithMention: React.FC<IrenderTextWithMention> = ({
88
94
  const nonHighlightedText = textPost.slice(currentPosition, index);
89
95
  // Add highlighted text
90
96
  const highlightedText = (
91
- <Text
92
- selectable
97
+ <TouchableOpacity
93
98
  onPress={() => handleOnClick(link, userId)}
94
99
  key={`highlighted-${i}`}
95
- style={styles.mentionText}
96
100
  >
97
- {textPost.slice(index, index + length)}
98
- </Text>
101
+ <Text style={styles.mentionText} selectable>
102
+ {textPost.slice(index, index + length)}
103
+ </Text>
104
+ </TouchableOpacity>
99
105
  );
100
106
 
101
107
  // Update currentPosition for the next iteration
@@ -105,3 +105,6 @@ export const QUERY_KEY = {
105
105
  POSTS_COLLECTION: 'posts-collections',
106
106
  PINNED_POSTS_COLLECTION: 'pinned-posts-collections',
107
107
  };
108
+
109
+ export const URL_REGEX =
110
+ /(?:(?:https?|ftp):\/\/(?:[a-zA-Z0-9.-]+|[\d.]+)(?::\d{1,5})?(?:\/(?:[^\s<>|()]*(?:\([^\s<>|()]*\)[^\s<>|()]*)*)*)?|mailto:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|www\.(?:[a-zA-Z0-9.-]+)(?:\/(?:[^\s<>|()]*(?:\([^\s<>|()]*\)[^\s<>|()]*)*)*)?(?!\.))/g;
@@ -3,5 +3,5 @@ export const RoomStatus = {
3
3
  live: 'live' as Amity.RoomStatus,
4
4
  recorded: 'recorded' as Amity.RoomStatus,
5
5
  ended: 'ended' as Amity.RoomStatus,
6
- waiting_reconnect: 'waiting_reconnect' as Amity.RoomStatus,
6
+ waiting_reconnect: 'waitingReconnect' as Amity.RoomStatus,
7
7
  };