diff --git a/README.md b/README.md
index 29cc3ec25..f5507095f 100644
--- a/README.md
+++ b/README.md
@@ -11,14 +11,14 @@ Sendbird UIKit for React-Native is a development kit with an user interface that
This mono-repository the UIKit source code is consists as explained below.
- [**packages/uikit-react-native**](/packages/uikit-react-native) is where you can find the open source code. Check out [UIKit Open Source Guidelines](/OPENSOURCE_GUIDELINES.md) for more information regarding our stance on open source.
-- [**sample**](/sample) is a chat app with UIKit’s core features in which you can see items such as push notifications, total unread message count and auto sign-in are demonstrated. When you sign in to the sample app, you will only see a list of channels rendered by the [GroupChannelListFragment](https://sendbird.com/docs/uikit/v3/react-native/key-functions/list-channels) on the screen.
+- [**sample**](/sample) is a chat app with UIKit’s core features in which you can see items such as push notifications, total unread message count and auto sign-in are demonstrated. When you sign in to the sample app, you will only see a list of channels rendered by the [GroupChannelListFragment](https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/list-channels) on the screen.
- [**packages/uikit-react-native-foundation**](/packages/uikit-react-native-foundation) is a UI package for `uikit-react-native`.
- [**packages/uikit-chat-hooks**](/packages/uikit-chat-hooks) is a react hooks package for `uikit-react-native`.
- [**packages/uikit-utils**](/packages/uikit-utils) is a utility package for `uikit-react-native`.
### More about Sendbird UIKit for React-Native
-Find out more about Sendbird UIKit for React-Native at [UIKit for React Native doc](https://sendbird.com/docs/uikit/v3/react-native/overview).
+Find out more about Sendbird UIKit for React-Native at [UIKit for React Native doc](https://sendbird.com/docs/chat/uikit/v3/react-native/overview).
If you need any help in resolving any issues or have questions, visit [our community](https://community.sendbird.com).
diff --git a/docs-validation/1_introduction/Authentication.tsx b/docs-validation/1_introduction/Authentication.tsx
index fedc77e01..3a3950f68 100644
--- a/docs-validation/1_introduction/Authentication.tsx
+++ b/docs-validation/1_introduction/Authentication.tsx
@@ -5,7 +5,7 @@ const PROFILE_FILE: FileType = { name: '', size: 0, type: '', uri: '' };
/**
* Connect to the Sendbird server
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/authentication#2-connect-to-the-sendbird-server}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/authentication#2-connect-to-the-sendbird-server}
* */
import { useConnection } from '@sendbird/uikit-react-native';
@@ -27,7 +27,7 @@ const Component = () => {
/**
* Disconnect from the Sendbird server
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/authentication#2-disconnect-from-the-sendbird-server}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/authentication#2-disconnect-from-the-sendbird-server}
* */
const Component2 = () => {
const { disconnect } = useConnection();
@@ -37,7 +37,7 @@ const Component2 = () => {
/**
* Retrieve online status of current user
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/authentication#2-retrieve-online-status-of-current-user}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/authentication#2-retrieve-online-status-of-current-user}
* */
import { useSendbirdChat } from '@sendbird/uikit-react-native';
@@ -54,7 +54,7 @@ const Component3 = () => {
/**
* Register for push notifications
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/authentication#2-register-for-push-notifications}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/authentication#2-register-for-push-notifications}
* */
import RNFBMessaging from '@react-native-firebase/messaging';
import * as Permissions from 'react-native-permissions';
@@ -77,7 +77,7 @@ const App = () => {
/**
* Unregister push notifications
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/authentication#2-unregister-push-notifications}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/authentication#2-unregister-push-notifications}
* */
const App2 = () => {
return (
@@ -92,7 +92,7 @@ const App2 = () => {
/**
* Update user profile
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/authentication#2-update-user-profile}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/authentication#2-update-user-profile}
* */
const Component4 = () => {
const { updateCurrentUserInfo } = useSendbirdChat();
diff --git a/docs-validation/1_introduction/NativeModules.tsx b/docs-validation/1_introduction/NativeModules.tsx
index ecb7cc3e7..decadec9c 100644
--- a/docs-validation/1_introduction/NativeModules.tsx
+++ b/docs-validation/1_introduction/NativeModules.tsx
@@ -3,15 +3,19 @@ import {
createExpoFileService,
createExpoMediaService,
createExpoNotificationService,
+ createExpoPlayerService,
+ createExpoRecorderService,
createNativeClipboardService,
createNativeFileService,
createNativeMediaService,
createNativeNotificationService,
+ createNativePlayerService,
+ createNativeRecorderService,
} from '@sendbird/uikit-react-native';
/**
* Helper functions#React Native CLI
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/native-modules#2-helper-functions-3-react-native-cli}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/native-modules#2-helper-functions-3-react-native-cli}
* */
import Clipboard from '@react-native-clipboard/clipboard';
import { CameraRoll } from '@react-native-camera-roll/camera-roll';
@@ -23,29 +27,40 @@ import * as ImagePicker from 'react-native-image-picker';
import * as Permissions from 'react-native-permissions';
import * as CreateThumbnail from 'react-native-create-thumbnail';
import * as ImageResizer from '@bam.tech/react-native-image-resizer';
+import * as AudioRecorderPlayer from 'react-native-audio-recorder-player';
-const NativeClipboardService = createNativeClipboardService(Clipboard);
-const NativeNotificationService = createNativeNotificationService({
- messagingModule: RNFBMessaging,
- permissionModule: Permissions,
-});
-const NativeFileService = createNativeFileService({
- fsModule: FileAccess,
- permissionModule: Permissions,
- imagePickerModule: ImagePicker,
- mediaLibraryModule: CameraRoll,
- documentPickerModule: DocumentPicker,
-});
-const NativeMediaService = createNativeMediaService({
- VideoComponent: Video,
- thumbnailModule: CreateThumbnail,
- imageResizerModule: ImageResizer,
-});
+const nativePlatformServices = {
+ clipboard: createNativeClipboardService(Clipboard),
+ notification: createNativeNotificationService({
+ messagingModule: RNFBMessaging,
+ permissionModule: Permissions,
+ }),
+ file: createNativeFileService({
+ imagePickerModule: ImagePicker,
+ documentPickerModule: DocumentPicker,
+ permissionModule: Permissions,
+ fsModule: FileAccess,
+ mediaLibraryModule: CameraRoll,
+ }),
+ media: createNativeMediaService({
+ VideoComponent: Video,
+ thumbnailModule: CreateThumbnail,
+ imageResizerModule: ImageResizer,
+ }),
+ player: createNativePlayerService({
+ audioRecorderModule: AudioRecorderPlayer,
+ permissionModule: Permissions,
+ }),
+ recorder: createNativeRecorderService({
+ audioRecorderModule: AudioRecorderPlayer,
+ permissionModule: Permissions,
+ }),
+};
/** ------------------ **/
/**
* Helper functions#Expo CLI
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/native-modules#2-helper-functions-3-expo-cli}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/native-modules#2-helper-functions-3-expo-cli}
* */
import * as ExpoClipboard from 'expo-clipboard';
import * as ExpoDocumentPicker from 'expo-document-picker';
@@ -57,18 +72,26 @@ import * as ExpoAV from 'expo-av';
import * as ExpoVideoThumbnail from 'expo-video-thumbnails';
import * as ExpoImageManipulator from 'expo-image-manipulator';
-const ExpoNotificationService = createExpoNotificationService(ExpoNotifications);
-const ExpoClipboardService = createExpoClipboardService(ExpoClipboard);
-const ExpoFileService = createExpoFileService({
- fsModule: ExpoFS,
- imagePickerModule: ExpoImagePicker,
- mediaLibraryModule: ExpoMediaLibrary,
- documentPickerModule: ExpoDocumentPicker,
-});
-const ExpoMediaService = createExpoMediaService({
- avModule: ExpoAV,
- thumbnailModule: ExpoVideoThumbnail,
- imageManipulator: ExpoImageManipulator,
- fsModule: ExpoFS,
-})
+const expoPlatformServices = {
+ clipboard: createExpoClipboardService(ExpoClipboard),
+ notification: createExpoNotificationService(ExpoNotifications),
+ file: createExpoFileService({
+ fsModule: ExpoFS,
+ imagePickerModule: ExpoImagePicker,
+ mediaLibraryModule: ExpoMediaLibrary,
+ documentPickerModule: ExpoDocumentPicker,
+ }),
+ media: createExpoMediaService({
+ avModule: ExpoAV,
+ thumbnailModule: ExpoVideoThumbnail,
+ imageManipulator: ExpoImageManipulator,
+ fsModule: ExpoFS,
+ }),
+ player: createExpoPlayerService({
+ avModule: ExpoAV,
+ }),
+ recorder: createExpoRecorderService({
+ avModule: ExpoAV,
+ }),
+};
/** ------------------ **/
diff --git a/docs-validation/1_introduction/ScreenNavigation.tsx b/docs-validation/1_introduction/ScreenNavigation.tsx
index 8b70eef4b..04bf96822 100644
--- a/docs-validation/1_introduction/ScreenNavigation.tsx
+++ b/docs-validation/1_introduction/ScreenNavigation.tsx
@@ -4,7 +4,7 @@ const GroupChannelScreen = () => ;
/**
* Set up navigation in a fragment
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/screen-navigation#2-set-up-navigation-in-a-fragment}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/screen-navigation#2-set-up-navigation-in-a-fragment}
* */
// @ts-ignore
import { Navigation } from 'react-native-navigation';
@@ -34,7 +34,7 @@ const GroupChannelListScreen = (props: { componentId: string }) => {
/**
* Integrate navigation library
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/screen-navigation#2-integrate-navigation-library}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/screen-navigation#2-integrate-navigation-library}
* */
Navigation.registerComponent('GroupChannel', () => GroupChannelScreen);
Navigation.registerComponent('GroupChannelList', () => GroupChannelListScreen);
diff --git a/docs-validation/1_introduction/SendYourFirstMessage.tsx b/docs-validation/1_introduction/SendYourFirstMessage.tsx
index f1a5774e2..07023e5c1 100644
--- a/docs-validation/1_introduction/SendYourFirstMessage.tsx
+++ b/docs-validation/1_introduction/SendYourFirstMessage.tsx
@@ -2,14 +2,17 @@ import React from 'react';
/**
* Implement platform service interfaces using native modules
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/send-first-message#2-get-started-3-step-4-implement-platform-service-interfaces-using-native-modules}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/send-first-message#2-get-started-3-step-4-implement-platform-service-interfaces-using-native-modules}
* */
import {
createNativeClipboardService,
createNativeFileService,
createNativeMediaService,
createNativeNotificationService,
-} from '@sendbird/uikit-react-native';
+ createNativePlayerService,
+ createNativeRecorderService,
+ SendbirdUIKitContainerProps
+} from "@sendbird/uikit-react-native";
import Clipboard from '@react-native-clipboard/clipboard';
import { CameraRoll } from '@react-native-camera-roll/camera-roll';
@@ -21,29 +24,41 @@ import * as ImagePicker from 'react-native-image-picker';
import * as Permissions from 'react-native-permissions';
import * as CreateThumbnail from 'react-native-create-thumbnail';
import * as ImageResizer from '@bam.tech/react-native-image-resizer';
-
-const ClipboardService = createNativeClipboardService(Clipboard);
-const NotificationService = createNativeNotificationService({
- messagingModule: RNFBMessaging,
- permissionModule: Permissions,
-});
-const FileService = createNativeFileService({
- fsModule: FileAccess,
- permissionModule: Permissions,
- imagePickerModule: ImagePicker,
- mediaLibraryModule: CameraRoll,
- documentPickerModule: DocumentPicker,
-});
-const MediaService = createNativeMediaService({
- VideoComponent: Video,
- thumbnailModule: CreateThumbnail,
- imageResizerModule: ImageResizer,
-});
+import * as AudioRecorderPlayer from 'react-native-audio-recorder-player';
+
+
+export const platformServices: SendbirdUIKitContainerProps['platformServices'] = {
+ clipboard: createNativeClipboardService(Clipboard),
+ notification: createNativeNotificationService({
+ messagingModule: RNFBMessaging,
+ permissionModule: Permissions,
+ }),
+ file: createNativeFileService({
+ imagePickerModule: ImagePicker,
+ documentPickerModule: DocumentPicker,
+ permissionModule: Permissions,
+ fsModule: FileAccess,
+ mediaLibraryModule: CameraRoll,
+ }),
+ media: createNativeMediaService({
+ VideoComponent: Video,
+ thumbnailModule: CreateThumbnail,
+ imageResizerModule: ImageResizer,
+ }),
+ player: createNativePlayerService({
+ audioRecorderModule: AudioRecorderPlayer,
+ permissionModule: Permissions,
+ }),
+ recorder: createNativeRecorderService({
+ audioRecorderModule: AudioRecorderPlayer,
+ permissionModule: Permissions,
+ }),
+};
/** ------------------ **/
/**
* Wrap your app in SendbirdUIKitContainer
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/send-first-message#2-get-started-3-step-5-wrap-your-app-in-sendbirduikitcontainer}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/send-first-message#2-get-started-3-step-5-wrap-your-app-in-sendbirduikitcontainer}
* */
import { SendbirdUIKitContainer } from '@sendbird/uikit-react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
@@ -53,12 +68,7 @@ const App = () => {
{/* Rest of your app */}
@@ -68,7 +78,7 @@ const App = () => {
/**
* Create a fragment and module components
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/send-first-message#2-get-started-3-step-7-create-a-fragment-and-module-components}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/send-first-message#2-get-started-3-step-7-create-a-fragment-and-module-components}
* */
import { useNavigation, useRoute } from '@react-navigation/native';
import { useGroupChannel } from '@sendbird/uikit-chat-hooks';
@@ -146,7 +156,7 @@ const GroupChannelScreen = () => {
/**
* Register navigation library to the screen
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/send-first-message#2-get-started-3-step-8-register-navigation-library-to-the-screen}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/send-first-message#2-get-started-3-step-8-register-navigation-library-to-the-screen}
* */
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
@@ -177,12 +187,7 @@ const App2 = () => {
@@ -192,7 +197,7 @@ const App2 = () => {
/**
* Connect to the Sendbird server
- * {@link https://sendbird.com/docs/uikit/v3/react-native/introduction/send-first-message#2-get-started-3-step-9-connect-to-the-sendbird-server}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/send-first-message#2-get-started-3-step-9-connect-to-the-sendbird-server}
* */
import { Pressable, Text, View } from 'react-native';
import { useConnection } from '@sendbird/uikit-react-native';
diff --git a/docs-validation/2_features/DeliveryReceipt.tsx b/docs-validation/2_features/DeliveryReceipt.tsx
index bf773d338..11b0a280d 100644
--- a/docs-validation/2_features/DeliveryReceipt.tsx
+++ b/docs-validation/2_features/DeliveryReceipt.tsx
@@ -1,6 +1,6 @@
/**
* How to use
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/delivery-receipt#2-how-to-use}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/delivery-receipt#2-how-to-use}
* */
import { SendbirdUIKitContainer } from '@sendbird/uikit-react-native';
@@ -12,7 +12,7 @@ const App = () => {
/**
* Icon resource
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/delivery-receipt#2-customize-the-ui-for-delivery-receipt-3-icon-resource}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/delivery-receipt#2-customize-the-ui-for-delivery-receipt-3-icon-resource}
* */
import { Icon } from '@sendbird/uikit-react-native-foundation';
diff --git a/docs-validation/2_features/FileSharing.tsx b/docs-validation/2_features/FileSharing.tsx
index 2d6debc75..5bec6f2ba 100644
--- a/docs-validation/2_features/FileSharing.tsx
+++ b/docs-validation/2_features/FileSharing.tsx
@@ -9,7 +9,7 @@ const isImageFile = (x: string) => x;
/**
* Customize the UI for file sharing
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/file-sharing#2-customize-the-ui-for-file-sharing}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/file-sharing#2-customize-the-ui-for-file-sharing}
* */
import { createGroupChannelFragment, GroupChannelMessageRenderer } from '@sendbird/uikit-react-native';
@@ -32,7 +32,7 @@ const GroupChannelScreen = () => {
/**
* Color resource
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/file-sharing#2-customize-the-ui-for-file-sharing-3-color-resource}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/file-sharing#2-customize-the-ui-for-file-sharing-3-color-resource}
* */
function _colorResource(colors: UIKitColors) {
colors.ui.groupChannelMessage;
@@ -50,7 +50,7 @@ function _colorResource(colors: UIKitColors) {
/**
* Icon resource
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/file-sharing#2-customize-the-ui-for-file-sharing-3-icon-resource}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/file-sharing#2-customize-the-ui-for-file-sharing-3-icon-resource}
* */
Icon.Assets['add'] = require('your_icons/add_icon.png');
Icon.Assets['document'] = require('your_icons/document_icon.png');
@@ -61,7 +61,7 @@ Icon.Assets['play'] = require('your_icons/play_icon.png');
/**
* String resource
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/file-sharing#2-customize-the-ui-for-file-sharing-3-string-resource}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/file-sharing#2-customize-the-ui-for-file-sharing-3-string-resource}
* */
function _stringResource(str: StringSet) {
str.GROUP_CHANNEL;
@@ -83,7 +83,7 @@ function _stringResource(str: StringSet) {
/**
* Image compression
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/file-sharing#2-image-compression}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/file-sharing#2-image-compression}
* */
import { SendbirdUIKitContainer } from '@sendbird/uikit-react-native';
diff --git a/docs-validation/2_features/ReadReceipt.tsx b/docs-validation/2_features/ReadReceipt.tsx
index 29c31d531..0dec7b514 100644
--- a/docs-validation/2_features/ReadReceipt.tsx
+++ b/docs-validation/2_features/ReadReceipt.tsx
@@ -1,6 +1,6 @@
/**
* How to use
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/read-receipt#2-how-to-use}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/read-receipt#2-how-to-use}
* */
import { SendbirdUIKitContainer } from '@sendbird/uikit-react-native';
const App = () => {
@@ -11,7 +11,7 @@ const App = () => {
/**
* Icon resource
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/read-receipt#2-customize-the-ui-for-delivery-receipt-3-icon-resource}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/read-receipt#2-customize-the-ui-for-delivery-receipt-3-icon-resource}
* */
import { Icon } from '@sendbird/uikit-react-native-foundation';
diff --git a/docs-validation/2_features/TypingIndicator.tsx b/docs-validation/2_features/TypingIndicator.tsx
index 7a6985a2c..5d79b9fec 100644
--- a/docs-validation/2_features/TypingIndicator.tsx
+++ b/docs-validation/2_features/TypingIndicator.tsx
@@ -2,7 +2,7 @@ import type { StringSet } from '@sendbird/uikit-react-native';
/**
* How to use
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/typing-indicator#2-how-to-use}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/typing-indicator#2-how-to-use}
* */
import { SendbirdUIKitContainer } from '@sendbird/uikit-react-native';
const App = () => {
@@ -13,7 +13,7 @@ const App = () => {
/**
* String resource
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/typing-indicator#2-customize-the-ui-for-typing-indicator-3-string-resource}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/typing-indicator#2-customize-the-ui-for-typing-indicator-3-string-resource}
* */
type TypingIndicatorTypings = StringSet['LABELS']['TYPING_INDICATOR_TYPINGS'];
/** ------------------ **/
diff --git a/docs-validation/3_core-components/Hooks.tsx b/docs-validation/3_core-components/Hooks.tsx
index aaf7feaf0..1bb6a7caf 100644
--- a/docs-validation/3_core-components/Hooks.tsx
+++ b/docs-validation/3_core-components/Hooks.tsx
@@ -1,6 +1,6 @@
/**
* useSendbirdChat
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/hooks#-3-usesendbirdchat-}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/hooks#-3-usesendbirdchat-}
* */
import { useSendbirdChat } from '@sendbird/uikit-react-native';
@@ -15,7 +15,7 @@ const Component = () => {
/**
* useConnection
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/hooks#-3-useconnection-}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/hooks#-3-useconnection-}
* */
import { useConnection, SendbirdUIKitContainer } from '@sendbird/uikit-react-native';
@@ -38,7 +38,7 @@ const App = () => {
/**
* useUIKitTheme
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/hooks#-3-useuikittheme-}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/hooks#-3-useuikittheme-}
* */
import { useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
@@ -52,7 +52,7 @@ const Component3 = () => {
/**
* usePlatformService
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/hooks#-3-useplatformservice-}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/hooks#-3-useplatformservice-}
* */
import { usePlatformService } from '@sendbird/uikit-react-native';
@@ -70,7 +70,7 @@ const Component4 = () => {
/**
* useLocalization
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/hooks#-3-uselocalization-}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/hooks#-3-uselocalization-}
* */
import { useLocalization } from '@sendbird/uikit-react-native';
@@ -81,7 +81,7 @@ const Component5 = () => {
/**
* useToast
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/hooks#-3-usetoast-}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/hooks#-3-usetoast-}
* */
import { useToast } from '@sendbird/uikit-react-native-foundation';
@@ -93,7 +93,7 @@ const Component6 = () => {
/**
* usePrompt
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/hooks#-3-useprompt-}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/hooks#-3-useprompt-}
* */
import { usePrompt } from '@sendbird/uikit-react-native-foundation';
@@ -109,7 +109,7 @@ const Component7 = () => {
/**
* useBottomSheet
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/hooks#-3-usebottomsheet-}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/hooks#-3-usebottomsheet-}
* */
import { useBottomSheet } from '@sendbird/uikit-react-native-foundation';
@@ -128,7 +128,7 @@ const Component8 = () => {
/**
* useActionMenu
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/hooks#-3-useactionmenu-}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/hooks#-3-useactionmenu-}
* */
import { useActionMenu } from '@sendbird/uikit-react-native-foundation';
@@ -147,7 +147,7 @@ const Component9 = () => {
/**
* useAlert
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/hooks#-3-usealert-}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/hooks#-3-usealert-}
* */
import { useAlert } from '@sendbird/uikit-react-native-foundation';
diff --git a/docs-validation/3_core-components/Provider/PlatformServiceProvider.tsx b/docs-validation/3_core-components/Provider/PlatformServiceProvider.tsx
index 710c30df0..4e9c32c18 100644
--- a/docs-validation/3_core-components/Provider/PlatformServiceProvider.tsx
+++ b/docs-validation/3_core-components/Provider/PlatformServiceProvider.tsx
@@ -6,7 +6,7 @@ const MyDocumentPickerModule = {
getDocumentAsync: async (_: object) => ({ type: '', mimeType: '', uri: '', size: 0, name: '' }),
};
const MyMediaLibraryModule = { requestPermission: async () => 0, saveToLibrary: async (_: string) => 0 };
-const RNFetchBlob = { config: (_: object) => ({ fetch: async (_: string, __: string) => ({ path: () => '' }) }) };
+const RNFetchBlob = { config: (_: object) => ({ fetch: async (_: string, __: string) => ({ path: () => '' }) }), cacheDir: 'cache' };
type FileCompat = { name: string; uri: string; size: number; type: string };
type SaveRes = null | string;
type OpenMediaLibraryRes = null | Array;
@@ -14,7 +14,7 @@ type GetFileRes = null | FileCompat;
/**
* FileServiceInterface
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/provider/platformserviceprovider#2-fileserviceinterface}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/provider/platformserviceprovider#2-fileserviceinterface}
* */
async function fileServiceInterface(service: FileServiceInterface) {
const mediaType = '' as 'photo' | 'video' | 'all' | undefined;
@@ -50,7 +50,7 @@ async function mediaServiceInterface(service: MediaServiceInterface) {
/**
* Usage
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/provider/platformserviceprovider#2-usage}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/provider/platformserviceprovider#2-usage}
* */
import { usePlatformService } from '@sendbird/uikit-react-native';
@@ -61,7 +61,7 @@ const Component = () => {
/**
* Direct implementation
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/provider/platformserviceprovider#2-direct-implementation}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/provider/platformserviceprovider#2-direct-implementation}
* */
import {
FilePickerResponse,
@@ -113,5 +113,12 @@ class MyFileService implements FileServiceInterface {
return response.path();
}
+ createRecordFilePath(customExtension = 'm4a'): { recordFilePath: string; uri: string } {
+ const filename = `${Date.now()}.${customExtension}`;
+ return {
+ uri: `${RNFetchBlob.cacheDir}/${filename}`,
+ recordFilePath: `${RNFetchBlob.cacheDir}/${filename}`,
+ }
+ }
}
/** ------------------ **/
diff --git a/docs-validation/3_core-components/Provider/SendbirdChatProvider.tsx b/docs-validation/3_core-components/Provider/SendbirdChatProvider.tsx
index 0e314f89f..c96147ffa 100644
--- a/docs-validation/3_core-components/Provider/SendbirdChatProvider.tsx
+++ b/docs-validation/3_core-components/Provider/SendbirdChatProvider.tsx
@@ -3,7 +3,7 @@ import React from 'react';
/**
* SendbirdChatProvider
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/provider/sendbirdchatprovider#1-sendbirdchatprovider}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/provider/sendbirdchatprovider#1-sendbirdchatprovider}
* */
import { useConnection, useSendbirdChat } from '@sendbird/uikit-react-native';
import { Button, Text } from '@sendbird/uikit-react-native-foundation';
diff --git a/docs-validation/3_core-components/SendbirdUIKitContainer.tsx b/docs-validation/3_core-components/SendbirdUIKitContainer.tsx
index 6f11550c7..d4b9ca528 100644
--- a/docs-validation/3_core-components/SendbirdUIKitContainer.tsx
+++ b/docs-validation/3_core-components/SendbirdUIKitContainer.tsx
@@ -3,7 +3,7 @@ import React from 'react';
/**
* SendbirdUIKitContainer
- * {@link https://sendbird.com/docs/uikit/v3/react-native/core-components/sendbirduikitcontainer#1-sendbirduikitcontainer}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/sendbirduikitcontainer#1-sendbirduikitcontainer}
* */
import { SendbirdUIKitContainer } from '@sendbird/uikit-react-native';
diff --git a/docs-validation/4_key-functions/Architecture/Context.tsx b/docs-validation/4_key-functions/Architecture/Context.tsx
index 0a24f9ba3..902e0b5f5 100644
--- a/docs-validation/4_key-functions/Architecture/Context.tsx
+++ b/docs-validation/4_key-functions/Architecture/Context.tsx
@@ -1,6 +1,6 @@
/**
* How to use context
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/architecture/context#2-how-to-use-context}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/architecture/context#2-how-to-use-context}
* */
import React, { useContext } from 'react';
import { Text } from 'react-native';
diff --git a/docs-validation/4_key-functions/Architecture/Fragment.tsx b/docs-validation/4_key-functions/Architecture/Fragment.tsx
index d390f4518..eab6a14cd 100644
--- a/docs-validation/4_key-functions/Architecture/Fragment.tsx
+++ b/docs-validation/4_key-functions/Architecture/Fragment.tsx
@@ -5,7 +5,7 @@ const FriendComponent = (_: { user: SendbirdUser }) => <>>;
/**
* Customize a Fragment
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/architecture/fragment#2-customize-a-fragment}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/architecture/fragment#2-customize-a-fragment}
* */
import { CustomQuery, useUserList } from '@sendbird/uikit-chat-hooks';
import { createUserListModule, useSendbirdChat } from '@sendbird/uikit-react-native';
diff --git a/docs-validation/4_key-functions/Architecture/Module.tsx b/docs-validation/4_key-functions/Architecture/Module.tsx
index a2d471558..639522324 100644
--- a/docs-validation/4_key-functions/Architecture/Module.tsx
+++ b/docs-validation/4_key-functions/Architecture/Module.tsx
@@ -4,7 +4,7 @@ const CustomHeader = () => <>>;
/**
* Create a module
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/architecture/module#2-create-a-module}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/architecture/module#2-create-a-module}
* */
import { createGroupChannelModule } from '@sendbird/uikit-react-native';
const GroupChannelModule = createGroupChannelModule();
@@ -26,7 +26,7 @@ const RenderModule = () => {
/**
* Customize a module
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/architecture/module#2-customize-a-module}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/architecture/module#2-customize-a-module}
* */
import { createGroupChannelFragment } from '@sendbird/uikit-react-native';
@@ -38,7 +38,7 @@ const GroupChannelFragment = createGroupChannelFragment({ Header: CustomHeader }
/**
* Customize a module component
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/architecture/module#2-customize-a-module-3-customize-a-module-component}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/architecture/module#2-customize-a-module-3-customize-a-module-component}
* */
import { Text } from 'react-native';
import type { GroupChannelProps } from '@sendbird/uikit-react-native';
diff --git a/docs-validation/4_key-functions/ChattingInAChannel/ChatInAGroupChannel.tsx b/docs-validation/4_key-functions/ChattingInAChannel/ChatInAGroupChannel.tsx
index 08622bc17..bef2841cd 100644
--- a/docs-validation/4_key-functions/ChattingInAChannel/ChatInAGroupChannel.tsx
+++ b/docs-validation/4_key-functions/ChattingInAChannel/ChatInAGroupChannel.tsx
@@ -4,7 +4,7 @@ const AdvertiseMessage = (_:object) => <>>
/**
* Usage
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-usage}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-usage}
* */
import { useSendbirdChat, createGroupChannelFragment } from "@sendbird/uikit-react-native";
import { useGroupChannel } from "@sendbird/uikit-chat-hooks";
@@ -33,7 +33,7 @@ const GroupChannelScreen = ({ route: { params } }: any) => {
/**
* Context
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-context}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-context}
* */
function _context(_: GroupChannelContextsType) {
const fragment = useContext(_.Fragment);
@@ -52,7 +52,7 @@ function _context(_: GroupChannelContextsType) {
/**
* Fragment
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-context-3-fragment}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-context-3-fragment}
* */
const Component = () => {
const {
@@ -69,7 +69,7 @@ const Component = () => {
/**
* TypingIndicator
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-context-3-typeselector}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-context-3-typeselector}
* */
const Component2 = () => {
const { typingUsers } = useContext(GroupChannelContexts.TypingIndicator);
@@ -78,7 +78,7 @@ const Component2 = () => {
/**
* Customization
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-customization}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-customization}
* */
import React, { useContext, useLayoutEffect } from 'react';
import { Pressable } from 'react-native';
diff --git a/docs-validation/4_key-functions/ChattingInAChannel/ChatInAnOpenChannel.tsx b/docs-validation/4_key-functions/ChattingInAChannel/ChatInAnOpenChannel.tsx
index b444ee305..ca766ea5d 100644
--- a/docs-validation/4_key-functions/ChattingInAChannel/ChatInAnOpenChannel.tsx
+++ b/docs-validation/4_key-functions/ChattingInAChannel/ChatInAnOpenChannel.tsx
@@ -4,7 +4,7 @@ const DonationMessage = (_:object) => <>>
/**
* Usage
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-an-open-channel#2-usage}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-an-open-channel#2-usage}
* */
import { useSendbirdChat, createOpenChannelFragment } from "@sendbird/uikit-react-native";
import { useOpenChannel } from "@sendbird/uikit-chat-hooks";
@@ -35,7 +35,7 @@ const OpenChannelScreen = ({ route: { params } }: any) => {
/**
* Context
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-context}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-context}
* */
function _context(_: OpenChannelContextsType) {
const fragment = useContext(_.Fragment);
@@ -49,7 +49,7 @@ function _context(_: OpenChannelContextsType) {
/**
* Fragment
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-context-3-fragment}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-context-3-fragment}
* */
const Component = () => {
const {
@@ -64,7 +64,7 @@ const Component = () => {
/**
* Customization
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-customization}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/chatting-in-a-channel/chat-in-a-group-channel#2-customization}
* */
import React, { useContext, useLayoutEffect } from 'react';
import { Pressable } from 'react-native';
diff --git a/docs-validation/4_key-functions/ConfiguringChannelSettings/ConfigureGroupChannelSettings.tsx b/docs-validation/4_key-functions/ConfiguringChannelSettings/ConfigureGroupChannelSettings.tsx
index 52d12d54f..25b87bb6e 100644
--- a/docs-validation/4_key-functions/ConfiguringChannelSettings/ConfigureGroupChannelSettings.tsx
+++ b/docs-validation/4_key-functions/ConfiguringChannelSettings/ConfigureGroupChannelSettings.tsx
@@ -2,7 +2,7 @@ import type { GroupChannelSettingsContextsType } from '@sendbird/uikit-react-nat
/**
* Usage
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-group-channel-settings#2-usage}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-group-channel-settings#2-usage}
* */
import React, { useState } from 'react';
import { useSendbirdChat, createGroupChannelSettingsFragment } from '@sendbird/uikit-react-native';
@@ -41,7 +41,7 @@ const GroupChannelSettingsScreen = ({ route: { params } }: any) => {
/**
* Context
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-group-channel-settings#2-context}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-group-channel-settings#2-context}
* */
function _context(_: GroupChannelSettingsContextsType) {
const fragment = useContext(_.Fragment);
@@ -54,7 +54,7 @@ function _context(_: GroupChannelSettingsContextsType) {
/**
* Fragment
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-group-channel-settings#2-context-3-fragment}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-group-channel-settings#2-context-3-fragment}
* */
import { useContext } from 'react';
import { GroupChannelSettingsContexts } from '@sendbird/uikit-react-native';
@@ -66,7 +66,7 @@ const Component = () => {
/**
* Customization
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-group-channel-settings#2-customization}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-group-channel-settings#2-customization}
* */
import { Share } from 'react-native';
diff --git a/docs-validation/4_key-functions/ConfiguringChannelSettings/ConfigureOpenChannelSettings.tsx b/docs-validation/4_key-functions/ConfiguringChannelSettings/ConfigureOpenChannelSettings.tsx
index 59bc40108..36360fcc8 100644
--- a/docs-validation/4_key-functions/ConfiguringChannelSettings/ConfigureOpenChannelSettings.tsx
+++ b/docs-validation/4_key-functions/ConfiguringChannelSettings/ConfigureOpenChannelSettings.tsx
@@ -2,7 +2,7 @@ import type { OpenChannelSettingsContextsType } from '@sendbird/uikit-react-nati
/**
* Usage
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-open-channel-settings#2-usage}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-open-channel-settings#2-usage}
* */
import React, { useState } from 'react';
import { useSendbirdChat, createOpenChannelSettingsFragment } from '@sendbird/uikit-react-native';
@@ -35,7 +35,7 @@ const OpenChannelSettingsScreen = ({ route: { params } }: any) => {
/**
* Context
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-open-channel-settings#2-context}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-open-channel-settings#2-context}
* */
function _context(_: OpenChannelSettingsContextsType) {
const fragment = useContext(_.Fragment);
@@ -48,7 +48,7 @@ function _context(_: OpenChannelSettingsContextsType) {
/**
* Fragment
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-open-channel-settings#2-context-3-fragment}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-open-channel-settings#2-context-3-fragment}
* */
import { useContext } from 'react';
import { OpenChannelSettingsContexts } from '@sendbird/uikit-react-native';
@@ -60,7 +60,7 @@ const Component = () => {
/**
* Customization
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-open-channel-settings#2-customization}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/configuring-channel-settings/configure-open-channel-settings#2-customization}
* */
import { Share } from 'react-native';
diff --git a/docs-validation/4_key-functions/CreatingAChannel/CreateAGroupChannel.tsx b/docs-validation/4_key-functions/CreatingAChannel/CreateAGroupChannel.tsx
index 65e4164ec..90d77d0d1 100644
--- a/docs-validation/4_key-functions/CreatingAChannel/CreateAGroupChannel.tsx
+++ b/docs-validation/4_key-functions/CreatingAChannel/CreateAGroupChannel.tsx
@@ -11,7 +11,7 @@ const createMyAppUserQuery = () => ({
/**
* Usage
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-usage}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-usage}
* */
import type { GroupChannelType, UserListContextsType } from "@sendbird/uikit-react-native";
import type { SendbirdUser } from '@sendbird/uikit-utils';
@@ -34,7 +34,7 @@ const GroupChannelCreateScreen = ({ route: { params } }: any) => {
/**
* Context
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-context}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-context}
* */
function _context(_: UserListContextsType) {
const fragment = useContext(_.Fragment);
@@ -49,7 +49,7 @@ function _context(_: UserListContextsType) {
/**
* Fragment
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-context-3-fragment}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-context-3-fragment}
* */
import { UserListContexts } from "@sendbird/uikit-react-native";
const Component = () => {
@@ -59,7 +59,7 @@ const Component = () => {
/**
* List
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-context-3-list}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-context-3-list}
* */
const Component2 = () => {
@@ -69,7 +69,7 @@ const Component2 = () => {
/**
* Customization
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-customization}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-customization}
* */
import type { UserStruct } from '@sendbird/uikit-utils';
import { CustomQuery } from '@sendbird/uikit-chat-hooks';
diff --git a/docs-validation/4_key-functions/InviteUsers.tsx b/docs-validation/4_key-functions/InviteUsers.tsx
index 656b83cac..75af96f71 100644
--- a/docs-validation/4_key-functions/InviteUsers.tsx
+++ b/docs-validation/4_key-functions/InviteUsers.tsx
@@ -11,7 +11,7 @@ const createMyAppUserQuery = () => ({
/**
* Usage
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-usage}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-usage}
* */
import { useState } from 'react';
import { useSendbirdChat, createGroupChannelInviteFragment } from "@sendbird/uikit-react-native";
@@ -39,7 +39,7 @@ const GroupChannelInviteScreen = ({ route: { params } }: any) => {
/**
* Context
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-context}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-context}
* */
import type { UserListContextsType } from "@sendbird/uikit-react-native";
@@ -56,7 +56,7 @@ function _context(_: UserListContextsType) {
/**
* Fragment
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-context-3-fragment}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-context-3-fragment}
* */
import { useContext } from 'react';
import { UserListContexts } from "@sendbird/uikit-react-native";
@@ -68,7 +68,7 @@ const Component = () => {
/**
* List
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-context-3-list}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-context-3-list}
* */
// import { useContext } from 'react';
// import { UserListContexts } from "@sendbird/uikit-react-native";
@@ -80,7 +80,7 @@ const Component2 = () => {
/**
* Customization
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-customization}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/creating-a-channel/create-a-group-channel#2-customization}
* */
import type { UserStruct } from '@sendbird/uikit-utils';
import { CustomQuery } from '@sendbird/uikit-chat-hooks';
diff --git a/docs-validation/4_key-functions/ListChannels.tsx b/docs-validation/4_key-functions/ListChannels.tsx
index 72e049d7b..2f46e79fa 100644
--- a/docs-validation/4_key-functions/ListChannels.tsx
+++ b/docs-validation/4_key-functions/ListChannels.tsx
@@ -3,7 +3,7 @@ import React, { useContext, useLayoutEffect } from 'react';
/**
* Usage
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/list-channels#2-usage}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/list-channels#2-usage}
* */
import { createGroupChannelListFragment } from '@sendbird/uikit-react-native';
@@ -23,7 +23,7 @@ const GroupChannelListScreen = () => {
/**
* Context
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/list-channels#2-context}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/list-channels#2-context}
* */
function _context(_: GroupChannelListContextsType) {
const fragment = useContext(_.Fragment);
@@ -39,7 +39,7 @@ function _context(_: GroupChannelListContextsType) {
/**
* Fragment
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/list-channels#2-context-3-fragment}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/list-channels#2-context-3-fragment}
* */
// import { useContext } from 'react';
import { GroupChannelListContexts } from '@sendbird/uikit-react-native';
@@ -51,7 +51,7 @@ const Component = () => {
/**
* TypeSelector
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/list-channels#2-context-3-typeselector}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/list-channels#2-context-3-typeselector}
* */
// import { useContext } from 'react';
// import { GroupChannelListContexts } from '@sendbird/uikit-react-native';
@@ -63,7 +63,7 @@ const Component2 = () => {
/**
* Customization
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/list-channels#2-customization}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/list-channels#2-customization}
* */
// import React, { useContext, useLayoutEffect } from 'react';
import { Pressable } from 'react-native';
diff --git a/docs-validation/4_key-functions/ModeratingChannelsAndMembers/ModerateGroupChannelsAndMembers.tsx b/docs-validation/4_key-functions/ModeratingChannelsAndMembers/ModerateGroupChannelsAndMembers.tsx
index 5892e8af6..045d3a5d1 100644
--- a/docs-validation/4_key-functions/ModeratingChannelsAndMembers/ModerateGroupChannelsAndMembers.tsx
+++ b/docs-validation/4_key-functions/ModeratingChannelsAndMembers/ModerateGroupChannelsAndMembers.tsx
@@ -2,7 +2,7 @@ const MyHeader = () => null;
/**
*
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/moderating-channels-and-members/moderate-group-channels-and-members}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/moderating-channels-and-members/moderate-group-channels-and-members}
* */
import React from 'react';
@@ -33,7 +33,7 @@ const GroupChannelModerationScreen = ({ route: { params } }: any) => {
/**
*
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/moderating-channels-and-members/moderate-group-channels-and-members}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/moderating-channels-and-members/moderate-group-channels-and-members}
* */
const GroupChannelModerationFragment2 = createGroupChannelModerationFragment({
Header: () => , // Use custom header
diff --git a/docs-validation/4_key-functions/ModeratingChannelsAndMembers/ModerateOpenChannelsAndParticipants.tsx b/docs-validation/4_key-functions/ModeratingChannelsAndMembers/ModerateOpenChannelsAndParticipants.tsx
index 876c6ba9d..27f9bc66e 100644
--- a/docs-validation/4_key-functions/ModeratingChannelsAndMembers/ModerateOpenChannelsAndParticipants.tsx
+++ b/docs-validation/4_key-functions/ModeratingChannelsAndMembers/ModerateOpenChannelsAndParticipants.tsx
@@ -2,7 +2,7 @@ const MyHeader = () => null;
/**
*
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/moderating-channels-and-members/moderate-group-channels-and-members}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/moderating-channels-and-members/moderate-group-channels-and-members}
* */
import React from 'react';
@@ -33,7 +33,7 @@ const OpenChannelModerationScreen = ({ route: { params } }: any) => {
/**
*
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/moderating-channels-and-members/moderate-group-channels-and-members}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/moderating-channels-and-members/moderate-group-channels-and-members}
* */
const OpenChannelModerationFragment2 = createOpenChannelModerationFragment({
Header: () => , // Use custom header
diff --git a/docs-validation/4_key-functions/Overview.tsx b/docs-validation/4_key-functions/Overview.tsx
index 11aafcdaf..4c65017df 100644
--- a/docs-validation/4_key-functions/Overview.tsx
+++ b/docs-validation/4_key-functions/Overview.tsx
@@ -3,7 +3,7 @@ const useHooksForChat = () => ({ dataA: '', dataB: '' });
/**
* Key functions
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/overview}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/overview}
* */
import React, { createContext, useContext } from 'react';
import { Text } from 'react-native';
diff --git a/docs-validation/4_key-functions/RegisteringAsOperator/RegisterMemberAsOperators.tsx b/docs-validation/4_key-functions/RegisteringAsOperator/RegisterMemberAsOperators.tsx
index dec42689c..8fc0dce28 100644
--- a/docs-validation/4_key-functions/RegisteringAsOperator/RegisterMemberAsOperators.tsx
+++ b/docs-validation/4_key-functions/RegisteringAsOperator/RegisterMemberAsOperators.tsx
@@ -2,7 +2,7 @@ const MyHeader = () => null;
/**
*
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/registering-as-operator-register-member-as-operators}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/registering-as-operator-register-member-as-operators}
* */
import React from 'react';
@@ -29,7 +29,7 @@ const GroupChannelRegisterOperatorScreen = ({ route: { params } }: any) => {
/**
*
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/registering-as-operator-register-member-as-operators}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/registering-as-operator-register-member-as-operators}
* */
const GroupChannelRegisterOperatorFragment2 = createGroupChannelRegisterOperatorFragment({
Header: () => , // Use custom header
diff --git a/docs-validation/4_key-functions/RegisteringAsOperator/RegisterParticipantsAsOperators.tsx b/docs-validation/4_key-functions/RegisteringAsOperator/RegisterParticipantsAsOperators.tsx
index 6c3707f07..abec02d61 100644
--- a/docs-validation/4_key-functions/RegisteringAsOperator/RegisterParticipantsAsOperators.tsx
+++ b/docs-validation/4_key-functions/RegisteringAsOperator/RegisterParticipantsAsOperators.tsx
@@ -2,7 +2,7 @@ const MyHeader = () => null;
/**
*
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/registering-as-operator-register-participants-as-operators}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/registering-as-operator-register-participants-as-operators}
* */
import React from 'react';
@@ -29,7 +29,7 @@ const OpenChannelRegisterOperatorScreen = ({ route: { params } }: any) => {
/**
*
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/registering-as-operator-register-participants-as-operators}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/registering-as-operator-register-participants-as-operators}
* */
const OpenChannelRegisterOperatorFragment2 = createOpenChannelRegisterOperatorFragment({
Header: () => , // Use custom header
diff --git a/docs-validation/4_key-functions/SearchMessage.tsx b/docs-validation/4_key-functions/SearchMessage.tsx
index 1b85229e5..fc20278d0 100644
--- a/docs-validation/4_key-functions/SearchMessage.tsx
+++ b/docs-validation/4_key-functions/SearchMessage.tsx
@@ -1,6 +1,6 @@
/**
* Usage
- * {@link https://sendbird.com/docs/uikit/v3/react-native/key-functions/list-channels#2-usage}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/key-functions/list-channels#2-usage}
* */
import React from 'react';
diff --git a/docs-validation/5_customization/Overview.tsx b/docs-validation/5_customization/Overview.tsx
index 31f1b7042..28e3c0c37 100644
--- a/docs-validation/5_customization/Overview.tsx
+++ b/docs-validation/5_customization/Overview.tsx
@@ -4,7 +4,7 @@ const Analytics = { logError: (_: unknown) => 0 };
/**
* HeaderComponent
- * {@link https://sendbird.com/docs/uikit/v3/react-native/customization/overview#2-sendbirduikitcontainer-3-headercomponent}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/customization/overview#2-sendbirduikitcontainer-3-headercomponent}
* */
import React, { useEffect } from 'react';
import { Pressable } from 'react-native';
@@ -43,7 +43,7 @@ const App = () => {
/**
* ErrorBoundary
- * {@link https://sendbird.com/docs/uikit/v3/react-native/customization/overview#2-sendbirduikitcontainer-3-errorboundary}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/customization/overview#2-sendbirduikitcontainer-3-errorboundary}
* */
import { View } from 'react-native';
// import { Text, Button } from '@sendbird/uikit-react-native-foundation';
diff --git a/docs-validation/6_themes/Overview.tsx b/docs-validation/6_themes/Overview.tsx
index 6f707846d..64e70319b 100644
--- a/docs-validation/6_themes/Overview.tsx
+++ b/docs-validation/6_themes/Overview.tsx
@@ -12,7 +12,7 @@ const CustomPalette: UIKitPalette = Palette;
/**
* Themes
- * {@link https://sendbird.com/docs/uikit/v3/react-native/themes/overview#1-themes}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/themes/overview#1-themes}
* */
import { useColorScheme, View, Text } from 'react-native';
@@ -34,7 +34,7 @@ const App = () => {
/**
* UIKitTheme
- * {@link https://sendbird.com/docs/uikit/v3/react-native/themes/overview#2-uikittheme}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/themes/overview#2-uikittheme}
* */
function colorScheme(_: UIKitColorScheme) {
switch (_) {
@@ -53,7 +53,7 @@ function theme(_: UIKitTheme) {
/**
* How to use
- * {@link https://sendbird.com/docs/uikit/v3/react-native/themes/overview#2-how-to-use}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/themes/overview#2-how-to-use}
* */
import { useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
@@ -69,7 +69,7 @@ const Component = () => {
/**
* Customize the theme
- * {@link https://sendbird.com/docs/uikit/v3/react-native/themes/overview#2-customize-the-theme}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/themes/overview#2-customize-the-theme}
* */
// import { DarkUIKitTheme, createTheme } from '@sendbird/uikit-react-native-foundation';
diff --git a/docs-validation/7_resources/ColorResource.tsx b/docs-validation/7_resources/ColorResource.tsx
index c32c42d2e..60dcb79f2 100644
--- a/docs-validation/7_resources/ColorResource.tsx
+++ b/docs-validation/7_resources/ColorResource.tsx
@@ -10,7 +10,7 @@ import {
/**
* UIKitPalette
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/color-resource#2-uikitpalette}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/color-resource#2-uikitpalette}
* */
import type { UIKitPalette } from '@sendbird/uikit-react-native-foundation';
@@ -63,7 +63,7 @@ const Palette: UIKitPalette = {
/**
* UIKitColors
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/color-resource#2-uikitpalette}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/color-resource#2-uikitpalette}
* */
function uikitColors(_: UIKitColors) {
const {
@@ -99,7 +99,7 @@ const Component = () => {
/**
* How to use
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/color-resource#2-how-to-use}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/color-resource#2-how-to-use}
* */
// import { useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
// import { View, Text } from 'react-native';
@@ -117,7 +117,7 @@ const Component2 = () => {
/**
* Customize with default themes
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/color-resource#2-customize-the-colors-3-customize-with-default-themes}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/color-resource#2-customize-the-colors-3-customize-with-default-themes}
* */
// import { LightUIKitTheme, Palette } from '@sendbird/uikit-react-native-foundation';
@@ -148,7 +148,7 @@ LightUIKitTheme.colors.ui.button.contained = {
/**
* Customize the createTheme()
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/color-resource#2-customize-the-colors-3-customize-with-createtheme-}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/color-resource#2-customize-the-colors-3-customize-with-createtheme-}
* */
import { SendbirdUIKitContainer } from '@sendbird/uikit-react-native';
// import { createTheme } from '@sendbird/uikit-react-native-foundation';
diff --git a/docs-validation/7_resources/IconResource.tsx b/docs-validation/7_resources/IconResource.tsx
index 447564f54..464c7e61c 100644
--- a/docs-validation/7_resources/IconResource.tsx
+++ b/docs-validation/7_resources/IconResource.tsx
@@ -5,7 +5,7 @@ import { Icon } from '@sendbird/uikit-react-native-foundation';
/**
* Icon component
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/icon-resource#2-how-to-use-3-icon-component}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/icon-resource#2-how-to-use-3-icon-component}
* */
// import { Pressable } from 'react-native';
// import { Icon } from '@sendbird/uikit-react-native-foundation';
@@ -21,7 +21,7 @@ const CameraButton = (props: object) => {
/**
* Icon.Assets
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/icon-resource#2-how-to-use-3-icon-assets}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/icon-resource#2-how-to-use-3-icon-assets}
* */
// import { Image, Pressable } from 'react-native';
// import { Icon } from '@sendbird/uikit-react-native-foundation';
@@ -37,7 +37,7 @@ const CameraButton2 = (props: object) => {
/**
* Customize the icons
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/icon-resource#2-customize-the-icons}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/icon-resource#2-customize-the-icons}
* */
// import { Icon } from '@sendbird/uikit-react-native-foundation';
diff --git a/docs-validation/7_resources/StringResource.tsx b/docs-validation/7_resources/StringResource.tsx
index b755e479f..7ec370e62 100644
--- a/docs-validation/7_resources/StringResource.tsx
+++ b/docs-validation/7_resources/StringResource.tsx
@@ -4,7 +4,7 @@ const Navigations = () => <>>;
/**
* Customize the StringSet
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/string-resource#2-customize-the-stringset}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/string-resource#2-customize-the-stringset}
* */
import { View } from 'react-native';
import dateLocale from 'date-fns/locale/ko';
diff --git a/docs-validation/7_resources/TypographyResource.tsx b/docs-validation/7_resources/TypographyResource.tsx
index 0039d0aa4..3a1aca347 100644
--- a/docs-validation/7_resources/TypographyResource.tsx
+++ b/docs-validation/7_resources/TypographyResource.tsx
@@ -3,7 +3,7 @@ import { Text, createTheme } from '@sendbird/uikit-react-native-foundation';
/**
* Text component
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/typography-resource#2-how-to-use-3-text-component}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/typography-resource#2-how-to-use-3-text-component}
* */
const TextList = () => {
return (
@@ -21,7 +21,7 @@ const TextList = () => {
/**
* Typography property
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/typography-resource#2-how-to-use-3-typography-property}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/typography-resource#2-how-to-use-3-typography-property}
* */
import { useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
// import { Text } from 'react-native';
@@ -35,7 +35,7 @@ const Component = () => {
/**
* Customize with default themes
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/typography-resource#2-customize-the-typography-3-customize-with-default-themes}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/typography-resource#2-customize-the-typography-3-customize-with-default-themes}
* */
import { LightUIKitTheme, createTypography } from '@sendbird/uikit-react-native-foundation';
@@ -63,7 +63,7 @@ LightUIKitTheme.typography.h1 = {};
/**
* Customize with createTheme()
- * {@link https://sendbird.com/docs/uikit/v3/react-native/resources/typography-resource#2-customize-the-typography-3-customize-with-createtheme-}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/resources/typography-resource#2-customize-the-typography-3-customize-with-createtheme-}
* */
// import { createTheme, LightUIKitTheme } from '@sendbird/uikit-react-native-foundation';
diff --git a/docs-validation/8_beta-features/MessageSearch.tsx b/docs-validation/8_beta-features/MessageSearch.tsx
index 149a2d2f5..e8859fa82 100644
--- a/docs-validation/8_beta-features/MessageSearch.tsx
+++ b/docs-validation/8_beta-features/MessageSearch.tsx
@@ -24,7 +24,7 @@ const App = () => {
/**
* Customize the UI for message search
- * {@link https://sendbird.com/docs/uikit/v3/react-native/beta-features/message-search#2-customize-the-ui-for-message-search}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/beta-features/message-search#2-customize-the-ui-for-message-search}
* */
const MessageSearchFragment = createMessageSearchFragment();
@@ -42,7 +42,7 @@ const MessageSearchScreen = () => {
/**
* String resource
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/message-search#2-customize-the-ui-for-message-search-3-string-resource}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/message-search#2-customize-the-ui-for-message-search-3-string-resource}
* */
function _stringResource(str: StringSet) {
str.MESSAGE_SEARCH.HEADER_INPUT_PLACEHOLDER
@@ -57,7 +57,7 @@ function _stringResource(str: StringSet) {
/**
* Icon resource
- * {@link https://sendbird.com/docs/uikit/v3/react-native/features/message-search#2-customize-the-ui-for-message-search-3-icon-resource}
+ * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/message-search#2-customize-the-ui-for-message-search-3-icon-resource}
* */
Icon.Assets['photo'] = require('your_icons/photo_icon.png');
Icon.Assets['play'] = require('your_icons/play_icon.png');
diff --git a/package.json b/package.json
index ee57b6767..4842ea216 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"bump:minor": "lerna version minor --no-git-tag-version",
"bump:patch": "lerna version patch --no-git-tag-version",
"deploy:npm": "lerna publish from-git",
+ "deploy:local": "lerna run build && lerna run publish:local",
"make:feat": "yarn workspace @sendbird/uikit-react-native create-domain"
},
"devDependencies": {
@@ -63,7 +64,8 @@
"react-native-safe-area-context": "^3.3.2",
"react-test-renderer": "^17.0.2",
"typedoc": "^0.25.3",
- "typescript": "5.2.2"
+ "typescript": "5.2.2",
+ "yalc": "^1.0.0-pre.53"
},
"jest": {
"preset": "react-native",
@@ -96,7 +98,7 @@
]
},
"resolutions": {
- "@sendbird/chat": "4.9.8",
+ "@sendbird/chat": "4.9.10",
"@types/react": "^18",
"@types/react-native": "^0.67"
}
diff --git a/packages/uikit-chat-hooks/package.json b/packages/uikit-chat-hooks/package.json
index 825536035..eda95e3f9 100644
--- a/packages/uikit-chat-hooks/package.json
+++ b/packages/uikit-chat-hooks/package.json
@@ -26,7 +26,8 @@
"test": "jest",
"build": "bob build",
"clean": "del lib",
- "publish:next": "npm publish --tag next"
+ "publish:next": "npm publish --tag next",
+ "publish:local": "yalc publish"
},
"repository": {
"type": "git",
diff --git a/packages/uikit-react-native-foundation/package.json b/packages/uikit-react-native-foundation/package.json
index 5ebb2b3bd..e707cbbf9 100644
--- a/packages/uikit-react-native-foundation/package.json
+++ b/packages/uikit-react-native-foundation/package.json
@@ -29,6 +29,7 @@
"build": "bob build",
"clean": "del lib",
"publish:next": "npm publish --tag next",
+ "publish:local": "yalc publish",
"generate-icons": "node src/assets/bundle-icons.js"
},
"repository": {
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off-filled.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off-filled.png
new file mode 100644
index 000000000..d733b17e7
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off-filled.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off-filled@2x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off-filled@2x.png
new file mode 100644
index 000000000..bbf6a8555
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off-filled@2x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off-filled@3x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off-filled@3x.png
new file mode 100644
index 000000000..5142f0783
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off-filled@3x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off.png
new file mode 100644
index 000000000..8c347d9c5
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off@2x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off@2x.png
new file mode 100644
index 000000000..137516558
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off@2x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off@3x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off@3x.png
new file mode 100644
index 000000000..536b8ccd7
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-off@3x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on-filled.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on-filled.png
new file mode 100644
index 000000000..f2a761336
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on-filled.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on-filled@2x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on-filled@2x.png
new file mode 100644
index 000000000..d7c893523
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on-filled@2x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on-filled@3x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on-filled@3x.png
new file mode 100644
index 000000000..d4e5910ed
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on-filled@3x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on.png
new file mode 100644
index 000000000..c1631de50
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on@2x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on@2x.png
new file mode 100644
index 000000000..493affeab
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on@2x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on@3x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on@3x.png
new file mode 100644
index 000000000..2844da276
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-audio-on@3x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-pause.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-pause.png
new file mode 100644
index 000000000..b55829037
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-pause.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-pause@2x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-pause@2x.png
new file mode 100644
index 000000000..f4a2e9a8b
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-pause@2x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-pause@3x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-pause@3x.png
new file mode 100644
index 000000000..d2def7b67
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-pause@3x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-recording.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-recording.png
new file mode 100644
index 000000000..9c7d62cd3
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-recording.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-recording@2x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-recording@2x.png
new file mode 100644
index 000000000..ab6b6fb7c
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-recording@2x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-recording@3x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-recording@3x.png
new file mode 100644
index 000000000..ec3f1a33f
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-recording@3x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-stop.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-stop.png
new file mode 100644
index 000000000..200bd6c48
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-stop.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-stop@2x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-stop@2x.png
new file mode 100644
index 000000000..8a0c7bdd7
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-stop@2x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-stop@3x.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-stop@3x.png
new file mode 100644
index 000000000..ad74026b7
Binary files /dev/null and b/packages/uikit-react-native-foundation/src/assets/icon/icon-stop@3x.png differ
diff --git a/packages/uikit-react-native-foundation/src/assets/icon/index.ts b/packages/uikit-react-native-foundation/src/assets/icon/index.ts
index c8401b31a..7943fea40 100644
--- a/packages/uikit-react-native-foundation/src/assets/icon/index.ts
+++ b/packages/uikit-react-native-foundation/src/assets/icon/index.ts
@@ -4,6 +4,10 @@ const IconAssets = {
'add': require('./icon-add.png'),
'archive': require('./icon-archive.png'),
'arrow-left': require('./icon-arrow-left.png'),
+ 'audio-off-filled': require('./icon-audio-off-filled.png'),
+ 'audio-off': require('./icon-audio-off.png'),
+ 'audio-on-filled': require('./icon-audio-on-filled.png'),
+ 'audio-on': require('./icon-audio-on.png'),
'ban': require('./icon-ban.png'),
'broadcast': require('./icon-broadcast.png'),
'camera': require('./icon-camera.png'),
@@ -40,12 +44,14 @@ const IconAssets = {
'notifications-off-filled': require('./icon-notifications-off-filled.png'),
'notifications': require('./icon-notifications.png'),
'operator': require('./icon-operator.png'),
+ 'pause': require('./icon-pause.png'),
'photo': require('./icon-photo.png'),
'play': require('./icon-play.png'),
'plus': require('./icon-plus.png'),
'question': require('./icon-question.png'),
'radio-off': require('./icon-radio-off.png'),
'radio-on': require('./icon-radio-on.png'),
+ 'recording': require('./icon-recording.png'),
'refresh': require('./icon-refresh.png'),
'remove': require('./icon-remove.png'),
'reply-filled': require('./icon-reply-filled.png'),
@@ -54,6 +60,7 @@ const IconAssets = {
'send': require('./icon-send.png'),
'settings-filled': require('./icon-settings-filled.png'),
'spinner': require('./icon-spinner.png'),
+ 'stop': require('./icon-stop.png'),
'streaming': require('./icon-streaming.png'),
'supergroup': require('./icon-supergroup.png'),
'theme': require('./icon-theme.png'),
diff --git a/packages/uikit-react-native-foundation/src/components/Box/index.tsx b/packages/uikit-react-native-foundation/src/components/Box/index.tsx
index dc8badc06..98e76ae07 100644
--- a/packages/uikit-react-native-foundation/src/components/Box/index.tsx
+++ b/packages/uikit-react-native-foundation/src/components/Box/index.tsx
@@ -45,7 +45,7 @@ const Box = ({ style, children, ...props }: BoxProps) => {
const boxStyle = useBoxStyle(props);
return (
-
+
{children}
);
diff --git a/packages/uikit-react-native-foundation/src/components/Icon/index.tsx b/packages/uikit-react-native-foundation/src/components/Icon/index.tsx
index b51bafa9e..28ea9c1c4 100644
--- a/packages/uikit-react-native-foundation/src/components/Icon/index.tsx
+++ b/packages/uikit-react-native-foundation/src/components/Icon/index.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode } from 'react';
+import React from 'react';
import { Image, ImageStyle, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import { FileType, convertFileTypeToMessageType, getFileIconFromMessageType } from '@sendbird/uikit-utils';
@@ -17,10 +17,8 @@ type Props = {
style?: StyleProp;
containerStyle?: StyleProp;
};
-const Icon: ((props: Props) => ReactNode) & {
- Assets: typeof IconAssets;
- File: typeof FileIcon;
-} = ({ icon, color, size = 24, containerStyle, style }) => {
+
+const Icon = ({ icon, color, size = 24, containerStyle, style }: Props) => {
const sizeStyle = sizeStyles[size as SizeFactor] ?? { width: size, height: size };
const { colors } = useUIKitTheme();
return (
@@ -68,6 +66,7 @@ const sizeStyles = createStyleSheet({
},
});
-Icon.Assets = IconAssets;
-Icon.File = FileIcon;
-export default Icon;
+export default Object.assign(Icon, {
+ Assets: IconAssets,
+ File: FileIcon,
+});
diff --git a/packages/uikit-react-native-foundation/src/components/Modal/index.tsx b/packages/uikit-react-native-foundation/src/components/Modal/index.tsx
index 3cbe04667..fc798390c 100644
--- a/packages/uikit-react-native-foundation/src/components/Modal/index.tsx
+++ b/packages/uikit-react-native-foundation/src/components/Modal/index.tsx
@@ -86,7 +86,7 @@ const Modal = ({
void;
onLongPress?: (event: GestureResponderEvent) => void;
delayLongPress?: number;
diff --git a/packages/uikit-react-native-foundation/src/components/ProgressBar/index.tsx b/packages/uikit-react-native-foundation/src/components/ProgressBar/index.tsx
new file mode 100644
index 000000000..53a179765
--- /dev/null
+++ b/packages/uikit-react-native-foundation/src/components/ProgressBar/index.tsx
@@ -0,0 +1,72 @@
+import React, { ReactNode, useEffect, useRef } from 'react';
+import { Animated, Easing, StyleSheet, ViewStyle } from 'react-native';
+
+import { NOOP } from '@sendbird/uikit-utils';
+
+import useUIKitTheme from '../../theme/useUIKitTheme';
+import Box from '../Box';
+
+type Props = {
+ current: number;
+ total: number;
+ trackColor?: string;
+ barColor?: string;
+ overlay?: ReactNode | undefined;
+ style?: ViewStyle;
+};
+const ProgressBar = ({ current = 100, total = 100, trackColor, barColor, overlay, style }: Props) => {
+ const { colors } = useUIKitTheme();
+
+ const uiColors = {
+ track: trackColor ?? colors.primary,
+ bar: barColor ?? colors.onBackground01,
+ };
+
+ const progress = useRef(new Animated.Value(0)).current;
+ const percent = current / total;
+ const stopped = percent === 0;
+
+ useEffect(() => {
+ if (!Number.isNaN(percent)) {
+ const animation = Animated.timing(progress, {
+ toValue: stopped ? 0 : percent,
+ duration: stopped ? 0 : 100,
+ useNativeDriver: false,
+ easing: Easing.linear,
+ });
+
+ animation.start();
+ return () => animation.stop();
+ }
+
+ return NOOP;
+ }, [percent]);
+
+ return (
+
+
+ {overlay}
+
+ );
+};
+
+export default ProgressBar;
diff --git a/packages/uikit-react-native-foundation/src/index.ts b/packages/uikit-react-native-foundation/src/index.ts
index e9d4ae611..47d860989 100644
--- a/packages/uikit-react-native-foundation/src/index.ts
+++ b/packages/uikit-react-native-foundation/src/index.ts
@@ -9,6 +9,7 @@ export { default as Image } from './components/Image';
export { default as ImageWithPlaceholder } from './components/ImageWithPlaceholder';
export { default as Modal } from './components/Modal';
export { default as PressBox } from './components/PressBox';
+export { default as ProgressBar } from './components/ProgressBar';
export { default as RegexText } from './components/RegexText';
export type { RegexTextPattern } from './components/RegexText';
export { default as Switch } from './components/Switch';
diff --git a/packages/uikit-react-native-foundation/src/styles/createScaleFactor.ts b/packages/uikit-react-native-foundation/src/styles/createScaleFactor.ts
index 1e6eb8dcc..363dcaf58 100644
--- a/packages/uikit-react-native-foundation/src/styles/createScaleFactor.ts
+++ b/packages/uikit-react-native-foundation/src/styles/createScaleFactor.ts
@@ -10,9 +10,9 @@ const createScaleFactor = (deviceWidth = DESIGNED_DEVICE_WIDTH) => {
return (dp: number) => PixelRatio.roundToNearestPixel(dp * rangedRatio);
};
-createScaleFactor.updateScaleFactor = (scaleFactor: (dp: number) => number) => {
- DEFAULT_SCALE_FACTOR = scaleFactor;
-};
-
export let DEFAULT_SCALE_FACTOR = createScaleFactor();
-export default createScaleFactor;
+export default Object.assign(createScaleFactor, {
+ updateScaleFactor: (scaleFactor: (dp: number) => number) => {
+ DEFAULT_SCALE_FACTOR = scaleFactor;
+ },
+});
diff --git a/packages/uikit-react-native-foundation/src/theme/DarkUIKitTheme.ts b/packages/uikit-react-native-foundation/src/theme/DarkUIKitTheme.ts
index 4541d7d50..ed0b5a9ad 100644
--- a/packages/uikit-react-native-foundation/src/theme/DarkUIKitTheme.ts
+++ b/packages/uikit-react-native-foundation/src/theme/DarkUIKitTheme.ts
@@ -3,41 +3,6 @@ import createTheme from './createTheme';
const DarkUIKitTheme = createTheme({
colorScheme: 'dark',
colors: (palette) => {
- const groupChannelMessage = {
- incoming: {
- enabled: {
- textMsg: palette.onBackgroundDark01,
- textEdited: palette.onBackgroundDark02,
- textTime: palette.onBackgroundDark03,
- textSenderName: palette.onBackgroundDark02,
- background: palette.background400,
- },
- pressed: {
- textMsg: palette.onBackgroundDark01,
- textEdited: palette.onBackgroundDark02,
- textTime: palette.onBackgroundDark03,
- textSenderName: palette.onBackgroundDark02,
- background: palette.primary500,
- },
- },
- outgoing: {
- enabled: {
- textMsg: palette.onBackgroundLight01,
- textEdited: palette.onBackgroundLight02,
- textTime: palette.onBackgroundDark03,
- textSenderName: palette.transparent,
- background: palette.primary200,
- },
- pressed: {
- textMsg: palette.onBackgroundLight01,
- textEdited: palette.onBackgroundLight02,
- textTime: palette.onBackgroundDark03,
- textSenderName: palette.transparent,
- background: palette.primary300,
- },
- },
- };
-
return {
primary: palette.primary200,
secondary: palette.secondary200,
@@ -149,7 +114,6 @@ const DarkUIKitTheme = createTheme({
},
},
},
- message: groupChannelMessage,
dateSeparator: {
default: {
none: {
@@ -158,7 +122,60 @@ const DarkUIKitTheme = createTheme({
},
},
},
- groupChannelMessage,
+ groupChannelMessage: {
+ incoming: {
+ enabled: {
+ textMsg: palette.onBackgroundDark01,
+ textEdited: palette.onBackgroundDark02,
+ textTime: palette.onBackgroundDark03,
+ textSenderName: palette.onBackgroundDark02,
+ background: palette.background400,
+ textVoicePlaytime: palette.onBackgroundDark01,
+ voiceSpinner: palette.primary200,
+ voiceProgressTrack: palette.background400,
+ voiceActionIcon: palette.primary200,
+ voiceActionIconBackground: palette.background700,
+ },
+ pressed: {
+ textMsg: palette.onBackgroundDark01,
+ textEdited: palette.onBackgroundDark02,
+ textTime: palette.onBackgroundDark03,
+ textSenderName: palette.onBackgroundDark02,
+ background: palette.primary500,
+ textVoicePlaytime: palette.onBackgroundDark01,
+ voiceSpinner: palette.primary200,
+ voiceProgressTrack: palette.background400,
+ voiceActionIcon: palette.primary200,
+ voiceActionIconBackground: palette.background700,
+ },
+ },
+ outgoing: {
+ enabled: {
+ textMsg: palette.onBackgroundLight01,
+ textEdited: palette.onBackgroundLight02,
+ textTime: palette.onBackgroundDark03,
+ textSenderName: palette.transparent,
+ background: palette.primary200,
+ textVoicePlaytime: palette.onBackgroundLight01,
+ voiceSpinner: palette.primary300,
+ voiceProgressTrack: palette.primary200,
+ voiceActionIcon: palette.primary200,
+ voiceActionIconBackground: palette.background700,
+ },
+ pressed: {
+ textMsg: palette.onBackgroundLight01,
+ textEdited: palette.onBackgroundLight02,
+ textTime: palette.onBackgroundDark03,
+ textSenderName: palette.transparent,
+ background: palette.primary300,
+ textVoicePlaytime: palette.onBackgroundLight01,
+ voiceSpinner: palette.primary300,
+ voiceProgressTrack: palette.primary200,
+ voiceActionIcon: palette.primary200,
+ voiceActionIconBackground: palette.background700,
+ },
+ },
+ },
groupChannelPreview: {
default: {
none: {
@@ -243,6 +260,32 @@ const DarkUIKitTheme = createTheme({
},
},
},
+ voiceMessageInput: {
+ default: {
+ active: {
+ textCancel: palette.primary200,
+ textTime: palette.onBackgroundLight01,
+ background: palette.background600,
+ actionIcon: palette.onBackgroundDark01,
+ actionIconBackground: palette.background500,
+ sendIcon: palette.onBackgroundLight01,
+ sendIconBackground: palette.primary200,
+ progressTrack: palette.primary200,
+ recording: palette.error300,
+ },
+ inactive: {
+ textCancel: palette.primary200,
+ textTime: palette.onBackgroundDark03,
+ background: palette.background600,
+ actionIcon: palette.onBackgroundDark01,
+ actionIconBackground: palette.background500,
+ sendIcon: palette.onBackgroundDark04,
+ sendIconBackground: palette.background500,
+ progressTrack: palette.background400,
+ recording: palette.error200,
+ },
+ },
+ },
},
};
},
diff --git a/packages/uikit-react-native-foundation/src/theme/LightUIKitTheme.ts b/packages/uikit-react-native-foundation/src/theme/LightUIKitTheme.ts
index a1a034e61..0466b3277 100644
--- a/packages/uikit-react-native-foundation/src/theme/LightUIKitTheme.ts
+++ b/packages/uikit-react-native-foundation/src/theme/LightUIKitTheme.ts
@@ -3,41 +3,6 @@ import createTheme from './createTheme';
const LightUIKitTheme = createTheme({
colorScheme: 'light',
colors: (palette) => {
- const groupChannelMessage = {
- incoming: {
- enabled: {
- textMsg: palette.onBackgroundLight01,
- textEdited: palette.onBackgroundLight02,
- textTime: palette.onBackgroundLight03,
- textSenderName: palette.onBackgroundLight02,
- background: palette.background100,
- },
- pressed: {
- textMsg: palette.onBackgroundLight01,
- textEdited: palette.onBackgroundLight02,
- textTime: palette.onBackgroundLight03,
- textSenderName: palette.onBackgroundLight02,
- background: palette.primary100,
- },
- },
- outgoing: {
- enabled: {
- textMsg: palette.onBackgroundDark01,
- textEdited: palette.onBackgroundDark02,
- textTime: palette.onBackgroundLight03,
- textSenderName: palette.transparent,
- background: palette.primary300,
- },
- pressed: {
- textMsg: palette.onBackgroundDark01,
- textEdited: palette.onBackgroundDark02,
- textTime: palette.onBackgroundLight03,
- textSenderName: palette.transparent,
- background: palette.primary400,
- },
- },
- };
-
return {
primary: palette.primary300,
secondary: palette.secondary300,
@@ -149,7 +114,6 @@ const LightUIKitTheme = createTheme({
},
},
},
- message: groupChannelMessage,
dateSeparator: {
default: {
none: {
@@ -158,7 +122,60 @@ const LightUIKitTheme = createTheme({
},
},
},
- groupChannelMessage,
+ groupChannelMessage: {
+ incoming: {
+ enabled: {
+ textMsg: palette.onBackgroundLight01,
+ textEdited: palette.onBackgroundLight02,
+ textTime: palette.onBackgroundLight03,
+ textSenderName: palette.onBackgroundLight02,
+ background: palette.background100,
+ textVoicePlaytime: palette.onBackgroundLight01,
+ voiceSpinner: palette.primary300,
+ voiceProgressTrack: palette.background100,
+ voiceActionIcon: palette.primary300,
+ voiceActionIconBackground: palette.background50,
+ },
+ pressed: {
+ textMsg: palette.onBackgroundLight01,
+ textEdited: palette.onBackgroundLight02,
+ textTime: palette.onBackgroundLight03,
+ textSenderName: palette.onBackgroundLight02,
+ background: palette.primary100,
+ textVoicePlaytime: palette.onBackgroundLight01,
+ voiceSpinner: palette.primary300,
+ voiceProgressTrack: palette.background100,
+ voiceActionIcon: palette.primary300,
+ voiceActionIconBackground: palette.background50,
+ },
+ },
+ outgoing: {
+ enabled: {
+ textMsg: palette.onBackgroundDark01,
+ textEdited: palette.onBackgroundDark02,
+ textTime: palette.onBackgroundLight03,
+ textSenderName: palette.transparent,
+ background: palette.primary300,
+ textVoicePlaytime: palette.onBackgroundDark01,
+ voiceSpinner: palette.primary200,
+ voiceProgressTrack: palette.primary300,
+ voiceActionIcon: palette.primary300,
+ voiceActionIconBackground: palette.background50,
+ },
+ pressed: {
+ textMsg: palette.onBackgroundDark01,
+ textEdited: palette.onBackgroundDark02,
+ textTime: palette.onBackgroundLight03,
+ textSenderName: palette.transparent,
+ background: palette.primary400,
+ textVoicePlaytime: palette.onBackgroundDark01,
+ voiceSpinner: palette.primary200,
+ voiceProgressTrack: palette.primary300,
+ voiceActionIcon: palette.primary300,
+ voiceActionIconBackground: palette.background50,
+ },
+ },
+ },
groupChannelPreview: {
default: {
none: {
@@ -243,6 +260,32 @@ const LightUIKitTheme = createTheme({
},
},
},
+ voiceMessageInput: {
+ default: {
+ active: {
+ textCancel: palette.primary300,
+ textTime: palette.onBackgroundDark01,
+ background: palette.background50,
+ actionIcon: palette.onBackgroundLight01,
+ actionIconBackground: palette.background100,
+ sendIcon: palette.onBackgroundDark01,
+ sendIconBackground: palette.primary300,
+ progressTrack: palette.primary300,
+ recording: palette.error300,
+ },
+ inactive: {
+ textCancel: palette.primary300,
+ textTime: palette.onBackgroundLight03,
+ background: palette.background50,
+ actionIcon: palette.onBackgroundLight01,
+ actionIconBackground: palette.background100,
+ sendIcon: palette.onBackgroundLight04,
+ sendIconBackground: palette.background100,
+ progressTrack: palette.background100,
+ recording: palette.error300,
+ },
+ },
+ },
},
};
},
diff --git a/packages/uikit-react-native-foundation/src/types.ts b/packages/uikit-react-native-foundation/src/types.ts
index 6b19e4288..f9cd9d426 100644
--- a/packages/uikit-react-native-foundation/src/types.ts
+++ b/packages/uikit-react-native-foundation/src/types.ts
@@ -43,7 +43,8 @@ export type Component =
| 'ProfileCard'
| 'Reaction'
| 'OpenChannelMessage'
- | 'OpenChannelPreview';
+ | 'OpenChannelPreview'
+ | 'VoiceMessageInput';
export type GetColorTree<
Tree extends {
@@ -74,6 +75,7 @@ export type ComponentColorTree = GetColorTree<{
Reaction: 'default' | 'rounded';
OpenChannelMessage: 'default';
OpenChannelPreview: 'default';
+ VoiceMessageInput: 'default';
};
State: {
Header: 'none';
@@ -89,6 +91,7 @@ export type ComponentColorTree = GetColorTree<{
Reaction: 'enabled' | 'selected';
OpenChannelMessage: 'enabled' | 'pressed';
OpenChannelPreview: 'none';
+ VoiceMessageInput: 'active' | 'inactive';
};
ColorPart: {
Header: 'background' | 'borderBottom';
@@ -98,7 +101,17 @@ export type ComponentColorTree = GetColorTree<{
Badge: 'text' | 'background';
Placeholder: 'content' | 'highlight';
DateSeparator: 'text' | 'background';
- GroupChannelMessage: 'textMsg' | 'textEdited' | 'textSenderName' | 'textTime' | 'background';
+ GroupChannelMessage:
+ | 'textMsg'
+ | 'textEdited'
+ | 'textSenderName'
+ | 'textTime'
+ | 'textVoicePlaytime'
+ | 'background'
+ | 'voiceSpinner'
+ | 'voiceProgressTrack'
+ | 'voiceActionIcon'
+ | 'voiceActionIconBackground';
GroupChannelPreview:
| 'textTitle'
| 'textTitleCaption'
@@ -128,6 +141,16 @@ export type ComponentColorTree = GetColorTree<{
| 'background'
| 'coverBackground'
| 'separator';
+ VoiceMessageInput:
+ | 'textCancel'
+ | 'textTime'
+ | 'background'
+ | 'actionIcon'
+ | 'actionIconBackground'
+ | 'sendIcon'
+ | 'sendIconBackground'
+ | 'progressTrack'
+ | 'recording';
};
}>;
export type ComponentColors = {
@@ -175,6 +198,7 @@ export interface UIKitColors {
reaction: ComponentColors<'Reaction'>;
openChannelMessage: ComponentColors<'OpenChannelMessage'>;
openChannelPreview: ComponentColors<'OpenChannelPreview'>;
+ voiceMessageInput: ComponentColors<'VoiceMessageInput'>;
};
}
diff --git a/packages/uikit-react-native-foundation/src/ui/Avatar/index.tsx b/packages/uikit-react-native-foundation/src/ui/Avatar/index.tsx
index d900937cb..e819ed179 100644
--- a/packages/uikit-react-native-foundation/src/ui/Avatar/index.tsx
+++ b/packages/uikit-react-native-foundation/src/ui/Avatar/index.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode, useState } from 'react';
+import React, { useState } from 'react';
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import { conditionChaining } from '@sendbird/uikit-utils';
@@ -10,10 +10,6 @@ import useUIKitTheme from '../../theme/useUIKitTheme';
import AvatarGroup from './AvatarGroup';
import AvatarIcon from './AvatarIcon';
-type SubComponents = {
- Group: typeof AvatarGroup;
- Icon: typeof AvatarIcon;
-};
type Props = {
uri?: string;
size?: number;
@@ -21,13 +17,8 @@ type Props = {
muted?: boolean;
containerStyle?: StyleProp;
};
-const Avatar: ((props: Props) => ReactNode) & SubComponents = ({
- uri,
- square,
- muted = false,
- size = 56,
- containerStyle,
-}) => {
+
+const Avatar = ({ uri, square, muted = false, size = 56, containerStyle }: Props) => {
const { colors, palette } = useUIKitTheme();
const [loadFailure, setLoadFailure] = useState(false);
@@ -74,6 +65,7 @@ const styles = createStyleSheet({
},
});
-Avatar.Group = AvatarGroup;
-Avatar.Icon = AvatarIcon;
-export default Avatar;
+export default Object.assign(Avatar, {
+ Group: AvatarGroup,
+ Icon: AvatarIcon,
+});
diff --git a/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/Message.file.voice.tsx b/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/Message.file.voice.tsx
new file mode 100644
index 000000000..2a9924c43
--- /dev/null
+++ b/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/Message.file.voice.tsx
@@ -0,0 +1,123 @@
+import React, { useEffect, useState } from 'react';
+
+import type { SendbirdFileMessage } from '@sendbird/uikit-utils';
+import { millsToMSS } from '@sendbird/uikit-utils';
+
+import Box from '../../components/Box';
+import Icon from '../../components/Icon';
+import PressBox from '../../components/PressBox';
+import ProgressBar from '../../components/ProgressBar';
+import Text from '../../components/Text';
+import createStyleSheet from '../../styles/createStyleSheet';
+import useUIKitTheme from '../../theme/useUIKitTheme';
+import LoadingSpinner from '../LoadingSpinner';
+import MessageContainer from './MessageContainer';
+import type { GroupChannelMessageProps } from './index';
+
+export type VoiceFileMessageState = {
+ status: 'preparing' | 'playing' | 'paused';
+ currentTime: number;
+ duration: number;
+};
+
+type Props = GroupChannelMessageProps<
+ SendbirdFileMessage,
+ {
+ durationMetaArrayKey?: string;
+ onUnmount: () => void;
+ }
+>;
+const VoiceFileMessage = (props: Props) => {
+ const {
+ onLongPress,
+ variant = 'incoming',
+ onToggleVoiceMessage,
+ message,
+ durationMetaArrayKey = 'KEY_VOICE_MESSAGE_DURATION',
+ onUnmount,
+ } = props;
+
+ const { colors } = useUIKitTheme();
+
+ const [state, setState] = useState(() => {
+ const meta = message.metaArrays.find((it) => it.key === durationMetaArrayKey);
+ const value = meta?.value?.[0];
+ const initialDuration = value ? parseInt(value, 10) : 0;
+ return {
+ status: 'paused',
+ currentTime: 0,
+ duration: initialDuration,
+ };
+ });
+
+ useEffect(() => {
+ return () => {
+ onUnmount();
+ };
+ }, []);
+
+ const uiColors = colors.ui.groupChannelMessage[variant];
+ const remainingTime = state.duration - state.currentTime;
+
+ return (
+
+
+ onToggleVoiceMessage?.(state, setState)} onLongPress={onLongPress}>
+
+ {state.status === 'preparing' ? (
+
+ ) : (
+
+ )}
+
+ {millsToMSS(state.currentTime === 0 ? state.duration : remainingTime)}
+
+
+ }
+ />
+
+ {props.children}
+
+
+ );
+};
+
+const styles = createStyleSheet({
+ container: {
+ borderRadius: 16,
+ overflow: 'hidden',
+ },
+ image: {
+ maxWidth: 240,
+ width: 240,
+ height: 160,
+ borderRadius: 16,
+ overflow: 'hidden',
+ },
+});
+
+export default VoiceFileMessage;
diff --git a/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/Message.unknown.tsx b/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/Message.unknown.tsx
index 07439ab96..5f8288257 100644
--- a/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/Message.unknown.tsx
+++ b/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/Message.unknown.tsx
@@ -14,7 +14,7 @@ const UnknownMessage = (props: GroupChannelMessageProps) => {
const { variant = 'incoming' } = props;
const { colors } = useUIKitTheme();
- const color = colors.ui.groupChannelMessage['incoming'];
+ const color = colors.ui.groupChannelMessage[variant];
const titleColor = variant === 'incoming' ? colors.onBackground01 : colors.onBackgroundReverse01;
const descColor = variant === 'incoming' ? colors.onBackground02 : colors.onBackgroundReverse02;
diff --git a/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/index.ts b/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/index.ts
index af071158f..bc46a214e 100644
--- a/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/index.ts
+++ b/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/index.ts
@@ -6,6 +6,8 @@ import AdminMessage from './Message.admin';
import FileMessage from './Message.file';
import ImageFileMessage from './Message.file.image';
import VideoFileMessage from './Message.file.video';
+import VoiceFileMessage from './Message.file.voice';
+import type { VoiceFileMessageState } from './Message.file.voice';
import UnknownMessage from './Message.unknown';
import UserMessage from './Message.user';
import OpenGraphUser from './Message.user.og';
@@ -34,6 +36,10 @@ export type GroupChannelMessageProps void;
onPressURL?: (url: string) => void;
onPressMentionedUser?: (mentionedUser?: SendbirdUser) => void;
+ onToggleVoiceMessage?: (
+ state: VoiceFileMessageState,
+ setState: React.Dispatch>,
+ ) => Promise;
} & AdditionalProps;
const GroupChannelMessage = {
@@ -42,6 +48,7 @@ const GroupChannelMessage = {
File: FileMessage,
ImageFile: ImageFileMessage,
VideoFile: VideoFileMessage,
+ VoiceFile: VoiceFileMessage,
Admin: AdminMessage,
Unknown: UnknownMessage,
};
diff --git a/packages/uikit-react-native-foundation/src/ui/Header/index.tsx b/packages/uikit-react-native-foundation/src/ui/Header/index.tsx
index 54983724b..c9e231c28 100644
--- a/packages/uikit-react-native-foundation/src/ui/Header/index.tsx
+++ b/packages/uikit-react-native-foundation/src/ui/Header/index.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode } from 'react';
+import React from 'react';
import { TouchableOpacity, TouchableOpacityProps, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -26,11 +26,7 @@ export type HeaderProps = BaseHeaderProps<
>;
const AlignMapper = { left: 'flex-start', center: 'center', right: 'flex-end' } as const;
-const Header: ((props: HeaderProps) => ReactNode) & {
- Button: typeof HeaderButton;
- Title: typeof HeaderTitle;
- Subtitle: typeof HeaderSubtitle;
-} = ({
+const Header = ({
children,
titleAlign,
title = null,
@@ -41,7 +37,7 @@ const Header: ((props: HeaderProps) => ReactNode) & {
clearTitleMargin = false,
clearStatusBarTopInset = false,
statusBarTopInsetAs = 'padding',
-}) => {
+}: HeaderProps) => {
const { topInset, defaultTitleAlign, defaultHeight } = useHeaderStyle();
const { colors } = useUIKitTheme();
@@ -183,7 +179,8 @@ const styles = createStyleSheet({
},
});
-Header.Button = HeaderButton;
-Header.Title = HeaderTitle;
-Header.Subtitle = HeaderSubtitle;
-export default Header;
+export default Object.assign(Header, {
+ Button: HeaderButton,
+ Title: HeaderTitle,
+ Subtitle: HeaderSubtitle,
+});
diff --git a/packages/uikit-react-native/README.md b/packages/uikit-react-native/README.md
index 228782ae9..6b4713793 100644
--- a/packages/uikit-react-native/README.md
+++ b/packages/uikit-react-native/README.md
@@ -78,6 +78,7 @@ Add the following permissions to your `android/app/src/main/AndroidManifest.xml`
package="com.your.app">
+
@@ -123,6 +124,8 @@ const App = () => {
notification: NotificationService,
clipboard: ClipboardService,
media: MediaService,
+ recorder: RecorderService,
+ player: PlayerService,
}}
>
{/* ... */}
@@ -134,7 +137,7 @@ const App = () => {
In order to implement the interfaces to your React Native app more easily, we provide various helper functions for each interface.
> **NOTE**: Helper function is not required! You can implement it with native modules you're using.
-> More details about PlatformService interfaces, please see [here](https://sendbird.com/docs/uikit/v3/react-native/core-components/provider/platformserviceprovider)
+> More details about PlatformService interfaces, please see [here](https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/provider/platformserviceprovider)
**Using React Native CLI**
@@ -147,6 +150,7 @@ npm install react-native-video \
react-native-image-picker \
react-native-document-picker \
react-native-create-thumbnail \
+ react-native-audio-recorder-player \
@react-native-clipboard/clipboard \
@react-native-camera-roll/camera-roll \
@react-native-firebase/app \
@@ -157,34 +161,45 @@ npx pod-install
```
```ts
-import * as ImageResizer from '@bam.tech/react-native-image-resizer';
-import { CameraRoll } from '@react-native-camera-roll/camera-roll';
import Clipboard from '@react-native-clipboard/clipboard';
+import { CameraRoll } from '@react-native-camera-roll/camera-roll';
import RNFBMessaging from '@react-native-firebase/messaging';
-import * as CreateThumbnail from 'react-native-create-thumbnail';
+import Video from 'react-native-video';
import * as DocumentPicker from 'react-native-document-picker';
import * as FileAccess from 'react-native-file-access';
import * as ImagePicker from 'react-native-image-picker';
import * as Permissions from 'react-native-permissions';
-import Video from 'react-native-video';
-
-const NativeClipboardService = createNativeClipboardService(Clipboard);
-const NativeNotificationService = createNativeNotificationService({
- messagingModule: RNFBMessaging,
- permissionModule: Permissions,
-});
-const NativeFileService = createNativeFileService({
- fsModule: FileAccess,
- permissionModule: Permissions,
- imagePickerModule: ImagePicker,
- mediaLibraryModule: CameraRoll,
- documentPickerModule: DocumentPicker,
-});
-const NativeMediaService = createNativeMediaService({
- VideoComponent: Video,
- thumbnailModule: CreateThumbnail,
- imageResizerModule: ImageResizer,
-});
+import * as CreateThumbnail from 'react-native-create-thumbnail';
+import * as ImageResizer from '@bam.tech/react-native-image-resizer';
+import * as AudioRecorderPlayer from 'react-native-audio-recorder-player';
+
+const nativePlatformServices = {
+ clipboard: createNativeClipboardService(Clipboard),
+ notification: createNativeNotificationService({
+ messagingModule: RNFBMessaging,
+ permissionModule: Permissions,
+ }),
+ file: createNativeFileService({
+ imagePickerModule: ImagePicker,
+ documentPickerModule: DocumentPicker,
+ permissionModule: Permissions,
+ fsModule: FileAccess,
+ mediaLibraryModule: CameraRoll,
+ }),
+ media: createNativeMediaService({
+ VideoComponent: Video,
+ thumbnailModule: CreateThumbnail,
+ imageResizerModule: ImageResizer,
+ }),
+ player: createNativePlayerService({
+ audioRecorderModule: AudioRecorderPlayer,
+ permissionModule: Permissions,
+ }),
+ recorder: createNativeRecorderService({
+ audioRecorderModule: AudioRecorderPlayer,
+ permissionModule: Permissions,
+ }),
+};
```
**Using Expo CLI**
@@ -204,30 +219,38 @@ expo install expo-image-picker \
```
```ts
-import * as ExpoAV from 'expo-av';
import * as ExpoClipboard from 'expo-clipboard';
import * as ExpoDocumentPicker from 'expo-document-picker';
import * as ExpoFS from 'expo-file-system';
-import * as ExpoImageManipulator from 'expo-image-manipulator';
import * as ExpoImagePicker from 'expo-image-picker';
import * as ExpoMediaLibrary from 'expo-media-library';
import * as ExpoNotifications from 'expo-notifications';
+import * as ExpoAV from 'expo-av';
import * as ExpoVideoThumbnail from 'expo-video-thumbnails';
+import * as ExpoImageManipulator from 'expo-image-manipulator';
-const ExpoNotificationService = createExpoNotificationService(ExpoNotifications);
-const ExpoClipboardService = createExpoClipboardService(ExpoClipboard);
-const ExpoFileService = createExpoFileService({
- fsModule: ExpoFS,
- imagePickerModule: ExpoImagePicker,
- mediaLibraryModule: ExpoMediaLibrary,
- documentPickerModule: ExpoDocumentPicker,
-});
-const ExpoMediaService = createExpoMediaService({
- avModule: ExpoAV,
- thumbnailModule: ExpoVideoThumbnail,
- imageManipulator: ExpoImageManipulator,
- fsModule: ExpoFS,
-});
+const expoPlatformServices = {
+ clipboard: createExpoClipboardService(ExpoClipboard),
+ notification: createExpoNotificationService(ExpoNotifications),
+ file: createExpoFileService({
+ fsModule: ExpoFS,
+ imagePickerModule: ExpoImagePicker,
+ mediaLibraryModule: ExpoMediaLibrary,
+ documentPickerModule: ExpoDocumentPicker,
+ }),
+ media: createExpoMediaService({
+ avModule: ExpoAV,
+ thumbnailModule: ExpoVideoThumbnail,
+ imageManipulator: ExpoImageManipulator,
+ fsModule: ExpoFS,
+ }),
+ player: createExpoPlayerService({
+ avModule: ExpoAV,
+ }),
+ recorder: createExpoRecorderService({
+ avModule: ExpoAV,
+ }),
+};
```
### Local caching (required)
@@ -236,7 +259,7 @@ You can implement Local caching easily.
```shell
npm i @react-native-async-storage/async-storage
-npx pod-isntall
+npx pod-install
```
```tsx
@@ -280,7 +303,7 @@ const App = () => {
### Integration with navigation library
Now you can create a screen and integrate it with a navigation library like [`react-navigation`](https://reactnavigation.org/).
-See more details on [here](https://st.sendbird.com/docs/uikit/v3/react-native/introduction/screen-navigation)
+See more details on [here](https://sendbird.com/docs/chat/uikit/v3/react-native/introduction/screen-navigation)
The example below shows how to integrate using `react-navigation`.
@@ -420,6 +443,8 @@ const App = () => {
notification: NotificationService,
clipboard: ClipboardService,
media: MediaService,
+ recorder: RecorderService,
+ player: PlayerService,
}}
>
@@ -429,4 +454,4 @@ const App = () => {
```
> You can use sendbird sdk using `useSendbirdChat()` hook, and you can connect or disconnect using `useConnection()` hook.
-> for more details about hooks, please refer to our [docs](https://sendbird.com/docs/uikit/v3/react-native/core-components/hooks)
+> for more details about hooks, please refer to our [docs](https://sendbird.com/docs/chat/uikit/v3/react-native/core-components/hooks)
diff --git a/packages/uikit-react-native/package.json b/packages/uikit-react-native/package.json
index 8e8c84d70..417bd552d 100644
--- a/packages/uikit-react-native/package.json
+++ b/packages/uikit-react-native/package.json
@@ -39,6 +39,7 @@
"build": "yarn generate-version && bob build",
"clean": "del lib",
"publish:next": "npm publish --tag next",
+ "publish:local": "yalc publish",
"generate-version": "node scripts/generate-version.js ./src/version.ts",
"create-domain": "node scripts/create-core-domain"
},
@@ -89,6 +90,7 @@
"js-convert-case": "^4.2.0",
"react": "17.0.2",
"react-native": "0.67.5",
+ "react-native-audio-recorder-player": "^3.6.0",
"react-native-builder-bob": "^0.18.0",
"react-native-create-thumbnail": "^1.5.1",
"react-native-document-picker": "^8.0.0",
@@ -118,6 +120,7 @@
"expo-video-thumbnails": ">=6.4.0",
"react": ">=17.0.2",
"react-native": ">=0.65.0",
+ "react-native-audio-recorder-player": ">=3.6.0",
"react-native-create-thumbnail": ">=1.5.1",
"react-native-document-picker": ">=8.0.0",
"react-native-file-access": ">=2.4.3",
diff --git a/packages/uikit-react-native/src/components/ChannelInput/MessageToReplyPreview.tsx b/packages/uikit-react-native/src/components/ChannelInput/MessageToReplyPreview.tsx
new file mode 100644
index 000000000..6bbfe0743
--- /dev/null
+++ b/packages/uikit-react-native/src/components/ChannelInput/MessageToReplyPreview.tsx
@@ -0,0 +1,133 @@
+import React from 'react';
+import { TouchableOpacity, View } from 'react-native';
+
+import {
+ Icon,
+ ImageWithPlaceholder,
+ Text,
+ VideoThumbnail,
+ createStyleSheet,
+ useUIKitTheme,
+} from '@sendbird/uikit-react-native-foundation';
+import {
+ FileIcon,
+ SendbirdBaseMessage,
+ SendbirdFileMessage,
+ SendbirdUserMessage,
+ getFileIconFromMessageType,
+ getMessageType,
+ getThumbnailUriFromFileMessage,
+} from '@sendbird/uikit-utils';
+
+import { useLocalization, usePlatformService } from '../../hooks/useContext';
+
+export type MessageToReplyPreviewProps = {
+ messageToReply?: SendbirdFileMessage | SendbirdUserMessage;
+ setMessageToReply?: (message?: undefined | SendbirdFileMessage | SendbirdUserMessage) => void;
+};
+
+export const MessageToReplyPreview = ({ messageToReply, setMessageToReply }: MessageToReplyPreviewProps) => {
+ const { colors, select, palette } = useUIKitTheme();
+ const { mediaService } = usePlatformService();
+ const { STRINGS } = useLocalization();
+
+ const getFileIconAsImage = (url: string) => {
+ return ;
+ };
+
+ const getFileIconAsVideoThumbnail = (url: string) => {
+ return (
+ mediaService.getVideoThumbnail({ url: uri, timeMills: 1000 })}
+ />
+ );
+ };
+
+ const getFileIconAsSymbol = (icon: FileIcon) => {
+ return (
+
+ );
+ };
+
+ const getFileIcon = (messageToReply: SendbirdBaseMessage) => {
+ if (messageToReply?.isFileMessage()) {
+ const messageType = getMessageType(messageToReply);
+ switch (messageType) {
+ case 'file.image':
+ return getFileIconAsImage(getThumbnailUriFromFileMessage(messageToReply));
+ case 'file.video':
+ return getFileIconAsVideoThumbnail(getThumbnailUriFromFileMessage(messageToReply));
+ case 'file.voice':
+ return null;
+ default:
+ return getFileIconAsSymbol(getFileIconFromMessageType(messageType));
+ }
+ }
+ return null;
+ };
+
+ if (!messageToReply) return null;
+
+ return (
+
+
+ {getFileIcon(messageToReply)}
+
+
+ {STRINGS.LABELS.CHANNEL_INPUT_REPLY_PREVIEW_TITLE(messageToReply.sender)}
+
+
+ {STRINGS.LABELS.CHANNEL_INPUT_REPLY_PREVIEW_BODY(messageToReply)}
+
+
+
+ setMessageToReply?.(undefined)}>
+
+
+
+ );
+};
+
+const styles = createStyleSheet({
+ previewImage: {
+ width: 36,
+ height: 36,
+ borderRadius: 10,
+ marginTop: 2,
+ marginRight: 10,
+ overflow: 'hidden',
+ },
+ messageToReplyContainer: {
+ flexDirection: 'row',
+ paddingLeft: 18,
+ paddingRight: 16,
+ paddingTop: 10,
+ paddingBottom: 8,
+ alignItems: 'center',
+ borderTopWidth: 1,
+ },
+ fileIcon: {
+ width: 36,
+ height: 36,
+ borderRadius: 10,
+ marginRight: 10,
+ marginTop: 2,
+ },
+ closeIcon: {
+ marginLeft: 4,
+ padding: 4,
+ },
+});
diff --git a/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx b/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx
index cd1a53c0e..19cd66d67 100644
--- a/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx
+++ b/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx
@@ -8,35 +8,22 @@ import {
View,
} from 'react-native';
-import { MentionType } from '@sendbird/chat/message';
-import type { BottomSheetItem } from '@sendbird/uikit-react-native-foundation';
+import { MentionType, MessageMetaArray } from '@sendbird/chat/message';
import {
Icon,
- ImageWithPlaceholder,
- Text,
+ Modal,
TextInput,
- VideoThumbnail,
createStyleSheet,
useAlert,
useBottomSheet,
useToast,
useUIKitTheme,
} from '@sendbird/uikit-react-native-foundation';
-import {
- FileIcon,
- Logger,
- SendbirdBaseMessage,
- SendbirdChannel,
- getFileIconFromMessageType,
- getMessageType,
- getThumbnailUriFromFileMessage,
- isImage,
- shouldCompressImage,
- useIIFE,
-} from '@sendbird/uikit-utils';
+import { Logger, useDeferredModalState, useIIFE } from '@sendbird/uikit-utils';
+import { VOICE_MESSAGE_META_ARRAY_DURATION_KEY, VOICE_MESSAGE_META_ARRAY_MESSAGE_TYPE_KEY } from '../../constants';
+import { useChannelInputItems } from '../../hooks/useChannelInputItems';
import { useLocalization, usePlatformService, useSendbirdChat } from '../../hooks/useContext';
-import SBUError from '../../libs/SBUError';
import SBUUtils from '../../libs/SBUUtils';
import type { FileType } from '../../platform/types';
import type { MentionedUser } from '../../types';
@@ -51,6 +38,8 @@ interface SendInputProps extends ChannelInputProps {
const SendInput = forwardRef(function SendInput(
{
+ VoiceMessageInput,
+ MessageToReplyPreview,
AttachmentsButton,
onPressSendUserMessage,
onPressSendFileMessage,
@@ -67,12 +56,18 @@ const SendInput = forwardRef(function SendInput(
},
ref,
) {
+ const { playerService, recorderService } = usePlatformService();
const { mentionManager, sbOptions } = useSendbirdChat();
- const { select, colors, palette } = useUIKitTheme();
const { STRINGS } = useLocalization();
const { openSheet } = useBottomSheet();
const toast = useToast();
- const { mediaService } = usePlatformService();
+
+ const {
+ onClose,
+ onDismiss,
+ visible: voiceMessageInputVisible,
+ setVisible: setVoiceMessageInputVisible,
+ } = useDeferredModalState();
const messageReplyParams = useIIFE(() => {
const { groupChannel } = sbOptions.uikit;
@@ -122,8 +117,35 @@ const SendInput = forwardRef(function SendInput(
setMessageToReply?.();
};
+ const sendVoiceMessage = (file: FileType, durationMills: number) => {
+ if (inputMuted) {
+ toast.show(STRINGS.TOAST.USER_MUTED_ERROR, 'error');
+ Logger.error(STRINGS.TOAST.USER_MUTED_ERROR);
+ } else if (inputFrozen) {
+ toast.show(STRINGS.TOAST.CHANNEL_FROZEN_ERROR, 'error');
+ Logger.error(STRINGS.TOAST.CHANNEL_FROZEN_ERROR);
+ } else {
+ onPressSendFileMessage({
+ file,
+ metaArrays: [
+ new MessageMetaArray({
+ key: VOICE_MESSAGE_META_ARRAY_DURATION_KEY,
+ value: [String(durationMills)],
+ }),
+ new MessageMetaArray({
+ key: VOICE_MESSAGE_META_ARRAY_MESSAGE_TYPE_KEY,
+ value: [`voice/${recorderService.options.extension}`],
+ }),
+ ],
+ ...messageReplyParams,
+ }).catch(onFailureToSend);
+ }
+
+ onChangeText('');
+ setMessageToReply?.();
+ };
+
const sheetItems = useChannelInputItems(channel, sendFileMessage);
- const onPressAttachment = () => openSheet({ sheetItems });
const getPlaceholder = () => {
if (inputMuted) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_MUTED;
@@ -134,90 +156,16 @@ const SendInput = forwardRef(function SendInput(
return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_ACTIVE;
};
- const getFileIconAsImage = (url: string) => {
- return ;
- };
-
- const getFileIconAsVideoThumbnail = (url: string) => {
- return (
- mediaService.getVideoThumbnail({ url: uri, timeMills: 1000 })}
- />
- );
- };
-
- const getFileIconAsSymbol = (icon: FileIcon) => {
- return (
-
- );
- };
-
- const getFileIcon = (messageToReply: SendbirdBaseMessage) => {
- if (messageToReply?.isFileMessage()) {
- const messageType = getMessageType(messageToReply);
- switch (messageType) {
- case 'file.image':
- return getFileIconAsImage(getThumbnailUriFromFileMessage(messageToReply));
- case 'file.video':
- return getFileIconAsVideoThumbnail(getThumbnailUriFromFileMessage(messageToReply));
- default:
- return getFileIconAsSymbol(getFileIconFromMessageType(messageType));
- }
- }
- return null;
- };
+ const voiceMessageEnabled = channel.isGroupChannel() && sbOptions.uikit.groupChannel.channel.enableVoiceMessage;
+ const sendButtonVisible = Boolean(text.trim());
return (
- {messageToReply && (
-
-
- {getFileIcon(messageToReply)}
-
-
- {STRINGS.LABELS.CHANNEL_INPUT_REPLY_PREVIEW_TITLE(messageToReply.sender)}
-
-
- {STRINGS.LABELS.CHANNEL_INPUT_REPLY_PREVIEW_BODY(messageToReply)}
-
-
-
- setMessageToReply?.(undefined)}>
-
-
-
+ {MessageToReplyPreview && (
+
)}
- {AttachmentsButton && }
+ {AttachmentsButton && openSheet({ sheetItems })} disabled={inputDisabled} />}
(function SendInput(
)}
- {Boolean(text.trim()) && (
-
-
-
+ {voiceMessageEnabled && (
+ setVoiceMessageInputVisible(true)}
+ />
+ )}
+
+ {voiceMessageEnabled && VoiceMessageInput && (
+ {
+ onDismiss();
+ Promise.allSettled([playerService.reset(), recorderService.reset()]);
+ }}
+ backgroundStyle={{ justifyContent: 'flex-end' }}
+ visible={voiceMessageInputVisible}
+ type={'slide-no-gesture'}
+ >
+ sendVoiceMessage(file, duration)} />
+
)}
);
});
-const useChannelInputItems = (channel: SendbirdChannel, sendFileMessage: (file: FileType) => void) => {
- const { sbOptions, imageCompressionConfig } = useSendbirdChat();
+type InputButtonProps = { visible: boolean; disabled: boolean; onPress: () => void };
+const VoiceMessageButton = ({ visible, disabled, onPress }: InputButtonProps) => {
const { STRINGS } = useLocalization();
- const { fileService, mediaService } = usePlatformService();
const { alert } = useAlert();
- const toast = useToast();
-
- const sheetItems: BottomSheetItem['sheetItems'] = [];
- const input = useIIFE(() => {
- switch (true) {
- case channel.isOpenChannel():
- return sbOptions.uikit.openChannel.channel.input;
- case channel.isGroupChannel():
- return sbOptions.uikit.groupChannel.channel.input;
- default:
- return {
- enableDocument: true,
- camera: { enablePhoto: true, enableVideo: true },
- gallery: { enablePhoto: true, enableVideo: true },
- };
+ const { playerService, recorderService } = usePlatformService();
+ const { colors } = useUIKitTheme();
+ if (!visible) return null;
+
+ const onPressWithPermissionCheck = async () => {
+ const recorderGranted = await recorderService.requestPermission();
+ if (!recorderGranted) {
+ alert({
+ title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
+ message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE(
+ STRINGS.LABELS.PERMISSION_MICROPHONE,
+ STRINGS.LABELS.PERMISSION_APP_NAME,
+ ),
+ buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
+ });
+ Logger.error('Failed to request permission for recorder');
+ return;
}
- });
- if (input.camera.enablePhoto) {
- sheetItems.push({
- title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_CAMERA_PHOTO,
- icon: 'camera',
- onPress: async () => {
- const mediaFile = await fileService.openCamera({
- mediaType: 'photo',
- onOpenFailure: (error) => {
- if (error.code === SBUError.CODE.ERR_PERMISSIONS_DENIED) {
- alert({
- title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
- message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE(
- STRINGS.LABELS.PERMISSION_CAMERA,
- STRINGS.LABELS.PERMISSION_APP_NAME,
- ),
- buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
- });
- } else {
- toast.show(STRINGS.TOAST.OPEN_CAMERA_ERROR, 'error');
- }
- },
- });
-
- if (mediaFile) {
- // Image compression
- if (
- isImage(mediaFile.uri, mediaFile.type) &&
- shouldCompressImage(mediaFile.type, sbOptions.chat.imageCompressionEnabled)
- ) {
- await SBUUtils.safeRun(async () => {
- const compressed = await mediaService.compressImage({
- uri: mediaFile.uri,
- maxWidth: imageCompressionConfig.width,
- maxHeight: imageCompressionConfig.height,
- compressionRate: imageCompressionConfig.compressionRate,
- });
-
- if (compressed) {
- mediaFile.uri = compressed.uri;
- mediaFile.size = compressed.size;
- }
- });
- }
-
- sendFileMessage(mediaFile);
- }
- },
- });
- }
-
- if (input.camera.enableVideo) {
- sheetItems.push({
- title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_CAMERA_VIDEO,
- icon: 'camera',
- onPress: async () => {
- const mediaFile = await fileService.openCamera({
- mediaType: 'video',
- onOpenFailure: (error) => {
- if (error.code === SBUError.CODE.ERR_PERMISSIONS_DENIED) {
- alert({
- title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
- message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE(
- STRINGS.LABELS.PERMISSION_CAMERA,
- STRINGS.LABELS.PERMISSION_APP_NAME,
- ),
- buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
- });
- } else {
- toast.show(STRINGS.TOAST.OPEN_CAMERA_ERROR, 'error');
- }
- },
- });
-
- if (mediaFile) {
- sendFileMessage(mediaFile);
- }
- },
- });
- }
-
- if (input.gallery.enablePhoto || input.gallery.enableVideo) {
- const mediaType = (() => {
- switch (true) {
- case input.gallery.enablePhoto && input.gallery.enableVideo:
- return 'all';
- case input.gallery.enablePhoto && !input.gallery.enableVideo:
- return 'photo';
- case !input.gallery.enablePhoto && input.gallery.enableVideo:
- return 'video';
- default:
- return 'all';
- }
- })();
-
- sheetItems.push({
- title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_PHOTO_LIBRARY,
- icon: 'photo',
- onPress: async () => {
- const mediaFiles = await fileService.openMediaLibrary({
- selectionLimit: 1,
- mediaType,
- onOpenFailure: (error) => {
- if (error.code === SBUError.CODE.ERR_PERMISSIONS_DENIED) {
- alert({
- title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
- message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE(
- STRINGS.LABELS.PERMISSION_DEVICE_STORAGE,
- STRINGS.LABELS.PERMISSION_APP_NAME,
- ),
- buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
- });
- } else {
- toast.show(STRINGS.TOAST.OPEN_PHOTO_LIBRARY_ERROR, 'error');
- }
- },
- });
-
- if (mediaFiles && mediaFiles[0]) {
- const mediaFile = mediaFiles[0];
-
- // Image compression
- if (
- isImage(mediaFile.uri, mediaFile.type) &&
- shouldCompressImage(mediaFile.type, sbOptions.chat.imageCompressionEnabled)
- ) {
- await SBUUtils.safeRun(async () => {
- const compressed = await mediaService.compressImage({
- uri: mediaFile.uri,
- maxWidth: imageCompressionConfig.width,
- maxHeight: imageCompressionConfig.height,
- compressionRate: imageCompressionConfig.compressionRate,
- });
-
- if (compressed) {
- mediaFile.uri = compressed.uri;
- mediaFile.size = compressed.size;
- }
- });
- }
-
- sendFileMessage(mediaFile);
- }
- },
- });
- }
-
- if (input.enableDocument) {
- sheetItems.push({
- title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_FILES,
- icon: 'document',
- onPress: async () => {
- const documentFile = await fileService.openDocument({
- onOpenFailure: () => toast.show(STRINGS.TOAST.OPEN_FILES_ERROR, 'error'),
- });
-
- if (documentFile) {
- // Image compression
- if (
- isImage(documentFile.uri, documentFile.type) &&
- shouldCompressImage(documentFile.type, sbOptions.chat.imageCompressionEnabled)
- ) {
- await SBUUtils.safeRun(async () => {
- const compressed = await mediaService.compressImage({
- uri: documentFile.uri,
- maxWidth: imageCompressionConfig.width,
- maxHeight: imageCompressionConfig.height,
- compressionRate: imageCompressionConfig.compressionRate,
- });
-
- if (compressed) {
- documentFile.uri = compressed.uri;
- documentFile.size = compressed.size;
- }
- });
- }
+ const playerGranted = await playerService.requestPermission();
+ if (!playerGranted) {
+ alert({
+ title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
+ message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE(
+ STRINGS.LABELS.PERMISSION_DEVICE_STORAGE,
+ STRINGS.LABELS.PERMISSION_APP_NAME,
+ ),
+ buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
+ });
+ Logger.error('Failed to request permission for player');
+ return;
+ }
- sendFileMessage(documentFile);
- }
- },
- });
- }
+ onPress();
+ };
- return sheetItems;
+ return (
+
+
+
+ );
+};
+const UserMessageSendButton = ({ visible, disabled, onPress }: InputButtonProps) => {
+ const { colors } = useUIKitTheme();
+ if (!visible) return null;
+ return (
+
+
+
+ );
};
const styles = createStyleSheet({
@@ -474,18 +291,10 @@ const styles = createStyleSheet({
maxHeight: 36 * Platform.select({ ios: 2.5, default: 2 }),
borderRadius: 20,
},
- iconSend: {
+ sendIcon: {
marginLeft: 4,
padding: 4,
},
- previewImage: {
- width: 36,
- height: 36,
- borderRadius: 10,
- marginTop: 2,
- marginRight: 10,
- overflow: 'hidden',
- },
});
export default SendInput;
diff --git a/packages/uikit-react-native/src/components/ChannelInput/VoiceMessageInput.tsx b/packages/uikit-react-native/src/components/ChannelInput/VoiceMessageInput.tsx
new file mode 100644
index 000000000..4a272a733
--- /dev/null
+++ b/packages/uikit-react-native/src/components/ChannelInput/VoiceMessageInput.tsx
@@ -0,0 +1,206 @@
+import React, { useEffect, useRef } from 'react';
+import { Animated } from 'react-native';
+
+import {
+ Box,
+ Icon,
+ PressBox,
+ ProgressBar,
+ Text,
+ createStyleSheet,
+ useUIKitTheme,
+} from '@sendbird/uikit-react-native-foundation';
+import { conditionChaining, millsToMMSS } from '@sendbird/uikit-utils';
+
+import { useLocalization } from '../../hooks/useContext';
+import useVoiceMessageInput from '../../hooks/useVoiceMessageInput';
+import type { FileType } from '../../platform/types';
+
+export type VoiceMessageInputProps = {
+ onClose: () => Promise;
+ onSend: (params: { file: FileType; duration: number }) => void;
+};
+
+const VoiceMessageInput = ({ onClose, onSend }: VoiceMessageInputProps) => {
+ const { STRINGS } = useLocalization();
+ const { colors } = useUIKitTheme();
+ const { actions, state } = useVoiceMessageInput({
+ onSend: (file, duration) => onSend({ file, duration }),
+ onClose,
+ });
+
+ const uiColors = colors.ui.voiceMessageInput.default[state.status !== 'idle' ? 'active' : 'inactive'];
+
+ const onPressCancel = async () => {
+ actions.cancel();
+ onClose();
+ };
+
+ const onPressSend = async () => {
+ actions.send();
+ onClose();
+ };
+
+ const onPressVoiceMessageAction = () => {
+ switch (state.status) {
+ case 'idle':
+ actions.startRecording();
+ break;
+ case 'recording':
+ if (lessThanMinimumDuration) {
+ actions.cancel();
+ } else {
+ actions.stopRecording();
+ }
+ break;
+ case 'recording_completed':
+ case 'playing_paused':
+ actions.playPlayer();
+ break;
+ case 'playing':
+ actions.pausePlayer();
+ break;
+ }
+ };
+ const renderActionIcon = () => {
+ switch (state.status) {
+ case 'idle':
+ return ;
+ case 'recording':
+ return ;
+ case 'recording_completed':
+ case 'playing_paused':
+ return ;
+ case 'playing':
+ return ;
+ }
+ };
+
+ const isRecorderState = state.status === 'recording' || state.status === 'recording_completed';
+ const lessThanMinimumDuration = state.recordingTime.currentTime < state.recordingTime.minDuration;
+ const remainingTime = state.playingTime.duration - state.playingTime.currentTime;
+
+ return (
+
+ {/** Progress bar **/}
+
+
+
+ {millsToMMSS(isRecorderState ? state.recordingTime.currentTime : remainingTime)}
+
+
+ }
+ />
+
+
+ {/** Cancel / Send **/}
+
+
+
+
+
+
+ {/** Record / Stop / Play / Pause **/}
+
+
+
+ {renderActionIcon()}
+
+
+
+
+
+ );
+};
+
+const RecordingLight = (props: { visible: boolean }) => {
+ const { colors } = useUIKitTheme();
+
+ const value = useRef(new Animated.Value(0)).current;
+ const animation = useRef(
+ Animated.loop(
+ Animated.sequence([
+ Animated.timing(value, { toValue: 1, duration: 500, useNativeDriver: true }),
+ Animated.timing(value, { toValue: 0, duration: 500, useNativeDriver: true }),
+ ]),
+ ),
+ ).current;
+
+ useEffect(() => {
+ if (props.visible) animation.start();
+ return () => {
+ animation.reset();
+ };
+ }, [props.visible]);
+
+ if (!props.visible) return null;
+ return (
+
+ );
+};
+
+const CancelButton = (props: { onPress: () => void; label: string }) => {
+ const { colors } = useUIKitTheme();
+
+ return (
+
+
+
+ {props.label}
+
+
+
+ );
+};
+
+const SendButton = (props: { onPress: () => void; disabled: boolean }) => {
+ const { colors } = useUIKitTheme();
+
+ const uiColors = colors.ui.voiceMessageInput.default[props.disabled ? 'inactive' : 'active'];
+
+ return (
+
+
+
+
+
+ );
+};
+
+const styles = createStyleSheet({
+ container: {
+ borderTopLeftRadius: 8,
+ borderTopRightRadius: 8,
+ },
+ progressBar: {
+ height: 36,
+ marginBottom: 16,
+ borderRadius: 18,
+ },
+});
+
+export default VoiceMessageInput;
diff --git a/packages/uikit-react-native/src/components/ChannelInput/index.tsx b/packages/uikit-react-native/src/components/ChannelInput/index.tsx
index 1f51781c2..dcb05a4d1 100644
--- a/packages/uikit-react-native/src/components/ChannelInput/index.tsx
+++ b/packages/uikit-react-native/src/components/ChannelInput/index.tsx
@@ -23,7 +23,10 @@ import type { MentionedUser, Range } from '../../types';
import type { AttachmentsButtonProps } from './AttachmentsButton';
import AttachmentsButton from './AttachmentsButton';
import EditInput from './EditInput';
+import type { MessageToReplyPreviewProps } from './MessageToReplyPreview';
+import { MessageToReplyPreview } from './MessageToReplyPreview';
import SendInput from './SendInput';
+import VoiceMessageInput, { VoiceMessageInputProps } from './VoiceMessageInput';
export type SuggestedMentionListProps = {
text: string;
@@ -65,6 +68,8 @@ export type ChannelInputProps = {
// sub-components
AttachmentsButton?: (props: AttachmentsButtonProps) => React.ReactNode | null;
+ MessageToReplyPreview?: (props: MessageToReplyPreviewProps) => React.ReactNode | null;
+ VoiceMessageInput?: (props: VoiceMessageInputProps) => React.ReactNode | null;
};
const AUTO_FOCUS = Platform.select({ ios: false, android: true, default: false });
@@ -93,6 +98,7 @@ const ChannelInput = (props: ChannelInputProps) => {
const mentionAvailable =
sbOptions.uikit.groupChannel.channel.enableMention && channel.isGroupChannel() && !channel.isBroadcast;
+
const inputKeyToRemount = GET_INPUT_KEY(mentionAvailable ? mentionedUsers.length === 0 : false);
const [inputHeight, setInputHeight] = useState(styles.inputDefault.height);
@@ -129,7 +135,9 @@ const ChannelInput = (props: ChannelInputProps) => {
onChangeText={onChangeText}
onSelectionChange={onSelectionChange}
mentionedUsers={mentionedUsers}
+ VoiceMessageInput={props.VoiceMessageInput ?? VoiceMessageInput}
AttachmentsButton={props.AttachmentsButton ?? AttachmentsButton}
+ MessageToReplyPreview={props.MessageToReplyPreview ?? MessageToReplyPreview}
/>
)}
{inputMode === 'edit' && messageToEdit && (
diff --git a/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx b/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx
index 951d7e31d..27254ed50 100644
--- a/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx
+++ b/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx
@@ -22,6 +22,7 @@ import {
getFileExtension,
getFileType,
isMyMessage,
+ isVoiceMessage,
messageKeyExtractor,
shouldRenderReaction,
toMegabyte,
@@ -277,7 +278,7 @@ const useGetMessagePressActions = void;
};
-const GroupChannelMessageParentMessage = ({ variant, message, childMessage, onPress }: Props) => {
+const GroupChannelMessageParentMessage = ({ variant, channel, message, childMessage, onPress }: Props) => {
const { currentUser } = useSendbirdChat();
const groupChannelPubSub = useContext(GroupChannelContexts.PubSub);
const { select, colors, palette } = useUIKitTheme();
@@ -52,6 +54,19 @@ const GroupChannelMessageParentMessage = ({ variant, message, childMessage, onPr
});
}, []);
+ const renderMessageWithText = (message: string) => {
+ return (
+
+
+ {message}
+
+
+ );
+ };
+
const renderFileMessageAsVideoThumbnail = (url: string) => {
return (
{
+ if (channel.messageOffsetTimestamp > parentMessage.createdAt) {
+ return renderMessageWithText(STRINGS.LABELS.MESSAGE_UNAVAILABLE);
+ }
+
switch (type) {
case 'user':
case 'user.opengraph': {
- return (
-
-
- {(parentMessage as SendbirdUserMessage).message}
-
-
- );
+ return renderMessageWithText((parentMessage as SendbirdUserMessage).message);
}
case 'file':
case 'file.audio': {
@@ -109,6 +119,9 @@ const GroupChannelMessageParentMessage = ({ variant, message, childMessage, onPr
case 'file.image': {
return renderFileMessageAsPreview(getThumbnailUriFromFileMessage(parentMessage as SendbirdFileMessage));
}
+ case 'file.voice': {
+ return renderMessageWithText(STRINGS.LABELS.VOICE_MESSAGE);
+ }
default: {
return null;
}
diff --git a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx
index 1d9cba6fa..218c5b703 100644
--- a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx
+++ b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useRef } from 'react';
import type { GroupChannelMessageProps, RegexTextPattern } from '@sendbird/uikit-react-native-foundation';
import { Box, GroupChannelMessage, Text, useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
@@ -10,11 +10,13 @@ import {
calcMessageGrouping,
getMessageType,
isMyMessage,
+ isVoiceMessage,
shouldRenderParentMessage,
shouldRenderReaction,
useIIFE,
} from '@sendbird/uikit-utils';
+import { VOICE_MESSAGE_META_ARRAY_DURATION_KEY } from '../../constants';
import type { GroupChannelProps } from '../../domain/groupChannel/types';
import { useLocalization, usePlatformService, useSendbirdChat } from '../../hooks/useContext';
import SBUUtils from '../../libs/SBUUtils';
@@ -36,10 +38,11 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
prevMessage,
nextMessage,
}) => {
+ const playerUnsubscribes = useRef<(() => void)[]>([]);
const { palette } = useUIKitTheme();
const { sbOptions, currentUser, mentionManager } = useSendbirdChat();
const { STRINGS } = useLocalization();
- const { mediaService } = usePlatformService();
+ const { mediaService, playerService } = usePlatformService();
const { groupWithPrev, groupWithNext } = calcMessageGrouping(
Boolean(enableMessageGrouping),
message,
@@ -58,6 +61,16 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
return null;
});
+ const resetPlayer = async () => {
+ playerUnsubscribes.current.forEach((unsubscribe) => {
+ try {
+ unsubscribe();
+ } catch {}
+ });
+ playerUnsubscribes.current.length = 0;
+ await playerService.reset();
+ };
+
const variant = isMyMessage(message, currentUser?.userId) ? 'outgoing' : 'incoming';
const messageProps: Omit, 'message'> = {
@@ -72,6 +85,55 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
onPressMentionedUser: (mentionedUser) => {
if (mentionedUser) onShowUserProfile?.(mentionedUser);
},
+ onToggleVoiceMessage: async (state, setState) => {
+ if (isVoiceMessage(message) && message.sendingStatus === 'succeeded') {
+ if (playerService.uri === message.url) {
+ if (playerService.state === 'playing') {
+ await playerService.pause();
+ } else {
+ await playerService.play(message.url);
+ }
+ } else {
+ if (playerService.state !== 'idle') {
+ await resetPlayer();
+ }
+
+ const shouldSeekToTime = state.duration > state.currentTime && state.currentTime > 0;
+ let seekFinished = !shouldSeekToTime;
+
+ const forPlayback = playerService.addPlaybackListener(({ stopped, currentTime, duration }) => {
+ if (seekFinished) {
+ setState((prevState) => ({ ...prevState, currentTime: stopped ? 0 : currentTime, duration }));
+ }
+ });
+ const forState = playerService.addStateListener((state) => {
+ switch (state) {
+ case 'preparing':
+ setState((prevState) => ({ ...prevState, status: 'preparing' }));
+ break;
+ case 'playing':
+ setState((prevState) => ({ ...prevState, status: 'playing' }));
+ break;
+ case 'idle':
+ case 'paused': {
+ setState((prevState) => ({ ...prevState, status: 'paused' }));
+ break;
+ }
+ case 'stopped':
+ setState((prevState) => ({ ...prevState, status: 'paused' }));
+ break;
+ }
+ });
+ playerUnsubscribes.current.push(forPlayback, forState);
+
+ await playerService.play(message.url);
+ if (shouldSeekToTime) {
+ await playerService.seek(state.currentTime);
+ seekFinished = true;
+ }
+ }
+ }
+ },
groupedWithPrev: groupWithPrev,
groupedWithNext: groupWithNext,
children: reactionChildren,
@@ -80,9 +142,10 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
) : null,
parentMessage: shouldRenderParentMessage(message) ? (
) : null,
@@ -184,6 +247,20 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
/>
);
}
+ case 'file.voice': {
+ return (
+ {
+ if (isVoiceMessage(message) && playerService.uri === message.url) {
+ resetPlayer();
+ }
+ }}
+ {...messageProps}
+ />
+ );
+ }
case 'unknown':
default: {
return ;
diff --git a/packages/uikit-react-native/src/components/MessageSearchResultItem.tsx b/packages/uikit-react-native/src/components/MessageSearchResultItem.tsx
index 78f9191fc..f99ffda38 100644
--- a/packages/uikit-react-native/src/components/MessageSearchResultItem.tsx
+++ b/packages/uikit-react-native/src/components/MessageSearchResultItem.tsx
@@ -10,7 +10,7 @@ import {
useUIKitTheme,
} from '@sendbird/uikit-react-native-foundation';
import type { SendbirdBaseMessage } from '@sendbird/uikit-utils';
-import { getFileIconFromMessage, useIIFE } from '@sendbird/uikit-utils';
+import { getFileIconFromMessage, isVoiceMessage, useIIFE } from '@sendbird/uikit-utils';
import type { MessageSearchProps } from '../domain/messageSearch/types';
import { useLocalization } from '../hooks/useContext';
@@ -21,6 +21,7 @@ const MessageSearchResultItem: MessageSearchProps['List']['renderSearchResultIte
const fileIcon = useIIFE(() => {
if (!message?.isFileMessage()) return undefined;
+ if (isVoiceMessage(message)) return undefined;
return getFileIconFromMessage(message);
});
diff --git a/packages/uikit-react-native/src/components/OpenChannelMessageRenderer/index.tsx b/packages/uikit-react-native/src/components/OpenChannelMessageRenderer/index.tsx
index 97946b062..b1be3d6df 100644
--- a/packages/uikit-react-native/src/components/OpenChannelMessageRenderer/index.tsx
+++ b/packages/uikit-react-native/src/components/OpenChannelMessageRenderer/index.tsx
@@ -63,6 +63,7 @@ const OpenChannelMessageRenderer: OpenChannelProps['Fragment']['renderMessage']
}
}
case 'file':
+ case 'file.voice':
case 'file.audio': {
return ;
}
diff --git a/packages/uikit-react-native/src/constants.ts b/packages/uikit-react-native/src/constants.ts
index b6d41c627..fb26b3cf8 100644
--- a/packages/uikit-react-native/src/constants.ts
+++ b/packages/uikit-react-native/src/constants.ts
@@ -3,3 +3,5 @@ export const MESSAGE_SEARCH_SAFE_SCROLL_DELAY = 500;
export const MESSAGE_FOCUS_ANIMATION_DELAY = 250;
export const UNKNOWN_USER_ID = '##__USER_ID_IS_NOT_PROVIDED__##';
+export const VOICE_MESSAGE_META_ARRAY_DURATION_KEY = 'KEY_VOICE_MESSAGE_DURATION';
+export const VOICE_MESSAGE_META_ARRAY_MESSAGE_TYPE_KEY = 'KEY_INTERNAL_MESSAGE_TYPE';
diff --git a/packages/uikit-react-native/src/containers/GroupChannelPreviewContainer.tsx b/packages/uikit-react-native/src/containers/GroupChannelPreviewContainer.tsx
index 7cf0dc419..428a8f629 100644
--- a/packages/uikit-react-native/src/containers/GroupChannelPreviewContainer.tsx
+++ b/packages/uikit-react-native/src/containers/GroupChannelPreviewContainer.tsx
@@ -17,6 +17,7 @@ import {
getFileTypeFromMessage,
isDifferentChannel,
isMyMessage,
+ isVoiceMessage,
useIIFE,
useUniqHandlerId,
} from '@sendbird/uikit-utils';
@@ -56,6 +57,7 @@ const GroupChannelPreviewContainer = ({ onPress, onLongPress, channel }: Props)
const fileType = useIIFE(() => {
if (!channel.lastMessage?.isFileMessage()) return undefined;
if (typingUsers.length > 0) return undefined;
+ if (isVoiceMessage(channel.lastMessage)) return undefined;
return getFileTypeFromMessage(channel.lastMessage);
});
diff --git a/packages/uikit-react-native/src/containers/SendbirdUIKitContainer.tsx b/packages/uikit-react-native/src/containers/SendbirdUIKitContainer.tsx
index 668b3a6cd..f59a2c04f 100644
--- a/packages/uikit-react-native/src/containers/SendbirdUIKitContainer.tsx
+++ b/packages/uikit-react-native/src/containers/SendbirdUIKitContainer.tsx
@@ -37,6 +37,7 @@ import ImageCompressionConfig from '../libs/ImageCompressionConfig';
import InternalLocalCacheStorage from '../libs/InternalLocalCacheStorage';
import MentionConfig, { MentionConfigInterface } from '../libs/MentionConfig';
import MentionManager from '../libs/MentionManager';
+import VoiceMessageConfig, { VoiceMessageConfigInterface } from '../libs/VoiceMessageConfig';
import StringSetEn from '../localization/StringSet.en';
import type { StringSet } from '../localization/StringSet.type';
import SBUDynamicModule from '../platform/dynamicModule';
@@ -45,6 +46,8 @@ import type {
FileServiceInterface,
MediaServiceInterface,
NotificationServiceInterface,
+ PlayerServiceInterface,
+ RecorderServiceInterface,
} from '../platform/types';
import type { ErrorBoundaryProps, LocalCacheStorage } from '../types';
import VERSION from '../version';
@@ -61,7 +64,7 @@ export const SendbirdUIKit = Object.freeze({
},
});
-type UnimplementedFeatures = 'enableVoiceMessage' | 'threadReplySelectType' | 'replyType';
+type UnimplementedFeatures = 'threadReplySelectType' | 'replyType';
export type ChatOmittedInitParams = Omit<
SendbirdChatParams<[GroupChannelModule, OpenChannelModule]>,
(typeof chatOmitKeys)[number]
@@ -89,6 +92,8 @@ export type SendbirdUIKitContainerProps = React.PropsWithChildren<{
notification: NotificationServiceInterface;
clipboard: ClipboardServiceInterface;
media: MediaServiceInterface;
+ player: PlayerServiceInterface;
+ recorder: RecorderServiceInterface;
};
chatOptions: {
localCacheStorage: LocalCacheStorage;
@@ -134,23 +139,24 @@ export type SendbirdUIKitContainerProps = React.PropsWithChildren<{
};
userMention?: Pick, 'mentionLimit' | 'suggestionLimit' | 'debounceMills'>;
imageCompression?: Partial;
+ voiceMessage?: PartialDeep;
}>;
-const SendbirdUIKitContainer = ({
- children,
- appId,
- chatOptions,
- uikitOptions,
- platformServices,
- localization,
- styles,
- errorBoundary,
- toast,
- userProfile,
- reaction,
- userMention,
- imageCompression,
-}: SendbirdUIKitContainerProps) => {
+const SendbirdUIKitContainer = (props: SendbirdUIKitContainerProps) => {
+ const {
+ children,
+ appId,
+ chatOptions,
+ uikitOptions,
+ platformServices,
+ localization,
+ styles,
+ errorBoundary,
+ toast,
+ userProfile,
+ reaction,
+ } = props;
+
if (!chatOptions.localCacheStorage) {
throw new Error('SendbirdUIKitContainer: chatOptions.localCacheStorage is required');
}
@@ -167,26 +173,9 @@ const SendbirdUIKitContainer = ({
return sendbird.chatSDK;
});
+ const { imageCompressionConfig, voiceMessageConfig, mentionConfig } = useConfigInstance(props);
const emojiManager = useMemo(() => new EmojiManager(internalStorage), [internalStorage]);
-
- const mentionManager = useMemo(() => {
- const config = new MentionConfig({
- mentionLimit: userMention?.mentionLimit || MentionConfig.DEFAULT.MENTION_LIMIT,
- suggestionLimit: userMention?.suggestionLimit || MentionConfig.DEFAULT.SUGGESTION_LIMIT,
- debounceMills: userMention?.debounceMills ?? MentionConfig.DEFAULT.DEBOUNCE_MILLS,
- delimiter: MentionConfig.DEFAULT.DELIMITER,
- trigger: MentionConfig.DEFAULT.TRIGGER,
- });
- return new MentionManager(config);
- }, [userMention?.mentionLimit, userMention?.suggestionLimit, userMention?.debounceMills]);
-
- const imageCompressionConfig = useMemo(() => {
- return new ImageCompressionConfig({
- compressionRate: imageCompression?.compressionRate || ImageCompressionConfig.DEFAULT.COMPRESSION_RATE,
- width: imageCompression?.width,
- height: imageCompression?.height,
- });
- }, [imageCompression?.compressionRate, imageCompression?.width, imageCompression?.height]);
+ const mentionManager = useMemo(() => new MentionManager(mentionConfig), [mentionConfig]);
useLayoutEffect(() => {
if (!isFirstMount) {
@@ -233,6 +222,7 @@ const SendbirdUIKitContainer = ({
emojiManager={emojiManager}
mentionManager={mentionManager}
imageCompressionConfig={imageCompressionConfig}
+ voiceMessageConfig={voiceMessageConfig}
enableAutoPushTokenRegistration={
chatOptions.enableAutoPushTokenRegistration ?? SendbirdUIKit.DEFAULT.AUTO_PUSH_TOKEN_REGISTRATION
}
@@ -247,6 +237,9 @@ const SendbirdUIKitContainer = ({
notificationService={platformServices.notification}
clipboardService={platformServices.clipboard}
mediaService={platformServices.media}
+ playerService={platformServices.player}
+ recorderService={platformServices.recorder}
+ voiceMessageConfig={voiceMessageConfig}
>
{
+ const mentionConfig = useMemo(() => {
+ return new MentionConfig({
+ mentionLimit: userMention?.mentionLimit || MentionConfig.DEFAULT.MENTION_LIMIT,
+ suggestionLimit: userMention?.suggestionLimit || MentionConfig.DEFAULT.SUGGESTION_LIMIT,
+ debounceMills: userMention?.debounceMills ?? MentionConfig.DEFAULT.DEBOUNCE_MILLS,
+ delimiter: MentionConfig.DEFAULT.DELIMITER,
+ trigger: MentionConfig.DEFAULT.TRIGGER,
+ });
+ }, [userMention?.mentionLimit, userMention?.suggestionLimit, userMention?.debounceMills]);
+
+ const imageCompressionConfig = useMemo(() => {
+ return new ImageCompressionConfig({
+ compressionRate: imageCompression?.compressionRate || ImageCompressionConfig.DEFAULT.COMPRESSION_RATE,
+ width: imageCompression?.width,
+ height: imageCompression?.height,
+ });
+ }, [imageCompression?.compressionRate, imageCompression?.width, imageCompression?.height]);
+
+ const voiceMessageConfig = useMemo(() => {
+ return new VoiceMessageConfig({
+ recorder: {
+ minDuration: voiceMessage?.recorder?.minDuration ?? VoiceMessageConfig.DEFAULT.RECORDER.MIN_DURATION,
+ maxDuration: voiceMessage?.recorder?.maxDuration ?? VoiceMessageConfig.DEFAULT.RECORDER.MAX_DURATION,
+ },
+ });
+ }, [voiceMessage?.recorder?.minDuration, voiceMessage?.recorder?.maxDuration]);
+
+ return {
+ mentionConfig,
+ imageCompressionConfig,
+ voiceMessageConfig,
+ };
+};
+
export default SendbirdUIKitContainer;
diff --git a/packages/uikit-react-native/src/contexts/PlatformServiceCtx.tsx b/packages/uikit-react-native/src/contexts/PlatformServiceCtx.tsx
index a70c654be..1aea5ca3d 100644
--- a/packages/uikit-react-native/src/contexts/PlatformServiceCtx.tsx
+++ b/packages/uikit-react-native/src/contexts/PlatformServiceCtx.tsx
@@ -1,37 +1,39 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import { useAppState } from '@sendbird/uikit-utils';
+
+import VoiceMessageConfig from '../libs/VoiceMessageConfig';
import type {
ClipboardServiceInterface,
FileServiceInterface,
MediaServiceInterface,
NotificationServiceInterface,
+ PlayerServiceInterface,
+ RecorderServiceInterface,
} from '../platform/types';
-type Props = React.PropsWithChildren<{
- fileService: FileServiceInterface;
- clipboardService: ClipboardServiceInterface;
- notificationService: NotificationServiceInterface;
- mediaService: MediaServiceInterface;
-}>;
-
export type PlatformServiceContextType = {
fileService: FileServiceInterface;
clipboardService: ClipboardServiceInterface;
notificationService: NotificationServiceInterface;
mediaService: MediaServiceInterface;
+ recorderService: RecorderServiceInterface;
+ playerService: PlayerServiceInterface;
};
+type Props = React.PropsWithChildren;
export const PlatformServiceContext = React.createContext(null);
-export const PlatformServiceProvider = ({
- children,
- fileService,
- clipboardService,
- notificationService,
- mediaService,
-}: Props) => {
- return (
-
- {children}
-
- );
+export const PlatformServiceProvider = ({ children, voiceMessageConfig, ...services }: Props) => {
+ useEffect(() => {
+ services.recorderService.options.minDuration = voiceMessageConfig.recorder.minDuration;
+ services.recorderService.options.maxDuration = voiceMessageConfig.recorder.maxDuration;
+ }, [voiceMessageConfig]);
+
+ useAppState('change', (state) => {
+ if (state !== 'active') {
+ Promise.allSettled([services.playerService.reset(), services.recorderService.reset()]);
+ }
+ });
+
+ return {children};
};
diff --git a/packages/uikit-react-native/src/contexts/SendbirdChatCtx.tsx b/packages/uikit-react-native/src/contexts/SendbirdChatCtx.tsx
index a11c9257b..267507f38 100644
--- a/packages/uikit-react-native/src/contexts/SendbirdChatCtx.tsx
+++ b/packages/uikit-react-native/src/contexts/SendbirdChatCtx.tsx
@@ -13,6 +13,7 @@ import { confirmAndMarkAsDelivered, useAppState, useForceUpdate } from '@sendbir
import type EmojiManager from '../libs/EmojiManager';
import type ImageCompressionConfig from '../libs/ImageCompressionConfig';
import type MentionManager from '../libs/MentionManager';
+import type VoiceMessageConfig from '../libs/VoiceMessageConfig';
import type { FileType } from '../platform/types';
export interface ChatRelatedFeaturesInUIKit {
@@ -23,18 +24,23 @@ export interface ChatRelatedFeaturesInUIKit {
interface Props extends ChatRelatedFeaturesInUIKit, React.PropsWithChildren {
sdkInstance: SendbirdChatSDK;
+
emojiManager: EmojiManager;
mentionManager: MentionManager;
imageCompressionConfig: ImageCompressionConfig;
+ voiceMessageConfig: VoiceMessageConfig;
}
export type SendbirdChatContextType = {
sdk: SendbirdChatSDK;
+ currentUser?: SendbirdUser;
+ setCurrentUser: React.Dispatch>;
+
+ // feature related instances
emojiManager: EmojiManager;
mentionManager: MentionManager;
imageCompressionConfig: ImageCompressionConfig;
- currentUser?: SendbirdUser;
- setCurrentUser: React.Dispatch>;
+ voiceMessageConfig: VoiceMessageConfig;
// helper functions
updateCurrentUserInfo: (nickname?: string, profile?: string | FileType) => Promise;
@@ -84,6 +90,7 @@ export const SendbirdChatProvider = ({
emojiManager,
mentionManager,
imageCompressionConfig,
+ voiceMessageConfig,
enableAutoPushTokenRegistration,
enableUseUserIdForNickname,
enableImageCompression,
@@ -155,6 +162,7 @@ export const SendbirdChatProvider = ({
emojiManager,
mentionManager,
imageCompressionConfig,
+ voiceMessageConfig,
currentUser,
setCurrentUser,
diff --git a/packages/uikit-react-native/src/domain/groupChannel/module/moduleContext.tsx b/packages/uikit-react-native/src/domain/groupChannel/module/moduleContext.tsx
index 192b7e1ac..d0e0e947f 100644
--- a/packages/uikit-react-native/src/domain/groupChannel/module/moduleContext.tsx
+++ b/packages/uikit-react-native/src/domain/groupChannel/module/moduleContext.tsx
@@ -11,6 +11,7 @@ import {
SendbirdMessage,
SendbirdUser,
SendbirdUserMessage,
+ getGroupChannelChatAvailableState,
isDifferentChannel,
useFreshCallback,
useUniqHandlerId,
@@ -97,7 +98,9 @@ export const GroupChannelContextsProvider: GroupChannelModule['Provider'] = ({
},
onChannelFrozen(frozenChannel) {
if (frozenChannel.url === channel.url) {
- setMessageToReply(undefined);
+ if (frozenChannel.isGroupChannel() && getGroupChannelChatAvailableState(channel).frozen) {
+ setMessageToReply(undefined);
+ }
}
},
onUserMuted(mutedChannel, user) {
diff --git a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx
index 1def49a9f..e618b776d 100644
--- a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx
+++ b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ReplyType } from '@sendbird/chat/message';
import { useGroupChannelMessages } from '@sendbird/uikit-chat-hooks';
@@ -25,7 +25,7 @@ import type {
GroupChannelProps,
GroupChannelPubSubContextPayload,
} from '../domain/groupChannel/types';
-import { useSendbirdChat } from '../hooks/useContext';
+import { usePlatformService, useSendbirdChat } from '../hooks/useContext';
import pubsub from '../utils/pubsub';
const createGroupChannelFragment = (initModule?: Partial): GroupChannelFragment => {
@@ -52,6 +52,7 @@ const createGroupChannelFragment = (initModule?: Partial): G
sortComparator = messageComparator,
flatListProps,
}) => {
+ const { playerService, recorderService } = usePlatformService();
const { sdk, currentUser, sbOptions } = useSendbirdChat();
const [internalSearchItem, setInternalSearchItem] = useState(searchItem);
@@ -97,6 +98,30 @@ const createGroupChannelFragment = (initModule?: Partial): G
enableCollectionWithoutLocalCache: true,
});
+ const onBlurFragment = () => {
+ return Promise.allSettled([playerService.reset(), recorderService.reset()]);
+ };
+ const _onPressHeaderLeft = useFreshCallback(async () => {
+ await onBlurFragment();
+ onPressHeaderLeft();
+ });
+ const _onPressHeaderRight = useFreshCallback(async () => {
+ await onBlurFragment();
+ onPressHeaderRight();
+ });
+ const _onPressMediaMessage: NonNullable = useFreshCallback(
+ async (message, deleteMessage, uri) => {
+ await onBlurFragment();
+ onPressMediaMessage(message, deleteMessage, uri);
+ },
+ );
+
+ useEffect(() => {
+ return () => {
+ onBlurFragment();
+ };
+ }, []);
+
const renderItem: GroupChannelProps['MessageList']['renderMessage'] = useFreshCallback((props) => {
if (renderMessage) return renderMessage(props);
return ;
@@ -176,8 +201,8 @@ const createGroupChannelFragment = (initModule?: Partial): G
>
}>
): G
renderScrollToBottomButton={renderScrollToBottomButton}
onResendFailedMessage={resendMessage}
onDeleteMessage={deleteMessage}
- onPressMediaMessage={onPressMediaMessage}
+ onPressMediaMessage={_onPressMediaMessage}
flatListProps={memoizedFlatListProps}
/>
void) => {
+ const { sbOptions, imageCompressionConfig } = useSendbirdChat();
+ const { STRINGS } = useLocalization();
+ const { fileService, mediaService } = usePlatformService();
+ const { alert } = useAlert();
+ const toast = useToast();
+
+ const sheetItems: BottomSheetItem['sheetItems'] = [];
+ const input = useIIFE(() => {
+ switch (true) {
+ case channel.isOpenChannel():
+ return sbOptions.uikit.openChannel.channel.input;
+ case channel.isGroupChannel():
+ return sbOptions.uikit.groupChannel.channel.input;
+ default:
+ return {
+ enableDocument: true,
+ camera: { enablePhoto: true, enableVideo: true },
+ gallery: { enablePhoto: true, enableVideo: true },
+ };
+ }
+ });
+
+ if (input.camera.enablePhoto) {
+ sheetItems.push({
+ title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_CAMERA_PHOTO,
+ icon: 'camera',
+ onPress: async () => {
+ const mediaFile = await fileService.openCamera({
+ mediaType: 'photo',
+ onOpenFailure: (error) => {
+ if (error.code === SBUError.CODE.ERR_PERMISSIONS_DENIED) {
+ alert({
+ title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
+ message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE(
+ STRINGS.LABELS.PERMISSION_CAMERA,
+ STRINGS.LABELS.PERMISSION_APP_NAME,
+ ),
+ buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
+ });
+ } else {
+ toast.show(STRINGS.TOAST.OPEN_CAMERA_ERROR, 'error');
+ }
+ },
+ });
+
+ if (mediaFile) {
+ // Image compression
+ if (
+ isImage(mediaFile.uri, mediaFile.type) &&
+ shouldCompressImage(mediaFile.type, sbOptions.chat.imageCompressionEnabled)
+ ) {
+ await SBUUtils.safeRun(async () => {
+ const compressed = await mediaService.compressImage({
+ uri: mediaFile.uri,
+ maxWidth: imageCompressionConfig.width,
+ maxHeight: imageCompressionConfig.height,
+ compressionRate: imageCompressionConfig.compressionRate,
+ });
+
+ if (compressed) {
+ mediaFile.uri = compressed.uri;
+ mediaFile.size = compressed.size;
+ }
+ });
+ }
+
+ sendFileMessage(mediaFile);
+ }
+ },
+ });
+ }
+
+ if (input.camera.enableVideo) {
+ sheetItems.push({
+ title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_CAMERA_VIDEO,
+ icon: 'camera',
+ onPress: async () => {
+ const mediaFile = await fileService.openCamera({
+ mediaType: 'video',
+ onOpenFailure: (error) => {
+ if (error.code === SBUError.CODE.ERR_PERMISSIONS_DENIED) {
+ alert({
+ title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
+ message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE(
+ STRINGS.LABELS.PERMISSION_CAMERA,
+ STRINGS.LABELS.PERMISSION_APP_NAME,
+ ),
+ buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
+ });
+ } else {
+ toast.show(STRINGS.TOAST.OPEN_CAMERA_ERROR, 'error');
+ }
+ },
+ });
+
+ if (mediaFile) {
+ sendFileMessage(mediaFile);
+ }
+ },
+ });
+ }
+
+ if (input.gallery.enablePhoto || input.gallery.enableVideo) {
+ const mediaType = (() => {
+ switch (true) {
+ case input.gallery.enablePhoto && input.gallery.enableVideo:
+ return 'all';
+ case input.gallery.enablePhoto && !input.gallery.enableVideo:
+ return 'photo';
+ case !input.gallery.enablePhoto && input.gallery.enableVideo:
+ return 'video';
+ default:
+ return 'all';
+ }
+ })();
+
+ sheetItems.push({
+ title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_PHOTO_LIBRARY,
+ icon: 'photo',
+ onPress: async () => {
+ const mediaFiles = await fileService.openMediaLibrary({
+ selectionLimit: 1,
+ mediaType,
+ onOpenFailure: (error) => {
+ if (error.code === SBUError.CODE.ERR_PERMISSIONS_DENIED) {
+ alert({
+ title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
+ message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE(
+ STRINGS.LABELS.PERMISSION_DEVICE_STORAGE,
+ STRINGS.LABELS.PERMISSION_APP_NAME,
+ ),
+ buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
+ });
+ } else {
+ toast.show(STRINGS.TOAST.OPEN_PHOTO_LIBRARY_ERROR, 'error');
+ }
+ },
+ });
+
+ if (mediaFiles && mediaFiles[0]) {
+ const mediaFile = mediaFiles[0];
+
+ // Image compression
+ if (
+ isImage(mediaFile.uri, mediaFile.type) &&
+ shouldCompressImage(mediaFile.type, sbOptions.chat.imageCompressionEnabled)
+ ) {
+ await SBUUtils.safeRun(async () => {
+ const compressed = await mediaService.compressImage({
+ uri: mediaFile.uri,
+ maxWidth: imageCompressionConfig.width,
+ maxHeight: imageCompressionConfig.height,
+ compressionRate: imageCompressionConfig.compressionRate,
+ });
+
+ if (compressed) {
+ mediaFile.uri = compressed.uri;
+ mediaFile.size = compressed.size;
+ }
+ });
+ }
+
+ sendFileMessage(mediaFile);
+ }
+ },
+ });
+ }
+
+ if (input.enableDocument) {
+ sheetItems.push({
+ title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_FILES,
+ icon: 'document',
+ onPress: async () => {
+ const documentFile = await fileService.openDocument({
+ onOpenFailure: () => toast.show(STRINGS.TOAST.OPEN_FILES_ERROR, 'error'),
+ });
+
+ if (documentFile) {
+ // Image compression
+ if (
+ isImage(documentFile.uri, documentFile.type) &&
+ shouldCompressImage(documentFile.type, sbOptions.chat.imageCompressionEnabled)
+ ) {
+ await SBUUtils.safeRun(async () => {
+ const compressed = await mediaService.compressImage({
+ uri: documentFile.uri,
+ maxWidth: imageCompressionConfig.width,
+ maxHeight: imageCompressionConfig.height,
+ compressionRate: imageCompressionConfig.compressionRate,
+ });
+
+ if (compressed) {
+ documentFile.uri = compressed.uri;
+ documentFile.size = compressed.size;
+ }
+ });
+ }
+
+ sendFileMessage(documentFile);
+ }
+ },
+ });
+ }
+
+ return sheetItems;
+};
diff --git a/packages/uikit-react-native/src/hooks/useConnection.ts b/packages/uikit-react-native/src/hooks/useConnection.ts
index 831433cc6..fb1aadbcd 100644
--- a/packages/uikit-react-native/src/hooks/useConnection.ts
+++ b/packages/uikit-react-native/src/hooks/useConnection.ts
@@ -67,7 +67,7 @@ const useConnection = () => {
Logger.warn('[useConnection]', 'clear cached-data');
await sdk.clearCachedData().catch((e) => Logger.warn('[useConnection]', 'clear cached-data failure', e));
} else if (sdk.currentUser) {
- await initEmoji(sdk, emojiManager);
+ await Promise.allSettled([initEmoji(sdk, emojiManager), initDashboardConfigs(sdk)]);
Logger.debug('[useConnection]', 'connected! (offline)');
setCurrentUser(sdk.currentUser);
diff --git a/packages/uikit-react-native/src/hooks/useVoiceMessageInput.ts b/packages/uikit-react-native/src/hooks/useVoiceMessageInput.ts
new file mode 100644
index 000000000..224b0972f
--- /dev/null
+++ b/packages/uikit-react-native/src/hooks/useVoiceMessageInput.ts
@@ -0,0 +1,237 @@
+import { useRef, useState } from 'react';
+
+import { useAlert } from '@sendbird/uikit-react-native-foundation';
+import { Logger, getVoiceMessageFileObject, matchesOneOf } from '@sendbird/uikit-utils';
+
+import SBUUtils from '../libs/SBUUtils';
+import { FileType } from '../platform/types';
+import { useLocalization, usePlatformService } from './useContext';
+
+type State = {
+ /**
+ * Status
+ *
+ * idle:
+ * - cancel(): idle
+ * - startRecording(): recording
+ * recording:
+ * - cancel(): idle
+ * - stopRecording(): recording_completed
+ * - send(): recording_completed > idle
+ * recording_completed:
+ * - cancel(): idle
+ * - playPlayer(): playing
+ * - send(): idle
+ * playing:
+ * - cancel(): idle
+ * - pausePlayer(): playing_paused
+ * - send(): idle
+ * playing_paused:
+ * - cancel(): idle
+ * - playPlayer(): playing
+ * - send(): idle
+ * */
+ status: 'idle' | 'recording' | 'recording_completed' | 'playing' | 'playing_paused';
+ recordingTime: {
+ currentTime: number;
+ minDuration: number;
+ maxDuration: number;
+ };
+ playingTime: {
+ currentTime: number;
+ duration: number;
+ };
+};
+
+export interface VoiceMessageInputResult {
+ actions: {
+ cancel: () => Promise;
+ startRecording: () => Promise;
+ stopRecording: () => Promise;
+ playPlayer: () => Promise;
+ pausePlayer: () => Promise;
+ send: () => Promise;
+ };
+ state: State;
+}
+
+type Props = {
+ onClose: () => Promise;
+ onSend: (voiceFile: FileType, duration: number) => void;
+};
+
+const useVoiceMessageInput = ({ onSend, onClose }: Props): VoiceMessageInputResult => {
+ const { alert } = useAlert();
+ const { STRINGS } = useLocalization();
+ const { recorderService, playerService, fileService } = usePlatformService();
+ const [status, setStatus] = useState('idle');
+
+ const [recordingTime, setRecordingTime] = useState({
+ currentTime: 0,
+ minDuration: recorderService.options.minDuration,
+ maxDuration: recorderService.options.maxDuration,
+ });
+ const [playingTime, setPlayingTime] = useState({
+ currentTime: 0,
+ duration: 0,
+ });
+
+ const recordingPath = useRef<{ recordFilePath: string; uri: string }>();
+ const getVoiceMessageRecordingPath = () => {
+ if (!recordingPath.current) throw new Error('No recording path');
+ return recordingPath.current;
+ };
+ const setVoiceMessageRecordingPath = (path: { recordFilePath: string; uri: string }) => {
+ recordingPath.current = path;
+ };
+
+ const clear = async () => {
+ recordingPath.current = undefined;
+ await playerService.reset();
+ await recorderService.reset();
+ setRecordingTime({
+ currentTime: 0,
+ minDuration: recorderService.options.minDuration,
+ maxDuration: recorderService.options.maxDuration,
+ });
+ setPlayingTime({
+ currentTime: 0,
+ duration: 0,
+ });
+ setStatus('idle');
+ };
+
+ return {
+ state: {
+ status,
+ recordingTime,
+ playingTime,
+ },
+ actions: {
+ async cancel() {
+ await clear();
+ },
+ async startRecording() {
+ const granted = await recorderService.requestPermission();
+ if (!granted) {
+ await onClose();
+ alert({
+ title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
+ message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE(
+ STRINGS.LABELS.PERMISSION_MICROPHONE,
+ STRINGS.LABELS.PERMISSION_APP_NAME,
+ ),
+ buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
+ });
+ Logger.error('Failed to request permission for recorder');
+ return;
+ }
+
+ if (matchesOneOf(status, ['idle'])) {
+ // Before start recording, if player is not idle, reset it.
+ if (playerService.state !== 'idle') {
+ await playerService.reset();
+ }
+
+ const unsubscribeRecording = recorderService.addRecordingListener(({ currentTime }) => {
+ setRecordingTime({
+ currentTime,
+ maxDuration: recorderService.options.maxDuration,
+ minDuration: recorderService.options.minDuration,
+ });
+ setPlayingTime((prev) => ({ ...prev, duration: currentTime }));
+ });
+
+ const unsubscribeState = recorderService.addStateListener((state) => {
+ switch (state) {
+ case 'recording':
+ setStatus('recording');
+ break;
+ case 'completed':
+ setStatus('recording_completed');
+ unsubscribeRecording();
+ unsubscribeState();
+ break;
+ }
+ });
+
+ if (SBUUtils.isExpo()) {
+ await recorderService.record();
+ if (recorderService.uri) {
+ setVoiceMessageRecordingPath({ recordFilePath: recorderService.uri, uri: recorderService.uri });
+ }
+ } else {
+ setVoiceMessageRecordingPath(fileService.createRecordFilePath(recorderService.options.extension));
+ await recorderService.record(getVoiceMessageRecordingPath().recordFilePath);
+ }
+ }
+ },
+ async stopRecording() {
+ if (matchesOneOf(status, ['recording'])) {
+ await recorderService.stop();
+ }
+ },
+ async playPlayer() {
+ const granted = await playerService.requestPermission();
+ if (!granted) {
+ alert({
+ title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
+ message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE(
+ STRINGS.LABELS.PERMISSION_DEVICE_STORAGE,
+ STRINGS.LABELS.PERMISSION_APP_NAME,
+ ),
+ buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
+ });
+ Logger.error('Failed to request permission for player');
+ return;
+ }
+
+ if (matchesOneOf(status, ['recording_completed', 'playing_paused'])) {
+ const unsubscribePlayback = playerService.addPlaybackListener(({ currentTime, duration }) => {
+ setPlayingTime({ currentTime, duration });
+ });
+
+ const unsubscribeState = playerService.addStateListener((state) => {
+ switch (state) {
+ case 'playing':
+ setStatus('playing');
+ break;
+ case 'paused': {
+ setStatus('playing_paused');
+ unsubscribeState();
+ unsubscribePlayback();
+ break;
+ }
+ case 'stopped': {
+ setStatus('playing_paused');
+ unsubscribeState();
+ unsubscribePlayback();
+ setPlayingTime((prev) => ({ ...prev, currentTime: 0 }));
+ break;
+ }
+ }
+ });
+
+ await playerService.play(getVoiceMessageRecordingPath().recordFilePath);
+ }
+ },
+ async pausePlayer() {
+ if (matchesOneOf(status, ['playing'])) {
+ await playerService.pause();
+ }
+ },
+ async send() {
+ if (
+ matchesOneOf(status, ['recording', 'recording_completed', 'playing', 'playing_paused']) &&
+ recordingPath.current
+ ) {
+ const voiceFile = getVoiceMessageFileObject(recordingPath.current.uri, recorderService.options.extension);
+ onSend(voiceFile, Math.floor(recordingTime.currentTime));
+ await clear();
+ }
+ },
+ },
+ };
+};
+
+export default useVoiceMessageInput;
diff --git a/packages/uikit-react-native/src/index.ts b/packages/uikit-react-native/src/index.ts
index 1e59a09b0..56d0f0dde 100644
--- a/packages/uikit-react-native/src/index.ts
+++ b/packages/uikit-react-native/src/index.ts
@@ -69,10 +69,14 @@ export { default as createNativeFileService } from './platform/createFileService
export { default as createNativeClipboardService } from './platform/createClipboardService.native';
export { default as createNativeNotificationService } from './platform/createNotificationService.native';
export { default as createNativeMediaService } from './platform/createMediaService.native';
+export { default as createNativePlayerService } from './platform/createPlayerService.native';
+export { default as createNativeRecorderService } from './platform/createRecorderService.native';
export { default as createExpoFileService } from './platform/createFileService.expo';
export { default as createExpoClipboardService } from './platform/createClipboardService.expo';
export { default as createExpoNotificationService } from './platform/createNotificationService.expo';
export { default as createExpoMediaService } from './platform/createMediaService.expo';
+export { default as createExpoPlayerService } from './platform/createPlayerService.expo';
+export { default as createExpoRecorderService } from './platform/createRecorderService.expo';
export * from './platform/types';
/** Feature - shared **/
diff --git a/packages/uikit-react-native/src/libs/SBUUtils.ts b/packages/uikit-react-native/src/libs/SBUUtils.ts
index c54de8e78..bda3dec13 100644
--- a/packages/uikit-react-native/src/libs/SBUUtils.ts
+++ b/packages/uikit-react-native/src/libs/SBUUtils.ts
@@ -37,4 +37,9 @@ export default class SBUUtils {
await callback();
} catch (e) {}
}
+
+ static isExpo() {
+ const _g = global ?? window;
+ return typeof _g === 'object' && 'expo' in _g;
+ }
}
diff --git a/packages/uikit-react-native/src/libs/VoiceMessageConfig.ts b/packages/uikit-react-native/src/libs/VoiceMessageConfig.ts
new file mode 100644
index 000000000..c6863f7f6
--- /dev/null
+++ b/packages/uikit-react-native/src/libs/VoiceMessageConfig.ts
@@ -0,0 +1,28 @@
+export interface VoiceMessageConfigInterface {
+ recorder: {
+ minDuration: number;
+ maxDuration: number;
+ };
+}
+
+class VoiceMessageConfig {
+ static DEFAULT = {
+ RECORDER: {
+ MIN_DURATION: 1000,
+ MAX_DURATION: 600 * 1000,
+ EXTENSION: 'm4a',
+
+ BIT_RATE: 12000,
+ SAMPLE_RATE: 11025,
+ CHANNELS: 1,
+ },
+ };
+
+ constructor(private _config: VoiceMessageConfigInterface) {}
+
+ get recorder() {
+ return this._config.recorder;
+ }
+}
+
+export default VoiceMessageConfig;
diff --git a/packages/uikit-react-native/src/localization/StringSet.type.ts b/packages/uikit-react-native/src/localization/StringSet.type.ts
index a12a1e938..d6256daed 100644
--- a/packages/uikit-react-native/src/localization/StringSet.type.ts
+++ b/packages/uikit-react-native/src/localization/StringSet.type.ts
@@ -240,6 +240,7 @@ export interface StringSet {
PERMISSION_APP_NAME: string;
PERMISSION_CAMERA: string;
PERMISSION_DEVICE_STORAGE: string;
+ PERMISSION_MICROPHONE: string;
USER_NO_NAME: string;
CHANNEL_NO_MEMBERS: string;
@@ -249,6 +250,7 @@ export interface StringSet {
parentMessage: SendbirdUserMessage | SendbirdFileMessage,
currentUserId?: string,
) => string;
+ MESSAGE_UNAVAILABLE: string;
USER_BAR_ME_POSTFIX: string;
USER_BAR_OPERATOR: string;
@@ -291,6 +293,10 @@ export interface StringSet {
/** Channel > Message > Failed **/
CHANNEL_MESSAGE_FAILED_RETRY: string;
CHANNEL_MESSAGE_FAILED_REMOVE: string;
+
+ /** Voice message **/
+ VOICE_MESSAGE: string;
+ VOICE_MESSAGE_INPUT_CANCEL: string;
};
FILE_VIEWER: {
TITLE: (message: SendbirdFileMessage) => string;
@@ -331,6 +337,8 @@ export interface StringSet {
RESEND_MSG_ERROR: string;
DELETE_MSG_ERROR: string;
SEND_MSG_ERROR: string;
+ USER_MUTED_ERROR: string;
+ CHANNEL_FROZEN_ERROR: string;
UPDATE_MSG_ERROR: string;
TURN_ON_NOTIFICATIONS_ERROR: string;
TURN_OFF_NOTIFICATIONS_ERROR: string;
diff --git a/packages/uikit-react-native/src/localization/createBaseStringSet.ts b/packages/uikit-react-native/src/localization/createBaseStringSet.ts
index 8c4a18c60..d4a8553bc 100644
--- a/packages/uikit-react-native/src/localization/createBaseStringSet.ts
+++ b/packages/uikit-react-native/src/localization/createBaseStringSet.ts
@@ -3,16 +3,16 @@ import type { Locale } from 'date-fns';
import type { PartialDeep } from '@sendbird/uikit-utils';
import {
getDateSeparatorFormat,
- getFileTypeFromMessage,
- getGroupChannelLastMessage,
getGroupChannelPreviewTime,
getGroupChannelTitle,
getMessagePreviewBody,
getMessagePreviewTime,
getMessagePreviewTitle,
getMessageTimeFormat,
+ getMessageType,
getOpenChannelParticipants,
getOpenChannelTitle,
+ isVoiceMessage,
} from '@sendbird/uikit-utils';
import { UNKNOWN_USER_ID } from '../constants';
@@ -33,6 +33,7 @@ type StringSetCreateOptions = {
export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOptions): StringSet => {
const USER_NO_NAME = overrides?.LABELS?.USER_NO_NAME ?? '(No name)';
const CHANNEL_NO_MEMBERS = overrides?.LABELS?.CHANNEL_NO_MEMBERS ?? '(No members)';
+
return {
OPEN_CHANNEL: {
HEADER_TITLE: (channel) => getOpenChannelTitle(channel),
@@ -191,7 +192,11 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp
CHANNEL_PREVIEW_TITLE: (currentUserId, channel) =>
getGroupChannelTitle(currentUserId, channel, USER_NO_NAME, CHANNEL_NO_MEMBERS),
CHANNEL_PREVIEW_TITLE_CAPTION: (channel, locale) => getGroupChannelPreviewTime(channel, locale ?? dateLocale),
- CHANNEL_PREVIEW_BODY: (channel) => getGroupChannelLastMessage(channel),
+ CHANNEL_PREVIEW_BODY: (channel) => {
+ if (!channel.lastMessage) return '';
+ if (isVoiceMessage(channel.lastMessage)) return 'Voice message';
+ return getMessagePreviewBody(channel.lastMessage);
+ },
TYPE_SELECTOR_HEADER_TITLE: 'Channel type',
TYPE_SELECTOR_GROUP: 'Group',
TYPE_SELECTOR_SUPER_GROUP: 'Super group',
@@ -232,7 +237,10 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp
HEADER_INPUT_PLACEHOLDER: 'Search',
HEADER_RIGHT: 'Search',
SEARCH_RESULT_ITEM_TITLE: (message) => getMessagePreviewTitle(message),
- SEARCH_RESULT_ITEM_BODY: (message) => getMessagePreviewBody(message),
+ SEARCH_RESULT_ITEM_BODY: (message) => {
+ if (isVoiceMessage(message)) return 'Voice message';
+ return getMessagePreviewBody(message);
+ },
SEARCH_RESULT_ITEM_TITLE_CAPTION: (message, locale) => {
return getMessagePreviewTime(message.createdAt, locale ?? dateLocale);
},
@@ -241,6 +249,7 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp
PERMISSION_APP_NAME: 'Application',
PERMISSION_CAMERA: 'camera',
PERMISSION_DEVICE_STORAGE: 'device storage',
+ PERMISSION_MICROPHONE: 'microphone',
USER_NO_NAME,
CHANNEL_NO_MEMBERS,
TYPING_INDICATOR_TYPINGS: (users, NO_NAME = USER_NO_NAME) => {
@@ -255,6 +264,7 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp
const receiverNickname = parent.sender.nickname || USER_NO_NAME;
return `${reply.sender.userId !== currentUserId ? senderNickname : 'You'} replied to ${receiverNickname}`;
},
+ MESSAGE_UNAVAILABLE: 'Message unavailable',
USER_BAR_ME_POSTFIX: ' (You)',
USER_BAR_OPERATOR: 'Operator',
@@ -288,22 +298,26 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp
CHANNEL_INPUT_REPLY_PREVIEW_TITLE: (user) => `Reply to ${user.nickname || USER_NO_NAME}`,
CHANNEL_INPUT_REPLY_PREVIEW_BODY: (message) => {
if (message.isFileMessage()) {
- const fileType = getFileTypeFromMessage(message);
- switch (fileType) {
- case 'image':
+ const messageType = getMessageType(message);
+ switch (messageType) {
+ case 'file.image':
return message.type.toLowerCase().includes('gif') ? 'GIF' : 'Photo';
- case 'video':
+ case 'file.video':
return 'Video';
- case 'audio':
+ case 'file.audio':
return 'Audio';
+ case 'file.voice':
+ return 'Voice message';
default:
return message.name;
}
} else if (message.isUserMessage()) {
return message.message;
}
- return 'Unknown message type.';
+ return 'Unknown message';
},
+ VOICE_MESSAGE: 'Voice message',
+ VOICE_MESSAGE_INPUT_CANCEL: 'Cancel',
...overrides?.LABELS,
},
FILE_VIEWER: {
@@ -329,7 +343,7 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp
ALERT_DEFAULT_OK: 'OK',
ALERT_PERMISSIONS_TITLE: 'Allow access?',
ALERT_PERMISSIONS_MESSAGE: (permission, appName = 'Application') => {
- return `${appName} need permission to access your ${permission}.`;
+ return `${appName} needs permission to access your ${permission}.`;
},
ALERT_PERMISSIONS_OK: 'Go to settings',
PROMPT_DEFAULT_OK: 'Submit',
@@ -348,6 +362,8 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp
DELETE_MSG_ERROR: "Couldn't delete message.",
RESEND_MSG_ERROR: "Couldn't send message.",
SEND_MSG_ERROR: "Couldn't send message.",
+ USER_MUTED_ERROR: "You're muted by the operator.",
+ CHANNEL_FROZEN_ERROR: 'Channel is frozen.',
UPDATE_MSG_ERROR: "Couldn't edit message.",
TURN_ON_NOTIFICATIONS_ERROR: "Couldn't turn on notifications.",
TURN_OFF_NOTIFICATIONS_ERROR: "Couldn't turn off notifications.",
diff --git a/packages/uikit-react-native/src/platform/createFileService.expo.ts b/packages/uikit-react-native/src/platform/createFileService.expo.ts
index a98091994..5c363692e 100644
--- a/packages/uikit-react-native/src/platform/createFileService.expo.ts
+++ b/packages/uikit-react-native/src/platform/createFileService.expo.ts
@@ -142,6 +142,16 @@ const createExpoFileService = ({
}
return response.uri;
}
+ createRecordFilePath(customExtension = 'm4a'): { recordFilePath: string; uri: string } {
+ const basePath = fsModule.cacheDirectory;
+ if (!basePath) throw new Error('Cannot determine directory');
+
+ const filename = `record-${Date.now()}.${customExtension}`;
+ return {
+ uri: `${basePath}/${filename}`,
+ recordFilePath: `${basePath}/${filename}`,
+ };
+ }
}
return new ExpoFileServiceInterface();
diff --git a/packages/uikit-react-native/src/platform/createFileService.native.ts b/packages/uikit-react-native/src/platform/createFileService.native.ts
index 814c80b84..afee38f3d 100644
--- a/packages/uikit-react-native/src/platform/createFileService.native.ts
+++ b/packages/uikit-react-native/src/platform/createFileService.native.ts
@@ -241,6 +241,25 @@ const createNativeFileService = ({
} as const,
};
};
+
+ createRecordFilePath(customExtension = 'm4a'): { recordFilePath: string; uri: string } {
+ const filename = `record-${Date.now()}.${customExtension}`;
+ const path = `${fsModule.Dirs.CacheDir}/${filename}`;
+ return Platform.select({
+ ios: {
+ uri: path,
+ recordFilePath: filename,
+ },
+ android: {
+ uri: path.startsWith('file://') ? path : 'file://' + path,
+ recordFilePath: path,
+ },
+ default: {
+ uri: path,
+ recordFilePath: path,
+ },
+ });
+ }
}
return new NativeFileService();
diff --git a/packages/uikit-react-native/src/platform/createPlayerService.expo.tsx b/packages/uikit-react-native/src/platform/createPlayerService.expo.tsx
new file mode 100644
index 000000000..ab416c57b
--- /dev/null
+++ b/packages/uikit-react-native/src/platform/createPlayerService.expo.tsx
@@ -0,0 +1,142 @@
+import type * as ExpoAV from 'expo-av';
+
+import { matchesOneOf } from '@sendbird/uikit-utils';
+
+import expoPermissionGranted from '../utils/expoPermissionGranted';
+import type { PlayerServiceInterface, Unsubscribe } from './types';
+
+type Modules = {
+ avModule: typeof ExpoAV;
+};
+type PlaybackListener = Parameters[number];
+type StateListener = Parameters[number];
+const createExpoPlayerService = ({ avModule }: Modules): PlayerServiceInterface => {
+ const sound = new avModule.Audio.Sound();
+
+ class VoicePlayer implements PlayerServiceInterface {
+ uri?: string;
+ state: PlayerServiceInterface['state'] = 'idle';
+
+ private readonly playbackSubscribers = new Set();
+ private readonly stateSubscribers = new Set();
+
+ private setState = (state: PlayerServiceInterface['state']) => {
+ this.state = state;
+ this.stateSubscribers.forEach((callback) => {
+ callback(state);
+ });
+ };
+
+ private setListener = () => {
+ sound.setProgressUpdateIntervalAsync(100);
+ sound.setOnPlaybackStatusUpdate((status) => {
+ if (status.isLoaded) {
+ if (status.didJustFinish) this.stop();
+ if (status.isPlaying) {
+ this.playbackSubscribers.forEach((callback) => {
+ callback({
+ currentTime: status.positionMillis,
+ duration: status.durationMillis ?? 0,
+ stopped: status.didJustFinish,
+ });
+ });
+ }
+ }
+ });
+ };
+
+ private removeListener = () => {
+ sound.setOnPlaybackStatusUpdate(null);
+ };
+
+ public requestPermission = async (): Promise => {
+ const status = await avModule.Audio.getPermissionsAsync();
+ if (expoPermissionGranted([status])) {
+ return true;
+ } else {
+ const status = await avModule.Audio.requestPermissionsAsync();
+ return expoPermissionGranted([status]);
+ }
+ };
+
+ public addPlaybackListener = (callback: PlaybackListener): Unsubscribe => {
+ this.playbackSubscribers.add(callback);
+ return () => {
+ this.playbackSubscribers.delete(callback);
+ };
+ };
+
+ public addStateListener = (callback: (state: PlayerServiceInterface['state']) => void): Unsubscribe => {
+ this.stateSubscribers.add(callback);
+ return () => {
+ this.stateSubscribers.delete(callback);
+ };
+ };
+
+ private prepare = async (uri: string) => {
+ this.setState('preparing');
+ await sound.loadAsync({ uri }, { shouldPlay: false }, true);
+ this.uri = uri;
+ };
+
+ public play = async (uri: string): Promise => {
+ if (matchesOneOf(this.state, ['idle', 'stopped'])) {
+ try {
+ await this.prepare(uri);
+ this.setListener();
+ await sound.playAsync();
+ this.setState('playing');
+ } catch (e) {
+ this.setState('idle');
+ this.uri = undefined;
+ this.removeListener();
+ throw e;
+ }
+ } else if (matchesOneOf(this.state, ['paused']) && this.uri === uri) {
+ try {
+ this.setListener();
+ await sound.playAsync();
+ this.setState('playing');
+ } catch (e) {
+ this.removeListener();
+ throw e;
+ }
+ }
+ };
+
+ public pause = async (): Promise => {
+ if (matchesOneOf(this.state, ['playing'])) {
+ await sound.pauseAsync();
+ this.removeListener();
+ this.setState('paused');
+ }
+ };
+
+ public stop = async (): Promise => {
+ if (matchesOneOf(this.state, ['playing', 'paused'])) {
+ await sound.stopAsync();
+ await sound.unloadAsync();
+ this.removeListener();
+ this.setState('stopped');
+ }
+ };
+
+ public reset = async (): Promise => {
+ await this.stop();
+ this.setState('idle');
+ this.uri = undefined;
+ this.playbackSubscribers.clear();
+ this.stateSubscribers.clear();
+ };
+
+ public seek = async (time: number): Promise => {
+ if (matchesOneOf(this.state, ['playing', 'paused'])) {
+ await sound.playFromPositionAsync(time);
+ }
+ };
+ }
+
+ return new VoicePlayer();
+};
+
+export default createExpoPlayerService;
diff --git a/packages/uikit-react-native/src/platform/createPlayerService.native.tsx b/packages/uikit-react-native/src/platform/createPlayerService.native.tsx
new file mode 100644
index 000000000..65abfb65a
--- /dev/null
+++ b/packages/uikit-react-native/src/platform/createPlayerService.native.tsx
@@ -0,0 +1,148 @@
+import { Platform } from 'react-native';
+import type * as RNAudioRecorder from 'react-native-audio-recorder-player';
+import * as Permissions from 'react-native-permissions';
+
+import { matchesOneOf, sleep } from '@sendbird/uikit-utils';
+
+import type { PlayerServiceInterface, Unsubscribe } from './types';
+
+type Modules = {
+ audioRecorderModule: typeof RNAudioRecorder;
+ permissionModule: typeof Permissions;
+};
+type PlaybackListener = Parameters[number];
+type StateListener = Parameters[number];
+const createNativePlayerService = ({ audioRecorderModule, permissionModule }: Modules): PlayerServiceInterface => {
+ const module = new audioRecorderModule.default();
+
+ class VoicePlayer implements PlayerServiceInterface {
+ uri?: string;
+ state: PlayerServiceInterface['state'] = 'idle';
+
+ private readonly playbackSubscribers = new Set();
+ private readonly stateSubscribers = new Set();
+
+ constructor() {
+ module.setSubscriptionDuration(0.1);
+ }
+
+ private setState = (state: PlayerServiceInterface['state']) => {
+ this.state = state;
+ this.stateSubscribers.forEach((callback) => {
+ callback(state);
+ });
+ };
+
+ private setListener = () => {
+ module.addPlayBackListener((data) => {
+ const stopped = data.currentPosition >= data.duration;
+
+ if (stopped) this.stop();
+ if (this.state === 'playing') {
+ this.playbackSubscribers.forEach((callback) => {
+ callback({ currentTime: data.currentPosition, duration: data.duration, stopped });
+ });
+ }
+ });
+ };
+
+ private removeListener = () => {
+ module.removePlayBackListener();
+ };
+
+ public requestPermission = async (): Promise => {
+ if (Platform.OS === 'android') {
+ const { READ_MEDIA_AUDIO, READ_EXTERNAL_STORAGE } = permissionModule.PERMISSIONS.ANDROID;
+ const permission = Platform.Version > 32 ? READ_MEDIA_AUDIO : READ_EXTERNAL_STORAGE;
+
+ const status = await permissionModule.check(permission);
+ if (status === 'granted') {
+ return true;
+ } else {
+ const status = await permissionModule.request(permission);
+ return status === 'granted';
+ }
+ } else {
+ return true;
+ }
+ };
+
+ public addPlaybackListener = (callback: PlaybackListener): Unsubscribe => {
+ this.playbackSubscribers.add(callback);
+ return () => {
+ this.playbackSubscribers.delete(callback);
+ };
+ };
+
+ public addStateListener = (callback: (state: PlayerServiceInterface['state']) => void): Unsubscribe => {
+ this.stateSubscribers.add(callback);
+ return () => {
+ this.stateSubscribers.delete(callback);
+ };
+ };
+
+ public play = async (uri: string): Promise => {
+ if (matchesOneOf(this.state, ['idle', 'stopped'])) {
+ try {
+ this.setState('preparing');
+ this.uri = uri;
+ this.setListener();
+
+ // FIXME: Workaround, `module.startPlayer()` caused a significant frame-drop and prevented the 'preparing' UI transition.
+ await sleep(0);
+ await module.startPlayer(uri);
+
+ this.setState('playing');
+ } catch (e) {
+ this.setState('idle');
+ this.uri = undefined;
+ this.removeListener();
+ throw e;
+ }
+ } else if (matchesOneOf(this.state, ['paused']) && this.uri === uri) {
+ try {
+ this.setListener();
+ await module.resumePlayer();
+ this.setState('playing');
+ } catch (e) {
+ this.removeListener();
+ throw e;
+ }
+ }
+ };
+
+ public pause = async (): Promise => {
+ if (matchesOneOf(this.state, ['playing'])) {
+ await module.pausePlayer();
+ this.removeListener();
+ this.setState('paused');
+ }
+ };
+
+ public stop = async (): Promise => {
+ if (matchesOneOf(this.state, ['preparing', 'playing', 'paused'])) {
+ await module.stopPlayer();
+ this.removeListener();
+ this.setState('stopped');
+ }
+ };
+
+ public reset = async (): Promise => {
+ await this.stop();
+ this.setState('idle');
+ this.uri = undefined;
+ this.playbackSubscribers.clear();
+ this.stateSubscribers.clear();
+ };
+
+ public seek = async (time: number): Promise => {
+ if (matchesOneOf(this.state, ['playing', 'paused'])) {
+ await module.seekToPlayer(time);
+ }
+ };
+ }
+
+ return new VoicePlayer();
+};
+
+export default createNativePlayerService;
diff --git a/packages/uikit-react-native/src/platform/createRecorderService.expo.tsx b/packages/uikit-react-native/src/platform/createRecorderService.expo.tsx
new file mode 100644
index 000000000..25b2006c7
--- /dev/null
+++ b/packages/uikit-react-native/src/platform/createRecorderService.expo.tsx
@@ -0,0 +1,160 @@
+import * as ExpoAV from 'expo-av';
+import type { RecordingOptions } from 'expo-av/build/Audio/Recording.types';
+import { Platform } from 'react-native';
+
+import { matchesOneOf, sleep } from '@sendbird/uikit-utils';
+
+import VoiceMessageConfig from '../libs/VoiceMessageConfig';
+import expoPermissionGranted from '../utils/expoPermissionGranted';
+import type { RecorderServiceInterface, Unsubscribe } from './types';
+
+type RecordingListener = Parameters[number];
+type StateListener = Parameters[number];
+type Modules = {
+ avModule: typeof ExpoAV;
+};
+const createExpoRecorderService = ({ avModule }: Modules): RecorderServiceInterface => {
+ class VoiceRecorder implements RecorderServiceInterface {
+ public uri: RecorderServiceInterface['uri'] = undefined;
+ public state: RecorderServiceInterface['state'] = 'idle';
+ public options: RecorderServiceInterface['options'] = {
+ minDuration: VoiceMessageConfig.DEFAULT.RECORDER.MIN_DURATION,
+ maxDuration: VoiceMessageConfig.DEFAULT.RECORDER.MAX_DURATION,
+ extension: VoiceMessageConfig.DEFAULT.RECORDER.EXTENSION,
+ };
+
+ // NOTE: In Android, even when startRecorder() is awaited, if stop() is executed immediately afterward, an error occurs
+ private _recordStartedAt = 0;
+ private _getRecorderStopSafeBuffer = () => {
+ const minWaitingTime = 500;
+ const elapsedTime = Date.now() - this._recordStartedAt;
+ if (elapsedTime > minWaitingTime) return 0;
+ else return minWaitingTime - elapsedTime;
+ };
+
+ private _recorder = new avModule.Audio.Recording();
+ private readonly _recordingSubscribers = new Set();
+ private readonly _stateSubscribers = new Set();
+ private readonly _audioSettings = {
+ sampleRate: VoiceMessageConfig.DEFAULT.RECORDER.SAMPLE_RATE,
+ bitRate: VoiceMessageConfig.DEFAULT.RECORDER.BIT_RATE,
+ numberOfChannels: VoiceMessageConfig.DEFAULT.RECORDER.CHANNELS,
+ // encoding: mpeg4_aac
+ };
+ private readonly _audioOptions: RecordingOptions = {
+ android: {
+ ...this._audioSettings,
+ extension: `.${this.options.extension}`,
+ audioEncoder: avModule.Audio.AndroidAudioEncoder.AAC,
+ outputFormat: avModule.Audio.AndroidOutputFormat.MPEG_4,
+ },
+ ios: {
+ ...this._audioSettings,
+ extension: `.${this.options.extension}`,
+ outputFormat: avModule.Audio.IOSOutputFormat.MPEG4AAC,
+ audioQuality: avModule.Audio.IOSAudioQuality.HIGH,
+ },
+ web: {},
+ };
+
+ private prepare = async () => {
+ this.setState('preparing');
+ if (Platform.OS === 'ios') {
+ await avModule.Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true });
+ }
+
+ if (this._recorder._isDoneRecording) {
+ this._recorder = new avModule.Audio.Recording();
+ }
+ this._recorder.setProgressUpdateInterval(100);
+ this._recorder.setOnRecordingStatusUpdate((status) => {
+ const completed = status.durationMillis >= this.options.maxDuration;
+ if (completed) this.stop();
+ if (status.isRecording) {
+ this._recordingSubscribers.forEach((callback) => {
+ callback({ currentTime: status.durationMillis, completed: completed });
+ });
+ }
+ });
+ await this._recorder.prepareToRecordAsync(this._audioOptions);
+ };
+
+ private setState = (state: RecorderServiceInterface['state']) => {
+ this.state = state;
+ this._stateSubscribers.forEach((callback) => {
+ callback(state);
+ });
+ };
+
+ public requestPermission = async (): Promise => {
+ const status = await avModule.Audio.getPermissionsAsync();
+ if (expoPermissionGranted([status])) {
+ return true;
+ } else {
+ const status = await avModule.Audio.requestPermissionsAsync();
+ return expoPermissionGranted([status]);
+ }
+ };
+
+ public addRecordingListener = (callback: RecordingListener): Unsubscribe => {
+ this._recordingSubscribers.add(callback);
+ return () => {
+ this._recordingSubscribers.delete(callback);
+ };
+ };
+
+ public addStateListener = (callback: StateListener): Unsubscribe => {
+ this._stateSubscribers.add(callback);
+ return () => {
+ this._stateSubscribers.delete(callback);
+ };
+ };
+
+ public record = async (): Promise => {
+ if (matchesOneOf(this.state, ['idle', 'completed'])) {
+ try {
+ await this.prepare();
+ await this._recorder.startAsync();
+
+ if (Platform.OS === 'android') {
+ this._recordStartedAt = Date.now();
+ }
+
+ const uri = this._recorder.getURI();
+ if (uri) this.uri = uri;
+ this.setState('recording');
+ } catch (e) {
+ this.setState('idle');
+ throw e;
+ }
+ }
+ };
+
+ public stop = async (): Promise => {
+ if (matchesOneOf(this.state, ['recording'])) {
+ if (Platform.OS === 'android') {
+ const buffer = this._getRecorderStopSafeBuffer();
+ if (buffer > 0) await sleep(buffer);
+ }
+
+ await this._recorder.stopAndUnloadAsync();
+ if (Platform.OS === 'ios') {
+ await avModule.Audio.setAudioModeAsync({ allowsRecordingIOS: false, playsInSilentModeIOS: false });
+ }
+ this.setState('completed');
+ }
+ };
+
+ public reset = async (): Promise => {
+ await this.stop();
+ this.uri = undefined;
+ this._recordingSubscribers.clear();
+ this._recorder = new avModule.Audio.Recording();
+ this.setState('idle');
+ };
+ }
+
+ return new VoiceRecorder();
+};
+
+export default createExpoRecorderService;
diff --git a/packages/uikit-react-native/src/platform/createRecorderService.native.tsx b/packages/uikit-react-native/src/platform/createRecorderService.native.tsx
new file mode 100644
index 000000000..094e9ed54
--- /dev/null
+++ b/packages/uikit-react-native/src/platform/createRecorderService.native.tsx
@@ -0,0 +1,170 @@
+import { Platform } from 'react-native';
+import * as RNAudioRecorder from 'react-native-audio-recorder-player';
+import * as Permissions from 'react-native-permissions';
+import { Permission } from 'react-native-permissions/src/types';
+
+import { matchesOneOf, sleep } from '@sendbird/uikit-utils';
+
+import VoiceMessageConfig from '../libs/VoiceMessageConfig';
+import nativePermissionGranted from '../utils/nativePermissionGranted';
+import type { RecorderServiceInterface, Unsubscribe } from './types';
+
+type RecordingListener = Parameters[number];
+type StateListener = Parameters[number];
+type Modules = {
+ audioRecorderModule: typeof RNAudioRecorder;
+ permissionModule: typeof Permissions;
+};
+const createNativeRecorderService = ({ audioRecorderModule, permissionModule }: Modules): RecorderServiceInterface => {
+ const module = new audioRecorderModule.default();
+
+ class VoiceRecorder implements RecorderServiceInterface {
+ public uri: RecorderServiceInterface['uri'] = undefined;
+ public state: RecorderServiceInterface['state'] = 'idle';
+ public options: RecorderServiceInterface['options'] = {
+ minDuration: VoiceMessageConfig.DEFAULT.RECORDER.MIN_DURATION,
+ maxDuration: VoiceMessageConfig.DEFAULT.RECORDER.MAX_DURATION,
+ extension: VoiceMessageConfig.DEFAULT.RECORDER.EXTENSION,
+ };
+
+ // NOTE: In Android, even when startRecorder() is awaited, if stop() is executed immediately afterward, an error occurs
+ private _recordStartedAt = 0;
+ private _getRecorderStopSafeBuffer = () => {
+ const minWaitingTime = 500;
+ const elapsedTime = Date.now() - this._recordStartedAt;
+ if (elapsedTime > minWaitingTime) return 0;
+ else return minWaitingTime - elapsedTime;
+ };
+
+ private readonly recordingSubscribers = new Set();
+ private readonly stateSubscribers = new Set();
+ private readonly audioSettings = {
+ sampleRate: VoiceMessageConfig.DEFAULT.RECORDER.SAMPLE_RATE,
+ bitRate: VoiceMessageConfig.DEFAULT.RECORDER.BIT_RATE,
+ audioChannels: VoiceMessageConfig.DEFAULT.RECORDER.CHANNELS,
+ // encoding: mpeg4_aac
+ };
+ private readonly audioOptions = Platform.select({
+ android: {
+ AudioEncodingBitRateAndroid: this.audioSettings.bitRate,
+ AudioChannelsAndroid: this.audioSettings.audioChannels,
+ AudioSamplingRateAndroid: this.audioSettings.sampleRate,
+ AudioEncoderAndroid: audioRecorderModule.AudioEncoderAndroidType.AAC,
+ OutputFormatAndroid: audioRecorderModule.OutputFormatAndroidType.MPEG_4,
+ AudioSourceAndroid: audioRecorderModule.AudioSourceAndroidType.VOICE_RECOGNITION,
+ },
+ ios: {
+ AVEncoderBitRateKeyIOS: this.audioSettings.bitRate,
+ AVNumberOfChannelsKeyIOS: this.audioSettings.audioChannels,
+ AVSampleRateKeyIOS: this.audioSettings.sampleRate,
+ AVFormatIDKeyIOS: audioRecorderModule.AVEncodingOption.mp4, // same with aac
+ AVEncoderAudioQualityKeyIOS: audioRecorderModule.AVEncoderAudioQualityIOSType.high,
+ },
+ default: {},
+ });
+
+ constructor() {
+ module.setSubscriptionDuration(0.1);
+ module.addRecordBackListener((data) => {
+ const completed = data.currentPosition >= this.options.maxDuration;
+
+ if (completed) this.stop();
+ if (this.state === 'recording') {
+ this.recordingSubscribers.forEach((callback) => {
+ callback({ currentTime: data.currentPosition, completed });
+ });
+ }
+ });
+ }
+
+ private setState = (state: RecorderServiceInterface['state']) => {
+ this.state = state;
+ this.stateSubscribers.forEach((callback) => {
+ callback(state);
+ });
+ };
+
+ public requestPermission = async (): Promise => {
+ const permission: Permission[] | undefined = Platform.select({
+ android: [permissionModule.PERMISSIONS.ANDROID.RECORD_AUDIO],
+ ios: [permissionModule.PERMISSIONS.IOS.MICROPHONE],
+ windows: [permissionModule.PERMISSIONS.WINDOWS.MICROPHONE],
+ default: undefined,
+ });
+
+ if (Platform.OS === 'android' && Platform.Version <= 28) {
+ permission?.push(permissionModule.PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE);
+ }
+
+ if (permission) {
+ const status = await permissionModule.checkMultiple(permission);
+ if (nativePermissionGranted(status)) {
+ return true;
+ } else {
+ const status = await permissionModule.requestMultiple(permission);
+ return nativePermissionGranted(status);
+ }
+ } else {
+ return true;
+ }
+ };
+
+ public addRecordingListener = (callback: RecordingListener): Unsubscribe => {
+ this.recordingSubscribers.add(callback);
+ return () => {
+ this.recordingSubscribers.delete(callback);
+ };
+ };
+
+ public addStateListener = (callback: StateListener): Unsubscribe => {
+ this.stateSubscribers.add(callback);
+ return () => {
+ this.stateSubscribers.delete(callback);
+ };
+ };
+
+ public record = async (uri: string): Promise => {
+ if (matchesOneOf(this.state, ['idle', 'completed'])) {
+ try {
+ this.setState('preparing');
+ await module.startRecorder(uri, {
+ ...this.audioOptions,
+ });
+
+ if (Platform.OS === 'android') {
+ this._recordStartedAt = Date.now();
+ }
+
+ this.uri = uri;
+ this.setState('recording');
+ } catch (e) {
+ this.setState('idle');
+ throw e;
+ }
+ }
+ };
+
+ public stop = async (): Promise => {
+ if (matchesOneOf(this.state, ['recording'])) {
+ if (Platform.OS === 'android') {
+ const buffer = this._getRecorderStopSafeBuffer();
+ if (buffer > 0) await sleep(buffer);
+ }
+
+ await module.stopRecorder();
+ this.setState('completed');
+ }
+ };
+
+ public reset = async (): Promise => {
+ await this.stop();
+ this.uri = undefined;
+ this.recordingSubscribers.clear();
+ this.setState('idle');
+ };
+ }
+
+ return new VoiceRecorder();
+};
+
+export default createNativeRecorderService;
diff --git a/packages/uikit-react-native/src/platform/types.ts b/packages/uikit-react-native/src/platform/types.ts
index 91fdb68d5..d27589194 100644
--- a/packages/uikit-react-native/src/platform/types.ts
+++ b/packages/uikit-react-native/src/platform/types.ts
@@ -60,6 +60,7 @@ export interface FileSystemServiceInterface {
// - Supports opening documents in place
// - Application supports iTunes file sharing
save(options?: SaveOptions): Promise;
+ createRecordFilePath(customExtension?: string): { recordFilePath: string; uri: string };
}
// ---------- MediaService ---------- //
@@ -105,3 +106,113 @@ export interface MediaServiceInterface {
getVideoThumbnail(options: GetVideoThumbnailOptions): GetVideoThumbnailResult;
compressImage(options: CompressImageOptions): CompressImageResult;
}
+
+// ---------- PlayerService ---------- //
+export interface PlayerServiceInterface {
+ uri?: string;
+ state: 'idle' | 'preparing' | 'playing' | 'paused' | 'stopped';
+
+ /**
+ * Check and request permission for the player.
+ * */
+ requestPermission(): Promise;
+
+ /**
+ * Add a playback listener.
+ * */
+ addPlaybackListener(
+ callback: (params: { currentTime: number; duration: number; stopped: boolean }) => void,
+ ): Unsubscribe;
+
+ /**
+ * Add a state listener.
+ * */
+ addStateListener(callback: (state: PlayerServiceInterface['state']) => void): Unsubscribe;
+
+ /**
+ * State transition:
+ * [idle, stopped] to [playing] - start from the beginning
+ * [paused] to [playing] - resume
+ * */
+ play(uri: string): Promise;
+
+ /**
+ * State transition:
+ * [playing] to [paused]
+ * */
+ pause(): Promise;
+
+ /**
+ * State transition:
+ * [preparing, playing, paused] to [stop]
+ * */
+ stop(): Promise;
+
+ /**
+ * State transition:
+ * [*] to [idle]
+ * */
+ reset(): Promise;
+
+ /**
+ * Seek time, only available when the state is [playing, paused].
+ * */
+ seek(time: number): Promise;
+}
+
+// ---------- RecorderService ---------- //
+export interface RecorderOptions {
+ /**
+ * Minimum recording duration (milliseconds).
+ * */
+ minDuration: number;
+
+ /**
+ * Maximum recording duration (milliseconds).
+ * */
+ maxDuration: number;
+
+ /**
+ * File extension for recorded audio file
+ * */
+ extension: string;
+}
+
+export interface RecorderServiceInterface {
+ uri?: string;
+ options: RecorderOptions;
+ state: 'idle' | 'preparing' | 'recording' | 'completed';
+
+ /**
+ * Check and request permission for the recorder.
+ * */
+ requestPermission(): Promise;
+
+ /**
+ * Add recording listener.
+ * */
+ addRecordingListener(callback: (params: { currentTime: number; completed: boolean }) => void): Unsubscribe;
+
+ /**
+ * Add state listener.
+ * */
+ addStateListener(callback: (state: RecorderServiceInterface['state']) => void): Unsubscribe;
+
+ /**
+ * State transition:
+ * [idle, completed] to [recording]
+ * */
+ record(uri?: string): Promise;
+
+ /**
+ * State transition:
+ * [recording] to [completed]
+ * */
+ stop(): Promise;
+
+ /**
+ * State transition:
+ * [*] to [idle]
+ * */
+ reset(): Promise;
+}
diff --git a/packages/uikit-testing-tools/package.json b/packages/uikit-testing-tools/package.json
index 3ab89d36e..de6a38180 100644
--- a/packages/uikit-testing-tools/package.json
+++ b/packages/uikit-testing-tools/package.json
@@ -20,6 +20,7 @@
"sideEffects": false,
"scripts": {
"test": "jest",
+ "build": "bob build",
"clean": "del lib"
},
"repository": {
diff --git a/packages/uikit-testing-tools/src/mocks/createMockChannel.ts b/packages/uikit-testing-tools/src/mocks/createMockChannel.ts
index cb6369555..5d6425249 100644
--- a/packages/uikit-testing-tools/src/mocks/createMockChannel.ts
+++ b/packages/uikit-testing-tools/src/mocks/createMockChannel.ts
@@ -30,6 +30,7 @@ import type {
PinnedMessageListQuery,
PinnedMessageListQueryParams,
} from '@sendbird/chat/lib/__definition';
+import { FileCompat } from '@sendbird/chat/lib/__definition';
import {
MessageChangelogs,
MessageRequestHandler,
@@ -49,6 +50,7 @@ import type {
SendbirdGroupChannel,
SendbirdMember,
SendbirdMessageCollection,
+ SendbirdMultipleFilesMessage,
SendbirdOpenChannel,
SendbirdRestrictedUser,
SendbirdUserMessage,
@@ -123,14 +125,9 @@ class MockChannel implements GetMockProps => {
+ refresh = jest.fn(async (): Promise => {
this.params.sdk?.__throwIfFailureTest();
-
- if (this.isGroupChannel()) {
- return this.asGroupChannel() as never;
- } else {
- return this.asOpenChannel() as never;
- }
+ return this;
});
enter = jest.fn(async () => {
@@ -352,6 +349,23 @@ class MockChannel implements GetMockProps {
throw new Error('Method not implemented.');
}
+ resendMessage(
+ failedMessage: SendbirdMultipleFilesMessage,
+ ): MultipleFilesMessageRequestHandler;
+ resendMessage(failedMessage: SendbirdFileMessage, file?: FileCompat): MessageRequestHandler;
+ resendMessage(failedMessage: SendbirdUserMessage): MessageRequestHandler;
+ resendMessage(): unknown {
+ throw new Error('Method not implemented.');
+ }
+ copyMessage(
+ channel: SendbirdGroupChannel,
+ message: SendbirdMultipleFilesMessage,
+ ): MessageRequestHandler;
+ copyMessage(channel: SendbirdBaseChannel, message: SendbirdFileMessage): MessageRequestHandler;
+ copyMessage(channel: SendbirdBaseChannel, message: SendbirdUserMessage): MessageRequestHandler;
+ copyMessage(): unknown {
+ throw new Error('Method not implemented.');
+ }
sendUserMessage(): MessageRequestHandler {
throw new Error('Method not implemented.');
}
diff --git a/packages/uikit-testing-tools/src/mocks/createMockMessage.ts b/packages/uikit-testing-tools/src/mocks/createMockMessage.ts
index 43f4aa3f2..806b34d67 100644
--- a/packages/uikit-testing-tools/src/mocks/createMockMessage.ts
+++ b/packages/uikit-testing-tools/src/mocks/createMockMessage.ts
@@ -1,4 +1,5 @@
import { ChannelType } from '@sendbird/chat';
+import { NotificationData } from '@sendbird/chat/feedChannel';
import { MessageType, SendingStatus } from '@sendbird/chat/message';
import type {
SendbirdAdminMessage,
@@ -65,6 +66,7 @@ class MockMessage implements GetMockProps {
appleCriticalAlertOptions = null;
scheduledInfo = null;
extendedMessage = {};
+ notificationData: NotificationData | null = null;
isFileMessage(): this is SendbirdFileMessage {
return this.messageType === MessageType.FILE && !Object.prototype.hasOwnProperty.call(this, 'fileInfoList');
diff --git a/packages/uikit-utils/package.json b/packages/uikit-utils/package.json
index c55c8dbbc..b04f9f9ce 100644
--- a/packages/uikit-utils/package.json
+++ b/packages/uikit-utils/package.json
@@ -25,7 +25,8 @@
"test": "jest",
"build": "bob build",
"clean": "del lib",
- "publish:next": "npm publish --tag next"
+ "publish:next": "npm publish --tag next",
+ "publish:local": "yalc publish"
},
"repository": {
"type": "git",
diff --git a/packages/uikit-utils/src/hooks/react-native.ts b/packages/uikit-utils/src/hooks/react-native.ts
index 5675f6944..e9fa8f7d0 100644
--- a/packages/uikit-utils/src/hooks/react-native.ts
+++ b/packages/uikit-utils/src/hooks/react-native.ts
@@ -1,4 +1,4 @@
-import { useEffect, useRef } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { AppState, AppStateEvent, AppStateStatus } from 'react-native';
import { EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -44,3 +44,26 @@ export const useAppState = (type: AppStateEvent, listener: AppStateListener) =>
};
}, []);
};
+
+/**
+ * To display a new modal in React-Native, you should ensure that a new modal is opened only after the existing modal has been dismissed to avoid conflicts.
+ * To achieve this, you can use a deferred onClose that can be awaited until the onDismiss is called.
+ * */
+export const useDeferredModalState = () => {
+ const resolveRef = useRef<(value: void) => void>();
+ const [visible, setVisible] = useState(false);
+
+ return {
+ onClose: () => {
+ return new Promise((resolve) => {
+ resolveRef.current = resolve;
+ setVisible(false);
+ });
+ },
+ onDismiss: () => {
+ resolveRef.current?.();
+ },
+ visible,
+ setVisible,
+ };
+};
diff --git a/packages/uikit-utils/src/sendbird/message.ts b/packages/uikit-utils/src/sendbird/message.ts
index 65401a651..97b98b318 100644
--- a/packages/uikit-utils/src/sendbird/message.ts
+++ b/packages/uikit-utils/src/sendbird/message.ts
@@ -1,6 +1,6 @@
import { MessageSearchOrder } from '@sendbird/chat/message';
-import { getFileExtension, getFileType } from '../shared/file';
+import { getFileExtension, getFileType, parseMimeType } from '../shared/file';
import type {
SendbirdBaseChannel,
SendbirdBaseMessage,
@@ -161,7 +161,7 @@ export type MessageType =
| 'file'
| 'unknown'
| `user.${'opengraph'}`
- | `file.${'image' | 'video' | 'audio'}`;
+ | `file.${'image' | 'video' | 'audio' | 'voice'}`;
export type FileType = 'file' | 'image' | 'audio' | 'video';
@@ -221,6 +221,10 @@ export function getMessageType(message: SendbirdMessage): MessageType {
}
if (message.isFileMessage()) {
+ if (isVoiceMessage(message)) {
+ return 'file.voice';
+ }
+
const fileType = getFileTypeFromMessage(message);
switch (fileType) {
case 'image':
@@ -247,3 +251,26 @@ export function getDefaultMessageSearchQueryParams(channel: SendbirdGroupChannel
order: MessageSearchOrder.TIMESTAMP,
};
}
+
+const SBU_MIME_PARAM_KEY = 'sbu_type';
+const SBU_MIME_PARAM_VOICE_MESSAGE_VALUE = 'voice';
+
+export function isVoiceMessage(message: SendbirdMessage): message is SendbirdFileMessage {
+ if (!message.isFileMessage()) return false;
+
+ const { parameters } = parseMimeType(message.type);
+ return !!parameters[SBU_MIME_PARAM_KEY] && parameters[SBU_MIME_PARAM_KEY] === SBU_MIME_PARAM_VOICE_MESSAGE_VALUE;
+}
+
+export function getVoiceMessageFileObject(uri: string, extension = 'm4a') {
+ return {
+ uri,
+ type: getVoiceMessageMimeType(extension),
+ name: `Voice_message.${extension}`,
+ size: 0,
+ };
+}
+
+export function getVoiceMessageMimeType(extension = 'm4a') {
+ return `audio/${extension};${SBU_MIME_PARAM_KEY}=${SBU_MIME_PARAM_VOICE_MESSAGE_VALUE}`;
+}
diff --git a/packages/uikit-utils/src/shared/file.ts b/packages/uikit-utils/src/shared/file.ts
index 0daa8c112..cd9ba7ca4 100644
--- a/packages/uikit-utils/src/shared/file.ts
+++ b/packages/uikit-utils/src/shared/file.ts
@@ -18,6 +18,7 @@ const EXTENSION_MIME_MAP = {
// Audio
'aac': 'audio/aac',
+ 'm4a': 'audio/m4a',
'mid': 'audio/midi',
'mp3': 'audio/mpeg',
'ogg': 'audio/ogg',
diff --git a/packages/uikit-utils/src/shared/index.ts b/packages/uikit-utils/src/shared/index.ts
index 8dcb54e4f..4f340a343 100644
--- a/packages/uikit-utils/src/shared/index.ts
+++ b/packages/uikit-utils/src/shared/index.ts
@@ -51,3 +51,11 @@ export function mergeObjectArrays(A: T[], B: T[], key: keyof T): T[] {
return newArr;
}
+
+export function sleep(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export function matchesOneOf(value: V, candidates: C) {
+ return candidates.includes(value);
+}
diff --git a/packages/uikit-utils/src/ui-format/common.ts b/packages/uikit-utils/src/ui-format/common.ts
index 5e8183d67..ff34e9ace 100644
--- a/packages/uikit-utils/src/ui-format/common.ts
+++ b/packages/uikit-utils/src/ui-format/common.ts
@@ -125,3 +125,30 @@ export const getMessagePreviewTime = (timestamp: number, locale?: Locale) => {
return format(timestamp, 'yyyy/MM/dd', { locale });
};
+
+/**
+ * milliseconds to 00:00 format
+ * */
+export const millsToMMSS = (mills: number) => {
+ let seconds = Math.floor(Math.max(0, mills) / 1000);
+ const minutes = Math.floor(seconds / 60);
+ seconds = seconds % 60;
+
+ const mm = String(minutes).padStart(2, '0');
+ const ss = String(seconds).padStart(2, '0');
+
+ return `${mm}:${ss}`;
+};
+
+/**
+ * milliseconds to 0:00 format
+ * */
+export const millsToMSS = (mills: number) => {
+ let seconds = Math.floor(Math.max(0, mills) / 1000);
+ const minutes = Math.floor(seconds / 60);
+ seconds = seconds % 60;
+
+ const ss = String(seconds).padStart(2, '0');
+
+ return `${minutes}:${ss}`;
+};
diff --git a/sample/android/app/src/main/AndroidManifest.xml b/sample/android/app/src/main/AndroidManifest.xml
index a88ebdde9..bdfbd0386 100644
--- a/sample/android/app/src/main/AndroidManifest.xml
+++ b/sample/android/app/src/main/AndroidManifest.xml
@@ -9,6 +9,8 @@
+
+
diff --git a/sample/ios/Podfile.lock b/sample/ios/Podfile.lock
index 425cdda9e..96d2201b0 100644
--- a/sample/ios/Podfile.lock
+++ b/sample/ios/Podfile.lock
@@ -435,6 +435,8 @@ PODS:
- React-jsi (= 0.67.5)
- React-logger (= 0.67.5)
- React-perflogger (= 0.67.5)
+ - RNAudioRecorderPlayer (3.5.4):
+ - React-Core
- RNCAsyncStorage (1.18.1):
- React-Core
- RNCClipboard (1.11.2):
@@ -545,6 +547,7 @@ DEPENDENCIES:
- React-RCTVibration (from `../../node_modules/react-native/Libraries/Vibration`)
- React-runtimeexecutor (from `../../node_modules/react-native/ReactCommon/runtimeexecutor`)
- ReactCommon/turbomodule/core (from `../../node_modules/react-native/ReactCommon`)
+ - RNAudioRecorderPlayer (from `../../node_modules/react-native-audio-recorder-player`)
- "RNCAsyncStorage (from `../../node_modules/@react-native-async-storage/async-storage`)"
- "RNCClipboard (from `../../node_modules/@react-native-clipboard/clipboard`)"
- "RNCPushNotificationIOS (from `../../node_modules/@react-native-community/push-notification-ios`)"
@@ -677,6 +680,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native/ReactCommon/runtimeexecutor"
ReactCommon:
:path: "../../node_modules/react-native/ReactCommon"
+ RNAudioRecorderPlayer:
+ :path: "../../node_modules/react-native-audio-recorder-player"
RNCAsyncStorage:
:path: "../../node_modules/@react-native-async-storage/async-storage"
RNCClipboard:
@@ -770,6 +775,7 @@ SPEC CHECKSUMS:
React-RCTVibration: c3b8a3245267a3849b0c7cb91a37606bf5f3aa65
React-runtimeexecutor: 434efc9e5b6d0f14f49867f130b39376c971c1aa
ReactCommon: a30c2448e5a88bae6fcb0e3da124c14ae493dac1
+ RNAudioRecorderPlayer: 1ff1ddabbe89031df12ba9d5ac2fa724697c5c48
RNCAsyncStorage: b90b71f45b8b97be43bc4284e71a6af48ac9f547
RNCClipboard: 3f0451a8100393908bea5c5c5b16f96d45f30bfc
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
diff --git a/sample/package.json b/sample/package.json
index 8190bf48e..826755ebb 100644
--- a/sample/package.json
+++ b/sample/package.json
@@ -8,7 +8,7 @@
"start": "react-native start",
"test": "jest",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
- "adb:reverse": "adb start-server && adb reverse tcp:8081 tcp:8081",
+ "adb:reverse": "adb start-server && sh ./scripts/adb-reverse.sh",
"get-stories": "sb-rn-get-stories",
"storybook-watcher": "sb-rn-watcher"
},
@@ -40,6 +40,7 @@
"fbjs": "^3.0.4",
"react": "17.0.2",
"react-native": "0.67.5",
+ "react-native-audio-recorder-player": "^3.6.0",
"react-native-create-thumbnail": "^1.5.1",
"react-native-document-picker": "^8.0.0",
"react-native-fast-image": "^8.5.11",
diff --git a/sample/scripts/adb-reverse.sh b/sample/scripts/adb-reverse.sh
new file mode 100644
index 000000000..a92dd07a5
--- /dev/null
+++ b/sample/scripts/adb-reverse.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+devices=$(adb devices | awk '$2 == "device" {print $1}')
+
+for device_id in $devices
+do
+ adb -s $device_id reverse tcp:8081 tcp:8081
+done
diff --git a/sample/src/App.tsx b/sample/src/App.tsx
index e4bcaf446..e617fe5a1 100644
--- a/sample/src/App.tsx
+++ b/sample/src/App.tsx
@@ -9,15 +9,7 @@ import { DarkUIKitTheme, LightUIKitTheme } from '@sendbird/uikit-react-native-fo
// import LogView from './components/LogView';
import { APP_ID } from './env';
-import {
- ClipboardService,
- FileService,
- GetTranslucent,
- MediaService,
- NotificationService,
- RootStack,
- SetSendbirdSDK,
-} from './factory';
+import { GetTranslucent, RootStack, SetSendbirdSDK, platformServices } from './factory';
import useAppearance from './hooks/useAppearance';
import { Routes, navigationActions, navigationRef } from './libs/navigation';
import { notificationHandler } from './libs/notification';
@@ -82,12 +74,7 @@ const App = () => {
onInitialized: SetSendbirdSDK,
enableAutoPushTokenRegistration: true,
}}
- platformServices={{
- file: FileService,
- notification: NotificationService,
- clipboard: ClipboardService,
- media: MediaService,
- }}
+ platformServices={platformServices}
styles={{
defaultHeaderTitleAlign: 'left', //'center',
theme: isLightTheme ? LightUIKitTheme : DarkUIKitTheme,
@@ -208,7 +195,7 @@ const Navigations = () => {
*
* @example How to customize UIKit global navigation
* ```
- * const UseReactNavigationHeader: HeaderStyleContextType['HeaderComponent'] = ({
+ * const ReactNavigationHeader: HeaderStyleContextType['HeaderComponent'] = ({
* title,
* right,
* left,
@@ -233,9 +220,7 @@ const Navigations = () => {
* return (
*
*
*
@@ -245,4 +230,39 @@ const Navigations = () => {
* ```
* */
+/**
+ * @example How to implement custom local cache storage
+ * ```
+ * import { MMKV } from 'react-native-mmkv';
+ * import { LocalCacheStorage } from '@sendbird/uikit-react-native';
+ *
+ * const mmkvStorage = new MMKV();
+ * const localCacheStorage: LocalCacheStorage = {
+ * async getAllKeys() {
+ * return mmkvStorage.getAllKeys();
+ * },
+ * async setItem(key: string, value: string) {
+ * return mmkvStorage.set(key, value);
+ * },
+ * async getItem(key: string) {
+ * return mmkvStorage.getString(key) ?? null;
+ * },
+ * async removeItem(key: string) {
+ * return mmkvStorage.delete(key);
+ * },
+ * };
+ *
+ * const App = () => {
+ * return (
+ *
+ *
+ *
+ * );
+ * };
+ * ```
+ * */
+
export default App;
diff --git a/sample/src/factory/index.ts b/sample/src/factory/index.ts
index 9ef462885..b44f1dc24 100644
--- a/sample/src/factory/index.ts
+++ b/sample/src/factory/index.ts
@@ -4,6 +4,7 @@ import Clipboard from '@react-native-clipboard/clipboard';
import RNFBMessaging from '@react-native-firebase/messaging';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Platform, StatusBar } from 'react-native';
+import * as AudioRecorderPlayer from 'react-native-audio-recorder-player';
import * as CreateThumbnail from 'react-native-create-thumbnail';
import * as DocumentPicker from 'react-native-document-picker';
import * as FileAccess from 'react-native-file-access';
@@ -12,10 +13,13 @@ import * as Permissions from 'react-native-permissions';
import Video from 'react-native-video';
import {
+ SendbirdUIKitContainerProps,
createNativeClipboardService,
createNativeFileService,
createNativeMediaService,
createNativeNotificationService,
+ createNativePlayerService,
+ createNativeRecorderService,
} from '@sendbird/uikit-react-native';
import { Logger, SendbirdChatSDK } from '@sendbird/uikit-utils';
@@ -26,23 +30,33 @@ export const GetSendbirdSDK = () => AppSendbirdSDK;
export const SetSendbirdSDK = (sdk: SendbirdChatSDK) => (AppSendbirdSDK = sdk);
export const RootStack = createNativeStackNavigator();
-export const ClipboardService = createNativeClipboardService(Clipboard);
-export const NotificationService = createNativeNotificationService({
- messagingModule: RNFBMessaging,
- permissionModule: Permissions,
-});
-export const FileService = createNativeFileService({
- imagePickerModule: ImagePicker,
- documentPickerModule: DocumentPicker,
- permissionModule: Permissions,
- fsModule: FileAccess,
- mediaLibraryModule: CameraRoll,
-});
-export const MediaService = createNativeMediaService({
- VideoComponent: Video,
- thumbnailModule: CreateThumbnail,
- imageResizerModule: ImageResizer,
-});
+export const platformServices: SendbirdUIKitContainerProps['platformServices'] = {
+ clipboard: createNativeClipboardService(Clipboard),
+ notification: createNativeNotificationService({
+ messagingModule: RNFBMessaging,
+ permissionModule: Permissions,
+ }),
+ file: createNativeFileService({
+ imagePickerModule: ImagePicker,
+ documentPickerModule: DocumentPicker,
+ permissionModule: Permissions,
+ fsModule: FileAccess,
+ mediaLibraryModule: CameraRoll,
+ }),
+ media: createNativeMediaService({
+ VideoComponent: Video,
+ thumbnailModule: CreateThumbnail,
+ imageResizerModule: ImageResizer,
+ }),
+ player: createNativePlayerService({
+ audioRecorderModule: AudioRecorderPlayer,
+ permissionModule: Permissions,
+ }),
+ recorder: createNativeRecorderService({
+ audioRecorderModule: AudioRecorderPlayer,
+ permissionModule: Permissions,
+ }),
+};
export const GetTranslucent = (state = true) => {
Platform.OS === 'android' && StatusBar.setTranslucent(state);
diff --git a/sample/src/screens/uikit/SettingsScreen.tsx b/sample/src/screens/uikit/SettingsScreen.tsx
index 2f1a319a5..d344c9d83 100644
--- a/sample/src/screens/uikit/SettingsScreen.tsx
+++ b/sample/src/screens/uikit/SettingsScreen.tsx
@@ -80,7 +80,7 @@ const SettingsScreen = () => {
if (!photo) return;
- await updateCurrentUserInfo(sdk.currentUser.nickname, photo);
+ await updateCurrentUserInfo(sdk.currentUser?.nickname, photo);
},
},
{
@@ -106,7 +106,7 @@ const SettingsScreen = () => {
});
if (!files || !files[0]) return;
- await updateCurrentUserInfo(sdk.currentUser.nickname, files[0]);
+ await updateCurrentUserInfo(sdk.currentUser?.nickname, files[0]);
},
},
],
diff --git a/yarn.lock b/yarn.lock
index b9fcec25e..ce9f4cca9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3372,10 +3372,10 @@
dependencies:
nanoid "^3.1.23"
-"@sendbird/chat@4.9.8", "@sendbird/chat@^4.9.8":
- version "4.9.8"
- resolved "https://registry.yarnpkg.com/@sendbird/chat/-/chat-4.9.8.tgz#c70378fc1da684ea0c532b2e55bd8b1f3939838c"
- integrity sha512-tRTr/5iXl4Fhxon6T8yO2xaRxvyO2KctFjqdEHVLW9JuhWEAWiWZsY/5Lbxivi/Pt+ebiQuBhl/af3V1WOJDCg==
+"@sendbird/chat@4.9.10", "@sendbird/chat@^4.9.8":
+ version "4.9.10"
+ resolved "https://registry.yarnpkg.com/@sendbird/chat/-/chat-4.9.10.tgz#b92a3f5d4ec895acd5c2ebbadf4fa10aa0ebda10"
+ integrity sha512-WR2DoFu190/8Z6UhSjeNNQE6vIhrfBgwH5hdL0Jb4mVQNm8bSBd9jbwlDzvBAZCAqHxvXwNU+rH2VlKvaw84wQ==
"@sendbird/react-native-scrollview-enhancer@^0.2.1":
version "0.2.1"
@@ -6801,6 +6801,11 @@ domexception@^2.0.1:
dependencies:
webidl-conversions "^5.0.0"
+dooboolab-welcome@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/dooboolab-welcome/-/dooboolab-welcome-1.3.2.tgz#4928595312f0429b4ea1b485ba8767bae6acdab7"
+ integrity sha512-2NbMaIIURElxEf/UAoVUFlXrO+7n/FRhLCiQlk4fkbGRh9cJ3/f8VEMPveR9m4Ug2l2Zey+UCXjd6EcBqHJ5bw==
+
dot-prop@^5.1.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@@ -8067,7 +8072,7 @@ fs-extra@^11.1.0:
jsonfile "^6.0.1"
universalify "^2.0.0"
-fs-extra@^8.1.0:
+fs-extra@^8.0.1, fs-extra@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
@@ -8720,6 +8725,13 @@ iferr@^0.1.5:
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
integrity sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==
+ignore-walk@^3.0.3:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335"
+ integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==
+ dependencies:
+ minimatch "^3.0.4"
+
ignore-walk@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-5.0.1.tgz#5f199e23e1288f518d90358d461387788a154776"
@@ -8814,6 +8826,11 @@ ini@^1.3.2, ini@^1.3.4:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
+ini@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
+ integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
+
init-package-json@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-3.0.2.tgz#f5bc9bac93f2bdc005778bc2271be642fecfcd69"
@@ -11733,6 +11750,16 @@ npm-package-arg@^9.0.0, npm-package-arg@^9.0.1:
semver "^7.3.5"
validate-npm-package-name "^4.0.0"
+npm-packlist@^2.1.5:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-2.2.2.tgz#076b97293fa620f632833186a7a8f65aaa6148c8"
+ integrity sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg==
+ dependencies:
+ glob "^7.1.6"
+ ignore-walk "^3.0.3"
+ npm-bundled "^1.1.1"
+ npm-normalize-package-bin "^1.0.1"
+
npm-packlist@^5.1.0, npm-packlist@^5.1.1:
version "5.1.3"
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-5.1.3.tgz#69d253e6fd664b9058b85005905012e00e69274b"
@@ -12911,6 +12938,13 @@ react-is@^17.0.1, react-is@^17.0.2:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+react-native-audio-recorder-player@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/react-native-audio-recorder-player/-/react-native-audio-recorder-player-3.6.0.tgz#3e64113197c6e0e988338ac31b3edf058521a928"
+ integrity sha512-eSg9kqihUACXWaqiUUxrjKfgkppmi49PAvl7GEq2hPogwuDAs/eeimT9mPYFKpp5UHCxDIdmMcG7pcMGRynz5w==
+ dependencies:
+ dooboolab-welcome "^1.3.2"
+
react-native-builder-bob@^0.18.0, react-native-builder-bob@^0.18.2:
version "0.18.3"
resolved "https://registry.yarnpkg.com/react-native-builder-bob/-/react-native-builder-bob-0.18.3.tgz#fb4d3e50a3b2290db3c88de6d40403ac7eb9f85f"
@@ -15562,6 +15596,20 @@ y18n@^5.0.5:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+yalc@^1.0.0-pre.53:
+ version "1.0.0-pre.53"
+ resolved "https://registry.yarnpkg.com/yalc/-/yalc-1.0.0-pre.53.tgz#c51db2bb924a6908f4cb7e82af78f7e5606810bc"
+ integrity sha512-tpNqBCpTXplnduzw5XC+FF8zNJ9L/UXmvQyyQj7NKrDNavbJtHvzmZplL5ES/RCnjX7JR7W9wz5GVDXVP3dHUQ==
+ dependencies:
+ chalk "^4.1.0"
+ detect-indent "^6.0.0"
+ fs-extra "^8.0.1"
+ glob "^7.1.4"
+ ignore "^5.0.4"
+ ini "^2.0.0"
+ npm-packlist "^2.1.5"
+ yargs "^16.1.1"
+
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
@@ -15617,7 +15665,7 @@ yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.1:
y18n "^4.0.0"
yargs-parser "^18.1.2"
-yargs@^16.2.0:
+yargs@^16.1.1, yargs@^16.2.0:
version "16.2.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==