diff --git a/.github/actions/install-and-build-sdk/action.yml b/.github/actions/install-and-build-sdk/action.yml
index 5ba82e4bc2..f79f913aa5 100644
--- a/.github/actions/install-and-build-sdk/action.yml
+++ b/.github/actions/install-and-build-sdk/action.yml
@@ -16,11 +16,6 @@ runs:
cd package/native-package/
yarn
shell: bash
- - name: Install & Build the Expo Package
- run: |
- cd package/expo-package/
- yarn
- shell: bash
- name: Install & Build the Sample App
working-directory: examples/SampleApp
run: yarn
diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml
index 4e5656143d..98113381d5 100644
--- a/.github/workflows/check-pr.yml
+++ b/.github/workflows/check-pr.yml
@@ -10,7 +10,7 @@ on:
jobs:
check_pr:
- runs-on: ubuntu-latest
+ runs-on: public
strategy:
matrix:
node-version: [24.x]
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7e6f1a7720..d782fa42e0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -4,16 +4,16 @@ on:
push:
branches:
- main
- - develop
+# - develop
permissions:
- id-token: write # for OIDC / npm provenance if you use it
- actions: write # if you dispatch other workflows
- contents: write # commits / tags / merge-back
+ id-token: write # for OIDC / npm provenance if you use it
+ actions: write # if you dispatch other workflows
+ contents: write # commits / tags / merge-back
jobs:
publish:
- runs-on: ubuntu-latest
+ runs-on: public
strategy:
matrix:
node-version: [24.x]
@@ -21,7 +21,7 @@ jobs:
steps:
- uses: actions/checkout@v2
with:
- fetch-depth: "0"
+ fetch-depth: '0'
- name: Fetch tags
run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
@@ -30,7 +30,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- registry-url: "https://registry.npmjs.org"
+ registry-url: 'https://registry.npmjs.org'
- name: Prepare git
run: |
diff --git a/.github/workflows/sample-distribution.yml b/.github/workflows/sample-distribution.yml
index 981c8bb2c6..5280534365 100644
--- a/.github/workflows/sample-distribution.yml
+++ b/.github/workflows/sample-distribution.yml
@@ -17,7 +17,7 @@ jobs:
runs-on: [macos-15]
strategy:
matrix:
- node-version: [ 24.x ]
+ node-version: [24.x]
steps:
- name: Connect Bot
uses: webfactory/ssh-agent@v0.7.0
@@ -30,7 +30,7 @@ jobs:
- uses: actions/checkout@v3
- uses: maxim-lobanov/setup-xcode@v1
with:
- xcode-version: '16.4.0' # Update as needed
+ xcode-version: '26.2' # Update as needed
- uses: ./.github/actions/ruby-cache
- name: Install && Build - SDK and Sample App
uses: ./.github/actions/install-and-build-sdk
@@ -51,18 +51,18 @@ jobs:
bundle exec pod install
- name: Build and release Testflight QA
working-directory: examples/SampleApp
- run: bundle exec fastlane deploy_to_testflight_qa deploy:${{ github.ref == 'refs/heads/develop' }};
+ run: bundle exec fastlane ios deploy_to_testflight_qa deploy:${{ github.ref == 'refs/heads/develop' }};
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }}
- build_and_deploy_android_s3:
+ build_and_deploy_android_firebase:
name: Build SampleApp Android and Deploy-${{ github.ref == 'refs/heads/develop' }}
- runs-on: ubuntu-latest
+ runs-on: public
strategy:
matrix:
- node-version: [ 24.x ]
+ node-version: [24.x]
steps:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
@@ -74,26 +74,15 @@ jobs:
distribution: 'zulu'
java-version: '17'
check-latest: true
+ - name: Setup Android SDK
+ uses: amyu/setup-android@v5
+
+ - uses: ./.github/actions/ruby-cache
- name: Install && Build - SDK and Sample App
uses: ./.github/actions/install-and-build-sdk
- - name: Build
+ - name: Build and deploy Android Firebase
working-directory: examples/SampleApp
- run: |
- mkdir android/app/src/main/assets
- mkdir tmp
- yarn react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest tmp
- cd android
- rm -rf $HOME/.gradle/caches/ && ./gradlew assembleRelease
- - name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@v1
- if: ${{ github.ref == 'refs/heads/develop' }}
- with:
- aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
- aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- aws-region: us-east-1
- - name: Upload APK
- if: ${{ github.ref == 'refs/heads/develop' }}
- # https://getstream.io/downloads/rn-sample-app.apk
- run: |
- cp examples/SampleApp/android/app/build/outputs/apk/release/app-release.apk rn-sample-app.apk
- aws s3 cp rn-sample-app.apk s3://${{ secrets.AWS_S3_BUCKET }} --sse AES256
+ run: bundle exec fastlane android firebase_build_and_upload deploy:${{ github.ref == 'refs/heads/develop' }};
+ env:
+ ANDROID_FIREBASE_APP_ID: ${{ secrets.ANDROID_FIREBASE_APP_ID }}
+ FIREBASE_CREDENTIALS_JSON: ${{ secrets.FIREBASE_CREDENTIALS_JSON }}
diff --git a/README.md b/README.md
index a93c62eca5..973ffc78bb 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
[](https://www.npmjs.com/package/stream-chat-react-native)
[](https://github.com/GetStream/stream-chat-react-native/actions)
[](https://getstream.io/chat/docs/sdk/reactnative)
-
+
diff --git a/examples/SampleApp/.gitignore b/examples/SampleApp/.gitignore
index 1f3e68a22e..7388cefebf 100644
--- a/examples/SampleApp/.gitignore
+++ b/examples/SampleApp/.gitignore
@@ -73,3 +73,7 @@ yarn-error.log
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+
+# Credentials
+credentials/
+app-build/
diff --git a/examples/SampleApp/App.tsx b/examples/SampleApp/App.tsx
index 771d8db352..60f518b389 100644
--- a/examples/SampleApp/App.tsx
+++ b/examples/SampleApp/App.tsx
@@ -60,6 +60,7 @@ import { Toast } from './src/components/ToastComponent/Toast';
import { useClientNotificationsToastHandler } from './src/hooks/useClientNotificationsToastHandler';
import AsyncStore from './src/utils/AsyncStore.ts';
import {
+ MessageInputFloatingConfigItem,
MessageListImplementationConfigItem,
MessageListModeConfigItem,
MessageListPruningConfigItem,
@@ -94,6 +95,7 @@ notifee.onBackgroundEvent(async ({ detail, type }) => {
const Drawer = createDrawerNavigator();
const Stack = createNativeStackNavigator();
const UserSelectorStack = createNativeStackNavigator();
+
const App = () => {
const { chatClient, isConnecting, loginUser, logout, switchUser } = useChatClient();
const [messageListImplementation, setMessageListImplementation] = useState<
@@ -105,8 +107,12 @@ const App = () => {
const [messageListPruning, setMessageListPruning] = useState<
MessageListPruningConfigItem['value'] | undefined
>(undefined);
+ const [messageInputFloating, setMessageInputFloating] = useState<
+ MessageInputFloatingConfigItem['value'] | undefined
+ >(undefined);
const colorScheme = useColorScheme();
const streamChatTheme = useStreamChatTheme();
+ const streami18n = new Streami18n();
useEffect(() => {
const messaging = getMessaging();
@@ -159,6 +165,10 @@ const App = () => {
'@stream-rn-sampleapp-messagelist-pruning',
{ value: undefined },
);
+ const messageInputFloatingStoredValue = await AsyncStore.getItem(
+ '@stream-rn-sampleapp-messageinput-floating',
+ { value: false },
+ );
setMessageListImplementation(
messageListImplementationStoredValue?.id as MessageListImplementationConfigItem['id'],
);
@@ -166,6 +176,9 @@ const App = () => {
setMessageListPruning(
messageListPruningStoredValue?.value as MessageListPruningConfigItem['value'],
);
+ setMessageInputFloating(
+ messageInputFloatingStoredValue?.value as MessageInputFloatingConfigItem['value'],
+ );
};
getMessageListConfig();
return () => {
@@ -183,6 +196,9 @@ const App = () => {
drafts: {
enabled: true,
},
+ linkPreviews: {
+ enabled: true,
+ }
});
setupCommandUIMiddlewares(composer);
@@ -209,39 +225,44 @@ const App = () => {
backgroundColor: streamChatTheme.colors?.white_snow || '#FCFCFC',
}}
>
-
-
-
- {isConnecting && !chatClient ? (
-
- ) : chatClient ? (
-
- ) : (
-
- )}
-
-
-
+
+
+
+
+
+ {isConnecting && !chatClient ? (
+
+ ) : chatClient ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
);
};
@@ -265,32 +286,26 @@ const isMessageAIGenerated = (message: LocalMessage) => !!message.ai_generated;
const DrawerNavigatorWrapper: React.FC<{
chatClient: StreamChat;
-}> = ({ chatClient }) => {
- const streamChatTheme = useStreamChatTheme();
- const streami18n = new Streami18n();
-
+ i18nInstance: Streami18n;
+}> = ({ chatClient, i18nInstance }) => {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/examples/SampleApp/Gemfile.lock b/examples/SampleApp/Gemfile.lock
index 83479ab205..e16cd2e14a 100644
--- a/examples/SampleApp/Gemfile.lock
+++ b/examples/SampleApp/Gemfile.lock
@@ -1,48 +1,56 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (3.0.7)
+ CFPropertyList (3.0.8)
+ abbrev (0.1.2)
+ activesupport (7.2.3)
base64
- nkf
- rexml
- activesupport (7.0.8.4)
- concurrent-ruby (~> 1.0, >= 1.0.2)
+ benchmark (>= 0.3)
+ bigdecimal
+ concurrent-ruby (~> 1.0, >= 1.3.1)
+ connection_pool (>= 2.2.5)
+ drb
i18n (>= 1.6, < 2)
+ logger (>= 1.4.2)
minitest (>= 5.1)
- tzinfo (~> 2.0)
- addressable (2.8.7)
- public_suffix (>= 2.0.2, < 7.0)
+ securerandom (>= 0.3)
+ tzinfo (~> 2.0, >= 2.0.5)
+ addressable (2.8.8)
+ public_suffix (>= 2.0.2, < 8.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.17)
- ast (2.4.2)
+ ast (2.4.3)
atomos (0.1.3)
- aws-eventstream (1.3.0)
- aws-partitions (1.966.0)
- aws-sdk-core (3.201.5)
+ aws-eventstream (1.4.0)
+ aws-partitions (1.1209.0)
+ aws-sdk-core (3.241.4)
aws-eventstream (~> 1, >= 1.3.0)
- aws-partitions (~> 1, >= 1.651.0)
+ aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
+ base64
+ bigdecimal
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.88.0)
- aws-sdk-core (~> 3, >= 3.201.0)
+ logger
+ aws-sdk-kms (1.121.0)
+ aws-sdk-core (~> 3, >= 3.241.4)
aws-sigv4 (~> 1.5)
- aws-sdk-s3 (1.158.0)
- aws-sdk-core (~> 3, >= 3.201.0)
+ aws-sdk-s3 (1.212.0)
+ aws-sdk-core (~> 3, >= 3.241.4)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
- aws-sigv4 (1.9.1)
+ aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
- benchmark (0.3.0)
- bigdecimal (3.1.5)
+ benchmark (0.5.0)
+ bigdecimal (4.0.1)
claide (1.1.0)
- cocoapods (1.14.3)
+ cocoapods (1.15.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
- cocoapods-core (= 1.14.3)
+ cocoapods-core (= 1.15.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
@@ -57,7 +65,7 @@ GEM
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.23.0, < 2.0)
- cocoapods-core (1.14.3)
+ cocoapods-core (1.15.2)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
@@ -80,18 +88,22 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
- concurrent-ruby (1.2.3)
+ concurrent-ruby (1.3.3)
+ connection_pool (3.0.2)
+ csv (3.3.5)
declarative (0.0.20)
- digest-crc (0.6.5)
+ digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
+ drb (2.2.3)
emoji_regex (3.2.3)
escape (0.0.4)
- ethon (0.16.0)
+ ethon (0.18.0)
ffi (>= 1.15.0)
- excon (0.111.0)
- faraday (1.10.3)
+ logger
+ excon (0.112.0)
+ faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -103,32 +115,36 @@ GEM
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
- faraday-cookie_jar (0.0.7)
+ faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
- http-cookie (~> 1.0.0)
+ http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
- faraday-em_synchrony (1.0.0)
+ faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
- faraday-multipart (1.0.4)
- multipart-post (~> 2)
+ faraday-multipart (1.2.0)
+ multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
- faraday_middleware (1.2.0)
+ faraday_middleware (1.2.1)
faraday (~> 1.0)
- fastimage (2.3.1)
- fastlane (2.222.0)
+ fastimage (2.4.0)
+ fastlane (2.231.1)
CFPropertyList (>= 2.3, < 4.0.0)
+ abbrev (~> 0.1.2)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
- bundler (>= 1.12.0, < 3.0.0)
+ base64 (~> 0.2.0)
+ benchmark (>= 0.1.0)
+ bundler (>= 1.17.3, < 5.0.0)
colored (~> 1.2)
commander (~> 4.6)
+ csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
@@ -136,6 +152,7 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
+ fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
@@ -145,10 +162,14 @@ GEM
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
+ logger (>= 1.6, < 2.0)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
+ mutex_m (~> 0.3.0)
naturally (~> 2.2)
+ nkf (~> 0.2.0)
optparse (>= 0.1.1, < 1.0.0)
+ ostruct (>= 0.1.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
@@ -159,15 +180,17 @@ GEM
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
- xcpretty (~> 0.3.0)
+ xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
- fastlane-plugin-firebase_app_distribution (0.9.1)
+ fastlane-plugin-firebase_app_distribution (0.10.1)
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
fastlane-plugin-load_json (0.0.1)
fastlane-plugin-stream_actions (0.3.73)
xctest_list (= 1.2.1)
- ffi (1.17.0)
+ fastlane-sirp (1.0.0)
+ sysrandom (~> 1.0)
+ ffi (1.17.3)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
@@ -191,12 +214,12 @@ GEM
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
- google-cloud-core (1.7.1)
+ google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
- google-cloud-errors (1.4.0)
+ google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
@@ -212,81 +235,88 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
- http-cookie (1.0.7)
+ http-cookie (1.0.8)
domain_name (~> 0.5)
- httpclient (2.8.3)
- i18n (1.14.5)
+ httpclient (2.9.0)
+ mutex_m
+ i18n (1.14.8)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
- json (2.7.2)
- jwt (2.8.2)
+ json (2.18.0)
+ jwt (2.10.2)
base64
- language_server-protocol (3.17.0.3)
- logger (1.6.0)
+ language_server-protocol (3.17.0.5)
+ lint_roller (1.1.0)
+ logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
- minitest (5.25.1)
+ minitest (6.0.1)
+ prism (~> 1.5)
molinillo (0.8.0)
- multi_json (1.15.0)
+ multi_json (1.19.1)
multipart-post (2.4.1)
- mutex_m (0.2.0)
+ mutex_m (0.3.0)
nanaimo (0.3.0)
nap (1.1.0)
- naturally (2.2.1)
+ naturally (2.3.0)
netrc (0.11.0)
nkf (0.2.0)
- optparse (0.5.0)
+ optparse (0.8.1)
os (1.1.4)
- parallel (1.26.3)
- parser (3.3.4.2)
+ ostruct (0.6.3)
+ parallel (1.27.0)
+ parser (3.3.10.1)
ast (~> 2.4.1)
racc
- plist (3.7.1)
+ plist (3.7.2)
+ prism (1.8.0)
public_suffix (4.0.7)
racc (1.8.1)
rainbow (3.1.1)
- rake (13.2.1)
- regexp_parser (2.9.2)
+ rake (13.3.1)
+ regexp_parser (2.11.3)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
- rexml (3.3.5)
- strscan
- rouge (2.0.7)
- rubocop (1.65.1)
+ rexml (3.4.4)
+ rouge (3.28.0)
+ rubocop (1.84.0)
json (~> 2.3)
- language_server-protocol (>= 3.17.0)
+ language_server-protocol (~> 3.17.0.2)
+ lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
- regexp_parser (>= 2.4, < 3.0)
- rexml (>= 3.2.5, < 4.0)
- rubocop-ast (>= 1.31.1, < 2.0)
+ regexp_parser (>= 2.9.3, < 3.0)
+ rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
- unicode-display_width (>= 2.4.0, < 3.0)
- rubocop-ast (1.32.1)
- parser (>= 3.3.1.0)
- rubocop-performance (1.21.1)
- rubocop (>= 1.48.1, < 2.0)
- rubocop-ast (>= 1.31.1, < 2.0)
+ unicode-display_width (>= 2.4.0, < 4.0)
+ rubocop-ast (1.49.0)
+ parser (>= 3.3.7.2)
+ prism (~> 1.7)
+ rubocop-performance (1.26.1)
+ lint_roller (~> 1.1)
+ rubocop (>= 1.75.0, < 2.0)
+ rubocop-ast (>= 1.47.1, < 2.0)
rubocop-require_tools (0.1.2)
rubocop (>= 0.49.1)
ruby-macho (2.5.1)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
- rubyzip (2.3.2)
+ rubyzip (2.4.1)
+ securerandom (0.4.1)
security (0.1.5)
- signet (0.19.0)
+ signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
- jwt (>= 1.5, < 3.0)
+ jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
- strscan (3.1.0)
+ sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
@@ -300,17 +330,17 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0)
- unicode-display_width (2.5.0)
+ unicode-display_width (2.6.0)
word_wrap (1.0.0)
- xcodeproj (1.25.0)
+ xcodeproj (1.25.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
- rexml (>= 3.3.2, < 4.0)
- xcpretty (0.3.0)
- rouge (~> 2.0.7)
+ rexml (>= 3.3.6, < 4.0)
+ xcpretty (0.4.1)
+ rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
xctest_list (1.2.1)
diff --git a/examples/SampleApp/android/app/build.gradle b/examples/SampleApp/android/app/build.gradle
index e1c59595a5..39b1680d7a 100644
--- a/examples/SampleApp/android/app/build.gradle
+++ b/examples/SampleApp/android/app/build.gradle
@@ -79,15 +79,15 @@ android {
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
- namespace "com.sampleapp"
+ namespace "io.getstream.reactnative.sampleapp"
defaultConfig {
- applicationId "com.sampleapp"
+ applicationId "io.getstream.reactnative.sampleapp"
minSdkVersion rootProject.ext.minSdkVersion
multiDexEnabled true
targetSdkVersion rootProject.ext.targetSdkVersion
vectorDrawables.useSupportLibrary = true
- versionCode 22
+ versionCode 1
versionName "0.0.22"
}
diff --git a/examples/SampleApp/android/app/google-services.json b/examples/SampleApp/android/app/google-services.json
index 1d5c701c81..ec09246afb 100644
--- a/examples/SampleApp/android/app/google-services.json
+++ b/examples/SampleApp/android/app/google-services.json
@@ -62,7 +62,7 @@
"client_info": {
"mobilesdk_app_id": "1:674907137625:android:5effa1cd0fef9003d7f348",
"android_client_info": {
- "package_name": "com.sampleapp"
+ "package_name": "io.getstream.reactnative.sampleapp"
}
},
"oauth_client": [
@@ -98,7 +98,7 @@
"client_info": {
"mobilesdk_app_id": "1:674907137625:android:07c76802bbfd5654d7f348",
"android_client_info": {
- "package_name": "com.sampleapp.rnpushtest"
+ "package_name": "io.getstream.reactnative.sampleapp.rnpushtest"
}
},
"oauth_client": [
diff --git a/examples/SampleApp/android/app/src/androidTest/java/com/sampleapp/DetoxTest.java b/examples/SampleApp/android/app/src/androidTest/java/com/sampleapp/DetoxTest.java
index df226139a6..9557ec76a9 100644
--- a/examples/SampleApp/android/app/src/androidTest/java/com/sampleapp/DetoxTest.java
+++ b/examples/SampleApp/android/app/src/androidTest/java/com/sampleapp/DetoxTest.java
@@ -1,4 +1,4 @@
-package com.sampleapp;
+package io.getstream.reactnative.sampleapp;
import com.wix.detox.Detox;
import com.wix.detox.config.DetoxConfig;
@@ -24,7 +24,7 @@ public void runDetoxTests() {
DetoxConfig detoxConfig = new DetoxConfig();
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
- detoxConfig.rnContextLoadTimeoutSec = (com.sampleapp.BuildConfig.DEBUG ? 180 : 60);
+ detoxConfig.rnContextLoadTimeoutSec = (io.getstream.reactnative.sampleapp.BuildConfig.DEBUG ? 180 : 60);
Detox.runTests(mActivityRule, detoxConfig);
}
diff --git a/examples/SampleApp/android/app/src/main/java/com/sampleapp/MainActivity.kt b/examples/SampleApp/android/app/src/main/java/com/sampleapp/MainActivity.kt
index f3ca98b78b..c811c58912 100644
--- a/examples/SampleApp/android/app/src/main/java/com/sampleapp/MainActivity.kt
+++ b/examples/SampleApp/android/app/src/main/java/com/sampleapp/MainActivity.kt
@@ -1,52 +1,49 @@
-package com.sampleapp
+package io.getstream.reactnative.sampleapp
-import com.facebook.react.ReactActivity
-import com.facebook.react.ReactActivityDelegate
-import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
-import com.facebook.react.defaults.DefaultReactActivityDelegate
-
-import android.os.Bundle
import android.os.Build
+import android.os.Bundle
import android.view.View
+import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
+import com.facebook.react.ReactActivity
+import com.facebook.react.ReactActivityDelegate
+import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
+import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(null)
-
- if (Build.VERSION.SDK_INT >= 35) {
- val rootView = findViewById(android.R.id.content)
-
-
- ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, insets ->
- val bars = insets.getInsets(
- WindowInsetsCompat.Type.systemBars()
- or WindowInsetsCompat.Type.displayCutout()
- or WindowInsetsCompat.Type.ime() // adding the ime's height
- )
- rootView.updatePadding(
- left = bars.left,
- top = bars.top,
- right = bars.right,
- bottom = bars.bottom
- )
- WindowInsetsCompat.CONSUMED
- }
- }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(null)
+
+ if (Build.VERSION.SDK_INT >= 35) {
+ val rootView = findViewById(android.R.id.content)
+
+ val initial = Insets.of(
+ rootView.paddingLeft,
+ rootView.paddingTop,
+ rootView.paddingRight,
+ rootView.paddingBottom
+ )
+
+ ViewCompat.setOnApplyWindowInsetsListener(rootView) { v, insets ->
+ val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
+
+ v.updatePadding(
+ left = initial.left,
+ top = initial.top,
+ right = initial.right,
+ bottom = initial.bottom + ime.bottom
+ )
+
+ insets
}
- /**
- * Returns the name of the main component registered from JavaScript. This is used to schedule
- * rendering of the component.
- */
- override fun getMainComponentName(): String = "SampleApp"
-
- /**
- * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
- * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
- */
- override fun createReactActivityDelegate(): ReactActivityDelegate =
- DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
+ }
+ }
+
+ override fun getMainComponentName(): String = "SampleApp"
+
+ override fun createReactActivityDelegate(): ReactActivityDelegate =
+ DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
}
diff --git a/examples/SampleApp/android/app/src/main/java/com/sampleapp/MainApplication.kt b/examples/SampleApp/android/app/src/main/java/com/sampleapp/MainApplication.kt
index 69b92b8443..84c3f9a65e 100644
--- a/examples/SampleApp/android/app/src/main/java/com/sampleapp/MainApplication.kt
+++ b/examples/SampleApp/android/app/src/main/java/com/sampleapp/MainApplication.kt
@@ -1,4 +1,4 @@
-package com.sampleapp
+package io.getstream.reactnative.sampleapp
import android.app.Application
import com.facebook.react.PackageList
diff --git a/examples/SampleApp/fastlane/Fastfile b/examples/SampleApp/fastlane/Fastfile
index bf10e28713..0efb03543f 100644
--- a/examples/SampleApp/fastlane/Fastfile
+++ b/examples/SampleApp/fastlane/Fastfile
@@ -1,13 +1,23 @@
-default_platform(:ios)
skip_docs
+# Common Configuration
github_repo = ENV['GITHUB_REPOSITORY'] || 'GetStream/stream-chat-react-native'
-bundle_id = 'io.getstream.reactnative.SampleApp'
-xcode_project = 'ios/SampleApp.xcodeproj'
-xcode_workspace = 'ios/SampleApp.xcworkspace'
root_path = File.absolute_path('../../../')
sdk_size_ext = 'KB'
@force_check = false
+build_output_directory = "./app-build"
+
+# iOS Platform Configuration
+bundle_id = 'io.getstream.reactnative.SampleApp'
+xcode_project = 'ios/SampleApp.xcodeproj'
+xcode_workspace = 'ios/SampleApp.xcworkspace'
+output_ipa_name = "reactnativesampleapp.ipa"
+
+# Android Platform Configuration
+package_name = 'io.getstream.reactnative.sampleapp'
+output_apk_name = "reactnativesampleapp.apk"
+apk_path = "#{build_output_directory}/#{output_apk_name}"
+
before_all do
if is_ci
@@ -20,82 +30,140 @@ end
###### iOS lanes ######
#######################
-lane :deploy_to_testflight_qa do |options|
- match_me
+platform :ios do
+ private_lane :latest_appstore_version_code do |options|
+ livestates = [true, false]
+ version_codes = []
+ livestates.each do |livestate|
+ vc = app_store_build_number(
+ live: livestate,
+ app_identifier: bundle_id
+ )
+ rescue StandardError
+ puts("No app store build found for liveState: #{livestate} bundle_id: #{bundle_id}")
+ else
+ version_codes.append(vc)
+ end
+ version_codes.max
+ end
- settings_to_override = {
- BUNDLE_IDENTIFIER: bundle_id,
- PROVISIONING_PROFILE_SPECIFIER: "match AppStore #{bundle_id}"
- }
+ lane :deploy_to_testflight_qa do |options|
+ match_me
- increment_version_number(
- version_number: load_json(json_path: './package.json')['version'],
- xcodeproj: xcode_project
- )
+ deploy = options.fetch(:deploy, false)
- current_build_number = app_store_build_number(
- api_key: appstore_api_key,
- live: false,
- app_identifier: bundle_id
- )
+ UI.message("Deploying to Testflight: #{deploy}")
- increment_build_number(
- build_number: current_build_number + 1,
- xcodeproj: xcode_project
- )
+ if deploy
+ increment_version_number(
+ version_number: load_json(json_path: './package.json')['version'],
+ xcodeproj: xcode_project
+ )
- gym(
- workspace: xcode_workspace,
- scheme: 'SampleApp',
- export_method: 'app-store',
- export_options: './fastlane/testflight_gym_export_options.plist',
- silent: true,
- clean: true,
- xcargs: settings_to_override,
- include_symbols: true,
- output_directory: './dist'
- )
+ current_build_number = latest_appstore_version_code
+
+ puts("Current build number: #{current_build_number}")
+
+ increment_build_number(
+ build_number: current_build_number + 1,
+ xcodeproj: xcode_project
+ )
+ end
+
+ settings_to_override = {
+ BUNDLE_IDENTIFIER: bundle_id,
+ PROVISIONING_PROFILE_SPECIFIER: "match AppStore #{bundle_id}"
+ }
+
+ gym(
+ workspace: xcode_workspace,
+ scheme: 'SampleApp',
+ export_method: 'app-store',
+ export_options: './fastlane/testflight_gym_export_options.plist',
+ silent: true,
+ clean: true,
+ xcargs: settings_to_override,
+ include_symbols: true,
+ output_directory: build_output_directory,
+ output_name: File.basename(output_ipa_name, '.ipa')
+ )
- if options[:deploy]
- begin
+ if deploy
upload_to_testflight(
+ api_key: appstore_api_key,
groups: ['Testers'],
changelog: 'Lots of amazing new features to test out!',
- reject_build_waiting_for_review: false
+ reject_build_waiting_for_review: true,
+ ipa: "#{build_output_directory}/#{output_ipa_name}",
+ skip_waiting_for_build_processing: true
)
- rescue StandardError => e
- if e.message.include?('Another build is in review')
- UI.important('Another build is already in beta review. Skipping beta review submission')
- else
- UI.user_error!(e)
- end
+ else
+ UI.message("Skipping Testflight upload! (deploy: #{deploy})")
end
end
-end
-private_lane :appstore_api_key do
- @appstore_api_key ||= app_store_connect_api_key(
- key_id: 'MT3PRT8TB7',
- issuer_id: '69a6de96-0738-47e3-e053-5b8c7c11a4d1',
- key_content: ENV.fetch('APPSTORE_API_KEY', nil),
- in_house: false
- )
-end
+ private_lane :appstore_api_key do
+ @appstore_api_key ||= app_store_connect_api_key(
+ key_id: 'MT3PRT8TB7',
+ issuer_id: '69a6de96-0738-47e3-e053-5b8c7c11a4d1',
+ key_content: ENV.fetch('APPSTORE_API_KEY', nil),
+ in_house: false
+ )
+ end
-desc "If `readonly: true` (by default), installs all Certs and Profiles necessary for development and ad-hoc.\nIf `readonly: false`, recreates all Profiles necessary for development and ad-hoc, updates them locally and remotely."
-lane :match_me do |options|
- custom_match(
- api_key: appstore_api_key,
- app_identifier: [bundle_id],
- readonly: options[:readonly],
- register_device: options[:register_device]
- )
+ desc "If `readonly: true` (by default), installs all Certs and Profiles necessary for development and ad-hoc.\nIf `readonly: false`, recreates all Profiles necessary for development and ad-hoc, updates them locally and remotely."
+ lane :match_me do |options|
+ custom_match(
+ api_key: appstore_api_key,
+ app_identifier: [bundle_id],
+ readonly: options[:readonly],
+ register_device: options[:register_device]
+ )
+ end
end
###########################
###### Android lanes ######
###########################
+platform :android do
+ lane :firebase_build_and_upload do |options|
+ deploy = options.fetch(:deploy, false)
+
+ UI.message("Deploying to Firebase: #{deploy}")
+
+ # Clean
+ gradle(
+ task: "clean",
+ project_dir: "./android"
+ )
+
+ # Build the AAB
+ gradle(
+ task: "assemble",
+ build_type: "Release",
+ project_dir: "./android"
+ )
+
+ Dir.chdir('..') do
+ sh("mkdir -p #{build_output_directory} && mv -f #{lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]} #{apk_path}")
+ end
+
+ if deploy
+ # Upload to Firebase App Distribution
+ firebase_app_distribution(
+ app: ENV.fetch('ANDROID_FIREBASE_APP_ID', nil),
+ service_credentials_json_data: ENV.fetch('FIREBASE_CREDENTIALS_JSON', nil),
+ android_artifact_path: apk_path,
+ android_artifact_type: "APK",
+ groups: "stream-testers"
+ )
+ else
+ UI.message("Skipping Firebase upload! (deploy: #{deploy})")
+ end
+ end
+end
+
##########################
###### Common lanes ######
##########################
diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock
index 171a4084a1..61fc07eb9c 100644
--- a/examples/SampleApp/ios/Podfile.lock
+++ b/examples/SampleApp/ios/Podfile.lock
@@ -2734,7 +2734,7 @@ PODS:
- FirebaseCoreExtension
- React-Core
- RNFBApp
- - RNGestureHandler (2.26.0):
+ - RNGestureHandler (2.30.0):
- boost
- DoubleConversion
- fast_float
@@ -2798,7 +2798,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - RNReanimated (4.0.1):
+ - RNReanimated (4.2.1):
- boost
- DoubleConversion
- fast_float
@@ -2825,11 +2825,11 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNReanimated/reanimated (= 4.0.1)
+ - RNReanimated/reanimated (= 4.2.1)
- RNWorklets
- SocketRocket
- Yoga
- - RNReanimated/reanimated (4.0.1):
+ - RNReanimated/reanimated (4.2.1):
- boost
- DoubleConversion
- fast_float
@@ -2856,11 +2856,11 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNReanimated/reanimated/apple (= 4.0.1)
+ - RNReanimated/reanimated/apple (= 4.2.1)
- RNWorklets
- SocketRocket
- Yoga
- - RNReanimated/reanimated/apple (4.0.1):
+ - RNReanimated/reanimated/apple (4.2.1):
- boost
- DoubleConversion
- fast_float
@@ -3039,7 +3039,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - RNWorklets (0.4.1):
+ - RNWorklets (0.7.2):
- boost
- DoubleConversion
- fast_float
@@ -3066,10 +3066,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNWorklets/worklets (= 0.4.1)
+ - RNWorklets/worklets (= 0.7.2)
- SocketRocket
- Yoga
- - RNWorklets/worklets (0.4.1):
+ - RNWorklets/worklets (0.7.2):
- boost
- DoubleConversion
- fast_float
@@ -3096,10 +3096,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNWorklets/worklets/apple (= 0.4.1)
+ - RNWorklets/worklets/apple (= 0.7.2)
- SocketRocket
- Yoga
- - RNWorklets/worklets/apple (0.4.1):
+ - RNWorklets/worklets/apple (0.7.2):
- boost
- DoubleConversion
- fast_float
@@ -3165,6 +3165,65 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
+ - Teleport (0.5.4):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Teleport/common (= 0.5.4)
+ - Yoga
+ - Teleport/common (0.5.4):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Yoga
- Yoga (0.0.0)
DEPENDENCIES:
@@ -3269,6 +3328,7 @@ DEPENDENCIES:
- RNWorklets (from `../node_modules/react-native-worklets`)
- SocketRocket (~> 0.7.1)
- stream-chat-react-native (from `../node_modules/stream-chat-react-native`)
+ - Teleport (from `../node_modules/react-native-teleport`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
@@ -3490,6 +3550,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-worklets"
stream-chat-react-native:
:path: "../node_modules/stream-chat-react-native"
+ Teleport:
+ :path: "../node_modules/react-native-teleport"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@@ -3517,9 +3579,9 @@ SPEC CHECKSUMS:
hermes-engine: bbc1152da7d2d40f9e59c28acc6576fcf5d28e2a
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
- NitroModules: 8849240f6ee6d3b295514112437e3b09e855cb67
- NitroSound: fe46960c89410e62e05e9a709d8bf28a8202d1b3
- op-sqlite: b9a4028bef60145d7b98fbbc4341c83420cdcfd5
+ NitroModules: 01ae20fc1e8fc9a3b088ab8ed06ab92527a04f0d
+ NitroSound: 347b6a21f2e7d5601c92ef81cec7836f8f8be44c
+ op-sqlite: a7e46cfdaebeef219fd0e939332967af9fe6d406
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
@@ -3529,91 +3591,92 @@ SPEC CHECKSUMS:
React: e7a4655b09d0e17e54be188cc34c2f3e2087318a
React-callinvoker: 62192daaa2f30c3321fc531e4f776f7b09cf892b
React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a
- React-Core: c400b068fdb6172177f3b3fae00c10d1077244d7
- React-CoreModules: 8e911a5a504b45824374eec240a78de7a6db8ca2
- React-cxxreact: 06a91f55ac5f842219d6ca47e0f77187a5b5f4ac
+ React-Core: b23cdaaa9d76389d958c06af3c57aa6ad611c542
+ React-CoreModules: 8e0f562e5695991e455abbebe1e968af71d52553
+ React-cxxreact: 6ccbe0cc2c652b29409b14b23cfb3cd74e084691
React-debug: ab52e07f7148480ea61c5e9b68408d749c6e2b8f
- React-defaultsnativemodule: fab7bf1b5ce1f3ed252aa4e949ec48a8df67d624
- React-domnativemodule: 735fa5238cceebceeecc18f9f4321016461178cf
- React-Fabric: c75719fc8818049c3cf9071f0619af988b020c9f
- React-FabricComponents: 74a381cc0dfaf2ec3ee29f39ef8533a7fd864b83
- React-FabricImage: 9a3ff143b1ac42e077c0b33ec790f3674ace5783
- React-featureflags: e1eca0727563a61c919131d57bbd0019c3bdb0f0
- React-featureflagsnativemodule: 692211fd48283b2ddee3817767021010e2f1788e
- React-graphics: 759b02bde621f12426c1dc1ae2498aaa6b441cd7
- React-hermes: b6e33fcd21aa7523dc76e62acd7a547e68c28a5b
- React-idlecallbacksnativemodule: 731552efc0815fc9d65a6931da55e722b1b3a397
- React-ImageManager: 2c510a480f2c358f56a82df823c66d5700949c96
- React-jserrorhandler: 411e18cbdcbdf546f8f8707091faeb00703527c1
- React-jsi: 3fde19aaf675c0607a0824c4d6002a4943820fd9
- React-jsiexecutor: 4f898228240cf261a02568e985dfa7e1d7ad1dfb
- React-jsinspector: 2f0751e6a4fb840f4ed325384d0795a9e9afaf39
- React-jsinspectorcdp: 71c48944d97f5f20e8e144e419ddf04ffa931e93
- React-jsinspectornetwork: 102f347669b278644cc9bb4ebf2f90422bd5ccef
- React-jsinspectortracing: 0f6f2ec7f3faa9dc73d591b24b460141612515eb
- React-jsitooling: b557f8e12efdaf16997e43b0d07dbd8a3fce3a5b
- React-jsitracing: f9a77561d99c0cd053a8230bab4829b100903949
- React-logger: ea80169d826e0cd112fa4d68f58b2b3b968f1ecb
- React-Mapbuffer: 230c34b1cabd1c4815726c711b9df221c3d3fbfb
- React-microtasksnativemodule: 29d62f132e4aba34ebb7f2b936dde754eb08971b
- react-native-blob-util: cbd6b292d0f558f09dce85e6afe68074cd031f3e
- react-native-cameraroll: 00057cc0ec595fdbdf282ecfb931d484b240565f
- react-native-document-picker: c5fa18e9fc47b34cfbab3b0a4447d0df918a5621
- react-native-geolocation: eb39c815c9b58ddc3efb552cafdd4b035e4cf682
- react-native-image-picker: e479ec8884df9d99a62c1f53f2307055ad43ea85
- react-native-maps: ee1e65647460c3d41e778071be5eda10e3da6225
- react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac
- react-native-safe-area-context: 7fd4c2c8023da8e18eaa3424cb49d52f626debee
- react-native-video: 71973843c2c9ac154c54f95a5a408fd8b041790e
- React-NativeModulesApple: d061f458c3febdf0ac99b1b0faf23b7305974b25
+ React-defaultsnativemodule: 291d2b0a93c399056121f4f0acc7f46d155a38ec
+ React-domnativemodule: c4968302e857bd422df8eec50a3cd4d078bd4ac0
+ React-Fabric: 7e3ba48433b87a416052c4077d5965aff64cb8c9
+ React-FabricComponents: 21de255cf52232644d12d3288cced1f0c519b25d
+ React-FabricImage: 15a0961a0ab34178f1c803aa0a7d28f21322ffc3
+ React-featureflags: 4e5dad365d57e3c3656447dfdad790f75878d9f4
+ React-featureflagsnativemodule: 5eac59389131c2b87d165dac4094b5e86067fabb
+ React-graphics: 2f9b3db89f156afd793da99f23782f400f58c1ee
+ React-hermes: cc8c77acee1406c258622cd8abbee9049f6b5761
+ React-idlecallbacksnativemodule: 1d7e1f73b624926d16db956e87c4885ef485664a
+ React-ImageManager: 8b6066f6638fba7d4a94fbd0b39b477ea8aced58
+ React-jserrorhandler: e5a4626d65b0eda9a11c43a9f14d0423d8a7080d
+ React-jsi: ea5c640ea63c127080f158dac7f4f393d13d415c
+ React-jsiexecutor: cf7920f82e46fe9a484c15c9f31e67d7179aa826
+ React-jsinspector: 094e3cb99952a0024fa977fa04706e68747cba18
+ React-jsinspectorcdp: dca545979146e3ecbdc999c0789dab55beecc140
+ React-jsinspectornetwork: 0a105fe74b0b1a93f70409d955c8a0556dc17c59
+ React-jsinspectortracing: 76088dd78a2de3ea637a860cdf39a6d9c2637d6b
+ React-jsitooling: a2e1e87382aae2294bc94a6e9682b9bc83da1d36
+ React-jsitracing: 45827be59e673f4c54175c150891301138846906
+ React-logger: 7cfc7b1ae1f8e5fe5097f9c746137cc3a8fad4ce
+ React-Mapbuffer: 8f620d1794c6b59a8c3862c3ae820a2e9e6c9bb0
+ React-microtasksnativemodule: dcf5321c9a41659a6718df8a5f202af1577c6825
+ react-native-blob-util: a511afccff6511544ebf56928e6afdf837b037a7
+ react-native-cameraroll: 8c3ba9b6f511cf645778de19d5039b61d922fdfb
+ react-native-document-picker: b37cf6660ad9087b782faa78a1e67687fac15bfd
+ react-native-geolocation: b7f68b8c04e36ee669c630dbc48dd42cf93a0a41
+ react-native-image-picker: 9bceb747cd6cde22a3416ffdc819d11b5b11f156
+ react-native-maps: 9febd31278b35cd21e4fad2cf6fa708993be5dab
+ react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
+ react-native-safe-area-context: 32293dc61d1b92ccf892499ab6f8acfd609f9aef
+ react-native-video: 4da16bfca01a02aa2095e40683d74f2d6563207c
+ React-NativeModulesApple: 342e280bb9fc2aa5f61f6c257b309a86b995e12d
React-oscompat: 56d6de59f9ae95cd006a1c40be2cde83bc06a4e1
- React-perflogger: 0633844e495d8b34798c9bf0cb32ce315f1d5c9f
- React-performancetimeline: 53bdf62ff49a9b0c4bd4d66329fdcf28d77c1c9d
+ React-perflogger: 4008bd05a8b6c157b06608c0ea0b8bd5d9c5e6c9
+ React-performancetimeline: 3ac316a346fe3d48801a746b754dd8f5b5146838
React-RCTActionSheet: 49138012280ec3bbb35193d8d09adb8bc61c982e
- React-RCTAnimation: c7ed4a9d5a4e43c9b10f68bb43cd238c4a2e7e89
- React-RCTAppDelegate: ea2ab6f4aef1489f72025b7128d8ab645b40eafb
- React-RCTBlob: c052799460b245e1fffe3d1dddea36fa88e998a0
- React-RCTFabric: fad6230640c42fb8cda29b1d0759f7a1fb8cc677
- React-RCTFBReactNativeSpec: ffb22c3ee3d359ae9245ca94af203845da9371ec
- React-RCTImage: 59fc2571f4f109a77139924f5babee8f9cd639c9
- React-RCTLinking: a045cb58c08188dce6c6f4621de105114b1b16ce
- React-RCTNetwork: fc7115a2f5e15ae0aa05e9a9be726817feefb482
- React-RCTRuntime: c69b86dc60dcc7297318097fc60bd8e40b050f74
- React-RCTSettings: 30d7dd7eae66290467a1e72bf42d927fa78c3884
- React-RCTText: 755d59284e66c7d33bb4f0ccc428fe69110c3e74
- React-RCTVibration: ffe019e588815df226f6f8ccdc65979f8b2bc440
+ React-RCTAnimation: ebfe7c62016d4c17b56b2cab3a221908ae46288d
+ React-RCTAppDelegate: 0108657ba9a19f6a1cd62dcd19c2c0485b3fc251
+ React-RCTBlob: 6cc309d1623f3c2679125a04a7425685b7219e6b
+ React-RCTFabric: 04d1cf11ee3747a699260492e319e92649d7ac88
+ React-RCTFBReactNativeSpec: ff3e37e2456afc04211334e86d07bf20488df0ae
+ React-RCTImage: bb98a59aeed953a48be3f917b9b745b213b340ab
+ React-RCTLinking: d6e9795d4d75d154c1dd821fd0746cc3e05d6670
+ React-RCTNetwork: 5c8a7a2dd26728323189362f149e788548ac72bc
+ React-RCTRuntime: 52b28e281aba881e1f94ee8b16611823b730d1c5
+ React-RCTSettings: b6a02d545ce10dd936b39914b32674db6e865307
+ React-RCTText: c7d9232da0e9b5082a99a617483d9164a9cd46e9
+ React-RCTVibration: fe636c985c1bf25e4a5b5b4d9315a3b882468a72
React-rendererconsistency: aba18fa58a4d037004f6bed6bb201eb368016c56
- React-renderercss: c7c140782f5f21103b638abfde7b3f11d6a5fd7e
- React-rendererdebug: 111519052db9610f1b93baf7350c800621df3d0c
+ React-renderercss: b490bd53486a6bae1e9809619735d1f2b2cabd7f
+ React-rendererdebug: 8db25b276b64d5a1dbf05677de0c4ff1039d5184
React-rncore: 22f344c7f9109b68c3872194b0b5081ca1aee655
- React-RuntimeApple: 30d20d804a216eb297ccc9ce1dc9e931738c03b1
- React-RuntimeCore: 6e1facd50e0b7aed1bc36b090015f33133958bb6
+ React-RuntimeApple: 19bdabedda0eeb70acb71e68bfc42d61bbcbacd9
+ React-RuntimeCore: 11bf03bdbd6e72857481c46d0f4eb9c19b14c754
React-runtimeexecutor: b35de9cb7f5d19c66ea9b067235f95b947697ba5
- React-RuntimeHermes: 222268a5931a23f095565c4d60e2673c04e2178a
- React-runtimescheduler: aea93219348ba3069fe6c7685a84fe17d3a4b4ee
+ React-RuntimeHermes: d8f736d0a2d38233c7ec7bd36040eb9b0a3ccd8c
+ React-runtimescheduler: 0c95966d030c8ebbebddaab49630cda2889ca073
React-timing: 42e8212c479d1e956d3024be0a07f205a2e34d9d
- React-utils: 0ebf25dd4eb1b5497933f4d8923b862d0fe9566f
- ReactAppDependencyProvider: 6c9197c1f6643633012ab646d2bfedd1b0d25989
- ReactCodegen: f8ae44cfcb65af88f409f4b094b879591232f12c
- ReactCommon: a237545f08f598b8b37dc3a9163c1c4b85817b0b
- RNCAsyncStorage: afe7c3711dc256e492aa3a50dcac43eecebd0ae5
- RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8
- RNFBApp: df5caad9f64b6bc87f8a0b110e6bc411fb00a12b
- RNFBMessaging: 6586f18ab3411aeb3349088c19fe54283d39e529
- RNGestureHandler: 4d36eb583264375d9f7ece09a2efd918ebc85605
- RNNotifee: 4a6ee5c7deaf00e005050052d73ee6315dff7ec9
- RNReactNativeHapticFeedback: 8bd4a2ba7c3daeb5d2acfceb8b61743f203076d0
- RNReanimated: 408767d090bcbfe3877cfbcc9dc9d29f5e878203
- RNScreens: 5ca475eb30f4362c5808f3ff4a1c7b786bcd878e
- RNShare: 083f05646b9534305999cf20452bd87ca0e8b0b0
- RNSVG: fd433fe5da0f7fee8c78f5865f29ab37321dbd7f
- RNWorklets: 7d34d4c80edec50bb1eec6bd034e7686db26da8e
+ React-utils: a185f723baa0c525c361e6c281a846d919623dbe
+ ReactAppDependencyProvider: 8df342c127fd0c1e30e8b9f71ff814c22414a7c0
+ ReactCodegen: 4928682e20747464165effacc170019a18da953c
+ ReactCommon: ec1cdf708729338070f8c4ad746768a782fd9eb1
+ RNCAsyncStorage: f30b3a83064e28b0fc46f1fbd3834589ed64c7b9
+ RNFastImage: 462a183c4b0b6b26fdfd639e1ed6ba37536c3b87
+ RNFBApp: db9c2e6d36fe579ab19b82c0a4a417ff7569db7e
+ RNFBMessaging: de62448d205095171915d622ed5fb45c2be5e075
+ RNGestureHandler: 0c0d36c0f3c17fc755543fad1c182e1cd541f898
+ RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168
+ RNReactNativeHapticFeedback: d39b9a5b334ce26f49ca6abe9eea8b3938532aee
+ RNReanimated: e3dd9527a9614e1c9e127018cca9486f2c13b2a9
+ RNScreens: 6a2d1ff4d263d29d3d3db9f3c19aad2f99fdd162
+ RNShare: 9d801eafd9ae835f51bcae6b5c8de9bf3389075b
+ RNSVG: bc7ccfe884848ac924d2279d9025d41b5f05cb0c
+ RNWorklets: 4bd2a43ae826633e5e0a92953fce2eb8265759d4
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
- stream-chat-react-native: 7a042480d22a8a87aaee6186bf2f1013af017d3a
+ stream-chat-react-native: f42e234640869e0eafcdd354441414ad1818b9fe
+ Teleport: c089481dd2bb020e3dced39b7f8849b93d1499f6
Yoga: ce248fb32065c9b00451491b06607f1c25b2f1ed
PODFILE CHECKSUM: 4f662370295f8f9cee909f1a4c59a614999a209d
-COCOAPODS: 1.14.3
+COCOAPODS: 1.15.2
diff --git a/examples/SampleApp/ios/SampleApp-tvOS/Info.plist b/examples/SampleApp/ios/SampleApp-tvOS/Info.plist
index a8d20ea4c7..5bbd1e0da7 100644
--- a/examples/SampleApp/ios/SampleApp-tvOS/Info.plist
+++ b/examples/SampleApp/ios/SampleApp-tvOS/Info.plist
@@ -50,4 +50,4 @@
UIViewControllerBasedStatusBarAppearance
-
\ No newline at end of file
+
diff --git a/examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj b/examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj
index fd2714a5da..51996b9be4 100644
--- a/examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj
+++ b/examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj
@@ -497,7 +497,7 @@
CODE_SIGN_ENTITLEMENTS = SampleApp/SampleAppDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 149;
+ CURRENT_PROJECT_VERSION = 926;
DEVELOPMENT_TEAM = EHV7XZLAHA;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = SampleApp/Info.plist;
@@ -534,7 +534,7 @@
CODE_SIGN_ENTITLEMENTS = SampleApp/SampleAppRelease.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 149;
+ CURRENT_PROJECT_VERSION = 926;
DEVELOPMENT_TEAM = EHV7XZLAHA;
INFOPLIST_FILE = SampleApp/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
diff --git a/examples/SampleApp/ios/SampleApp/Info.plist b/examples/SampleApp/ios/SampleApp/Info.plist
index ae8b0f4d7d..b072573cc4 100644
--- a/examples/SampleApp/ios/SampleApp/Info.plist
+++ b/examples/SampleApp/ios/SampleApp/Info.plist
@@ -59,4 +59,4 @@
UIViewControllerBasedStatusBarAppearance
-
\ No newline at end of file
+
diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json
index 5da8d63f2a..18bc840a1c 100644
--- a/examples/SampleApp/package.json
+++ b/examples/SampleApp/package.json
@@ -20,7 +20,11 @@
"release-next": "echo \"Skipping next release for SampleApp\"",
"test:unit": "echo \"Skipping unit tests for SampleApp\"",
"clean": "watchman watch-del-all && yarn cache clean && rm -rf ios/build && pod cache clean --all && rm -rf android/build && cd android && ./gradlew clean && cd -",
- "clean-all": "yarn clean && rm -rf node_modules && rm -rf ios/Pods && rm -rf vendor && bundle install && yarn install && cd ios && bundle exec pod install && cd -"
+ "clean-all": "yarn clean && rm -rf node_modules && rm -rf ios/Pods && rm -rf vendor && bundle install && yarn install && cd ios && bundle exec pod install && cd -",
+ "fastlane:android-build": "bundle exec fastlane android firebase_build_and_upload deploy:false",
+ "fastlane:android-deploy": "bundle exec fastlane android firebase_build_and_upload deploy:true",
+ "fastlane:ios-build": "bundle exec fastlane ios deploy_to_testflight_qa deploy:false",
+ "fastlane:ios-deploy": "bundle exec fastlane ios deploy_to_testflight_qa deploy:true"
},
"dependencies": {
"@emoji-mart/data": "^1.2.1",
@@ -44,19 +48,20 @@
"react-native": "^0.80.2",
"react-native-blob-util": "^0.22.2",
"react-native-fast-image": "^8.6.3",
- "react-native-gesture-handler": "^2.26.0",
+ "react-native-gesture-handler": "^2.30.0",
"react-native-haptic-feedback": "^2.3.3",
"react-native-image-picker": "^8.2.1",
"react-native-maps": "1.20.1",
"react-native-nitro-modules": "^0.31.3",
"react-native-nitro-sound": "^0.2.9",
- "react-native-reanimated": "^4.0.1",
+ "react-native-reanimated": "^4.2.0",
"react-native-safe-area-context": "^5.4.1",
"react-native-screens": "^4.11.1",
"react-native-share": "^12.0.11",
"react-native-svg": "^15.12.0",
+ "react-native-teleport": "^0.5.4",
"react-native-video": "^6.16.1",
- "react-native-worklets": "^0.4.1",
+ "react-native-worklets": "^0.7.2",
"stream-chat-react-native": "link:../../package/native-package",
"stream-chat-react-native-core": "link:../../package"
},
diff --git a/examples/SampleApp/src/components/AttachmentPickerContent.tsx b/examples/SampleApp/src/components/AttachmentPickerContent.tsx
new file mode 100644
index 0000000000..a7a56e13ef
--- /dev/null
+++ b/examples/SampleApp/src/components/AttachmentPickerContent.tsx
@@ -0,0 +1,53 @@
+import React, { useCallback, useState } from 'react';
+import {
+ useAttachmentPickerState,
+ AttachmentPickerContentProps,
+ AttachmentPickerContent,
+ AttachmentPickerGenericContent,
+ useStableCallback,
+ useTheme,
+ useTranslationContext,
+} from 'stream-chat-react-native';
+import { ShareLocationIcon } from '../icons/ShareLocationIcon.tsx';
+import { LiveLocationCreateModal } from './LocationSharing/CreateLocationModal.tsx';
+
+export const CustomAttachmentPickerContent = (props: AttachmentPickerContentProps) => {
+ const [modalVisible, setModalVisible] = useState(false);
+ const { selectedPicker } = useAttachmentPickerState();
+ const { t } = useTranslationContext();
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ const Icon = useCallback(
+ () => ,
+ [semantics.textTertiary],
+ );
+
+ const onRequestClose = () => {
+ setModalVisible(false);
+ };
+
+ const onOpenModal = useStableCallback(() => {
+ setModalVisible(true);
+ });
+
+ if (selectedPicker === 'location') {
+ return (
+ <>
+
+ {modalVisible ? (
+
+ ) : null}
+ >
+ );
+ }
+
+ return ;
+};
diff --git a/examples/SampleApp/src/components/AttachmentPickerSelectionBar.tsx b/examples/SampleApp/src/components/AttachmentPickerSelectionBar.tsx
index 0b858bcb1c..c663e500c4 100644
--- a/examples/SampleApp/src/components/AttachmentPickerSelectionBar.tsx
+++ b/examples/SampleApp/src/components/AttachmentPickerSelectionBar.tsx
@@ -1,36 +1,57 @@
-import { useState } from 'react';
-import { Pressable, StyleSheet, View } from 'react-native';
-import { AttachmentPickerSelectionBar, useMessageInputContext } from 'stream-chat-react-native';
+import React, { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+import {
+ AttachmentTypePickerButton,
+ useAttachmentPickerState,
+ CameraPickerButton,
+ CommandsPickerButton,
+ FilePickerButton,
+ MediaPickerButton,
+ PollPickerButton,
+ useAttachmentPickerContext,
+ useStableCallback,
+} from 'stream-chat-react-native';
import { ShareLocationIcon } from '../icons/ShareLocationIcon';
import { LiveLocationCreateModal } from './LocationSharing/CreateLocationModal';
export const CustomAttachmentPickerSelectionBar = () => {
const [modalVisible, setModalVisible] = useState(false);
- const { closeAttachmentPicker } = useMessageInputContext();
+ const { attachmentPickerStore } = useAttachmentPickerContext();
+ const { selectedPicker } = useAttachmentPickerState();
const onRequestClose = () => {
setModalVisible(false);
- closeAttachmentPicker();
};
- const onOpenModal = () => {
+ const onOpenModal = useStableCallback(() => {
+ attachmentPickerStore.setSelectedPicker('location');
setModalVisible(true);
- };
+ });
return (
-
-
-
-
-
+
+
+
+
+
+
+ {modalVisible ? (
+
+ ) : null}
);
};
const styles = StyleSheet.create({
- selectionBar: { flexDirection: 'row', alignItems: 'center' },
- liveLocationButton: {
- paddingLeft: 4,
+ selectionBar: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingBottom: 12,
},
});
diff --git a/examples/SampleApp/src/components/ChannelInfoOverlay.tsx b/examples/SampleApp/src/components/ChannelInfoOverlay.tsx
index 911353cd9a..228c715b87 100644
--- a/examples/SampleApp/src/components/ChannelInfoOverlay.tsx
+++ b/examples/SampleApp/src/components/ChannelInfoOverlay.tsx
@@ -15,13 +15,13 @@ import Animated, {
withTiming,
} from 'react-native-reanimated';
import {
- Avatar,
CircleClose,
Delete,
User,
UserMinus,
useTheme,
useViewport,
+ UserAvatar,
} from 'stream-chat-react-native';
import { ChannelMemberResponse } from 'stream-chat';
@@ -34,8 +34,6 @@ import { SafeAreaView } from 'react-native-safe-area-context';
dayjs.extend(relativeTime);
-const avatarSize = 64;
-
const styles = StyleSheet.create({
avatarPresenceIndicator: {
right: 5,
@@ -83,7 +81,7 @@ const styles = StyleSheet.create({
fontSize: 14,
fontWeight: '700',
},
- userItemContainer: { marginHorizontal: 8, width: 64 },
+ userItemContainer: { marginHorizontal: 8, alignItems: 'center' },
userName: {
fontSize: 12,
fontWeight: '600',
@@ -112,7 +110,8 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
const {
theme: {
- colors: { accent_red, black, border, grey, white },
+ colors: { accent_red, black, grey, white },
+ semantics,
},
} = useTheme();
@@ -129,19 +128,19 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
}
showScreen.value = show
? withTiming(1, {
- duration: 150,
- easing: Easing.in(Easing.ease),
- })
+ duration: 150,
+ easing: Easing.in(Easing.ease),
+ })
: withTiming(
- 0,
- {
- duration: 150,
- easing: Easing.out(Easing.ease),
- },
- () => {
- runOnJS(reset)();
- },
- );
+ 0,
+ {
+ duration: 150,
+ easing: Easing.out(Easing.ease),
+ },
+ () => {
+ runOnJS(reset)();
+ },
+ );
};
useEffect(() => {
@@ -187,12 +186,12 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
translateY.value =
evt.velocityY > 1000
? withDecay({
- velocity: evt.velocityY,
- })
+ velocity: evt.velocityY,
+ })
: withTiming(screenHeight, {
- duration: 200,
- easing: Easing.out(Easing.ease),
- });
+ duration: 200,
+ easing: Easing.out(Easing.ease),
+ });
} else {
translateY.value = withTiming(0);
overlayOpacity.value = withTiming(1);
@@ -227,31 +226,31 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
: 0;
const channelName = channel
? channel.data?.name ||
- Object.values(channel.state.members)
- .slice(0)
- .reduce((returnString, currentMember, index, originalArray) => {
- const returnStringLength = returnString.length;
- const currentMemberName =
- currentMember.user?.name || currentMember.user?.id || 'Unknown User';
- // a rough approximation of when the +Number shows up
- if (returnStringLength + (currentMemberName.length + 2) < maxWidth) {
- if (returnStringLength) {
- returnString += `, ${currentMemberName}`;
- } else {
- returnString = currentMemberName;
- }
+ Object.values(channel.state.members)
+ .slice(0)
+ .reduce((returnString, currentMember, index, originalArray) => {
+ const returnStringLength = returnString.length;
+ const currentMemberName =
+ currentMember.user?.name || currentMember.user?.id || 'Unknown User';
+ // a rough approximation of when the +Number shows up
+ if (returnStringLength + (currentMemberName.length + 2) < maxWidth) {
+ if (returnStringLength) {
+ returnString += `, ${currentMemberName}`;
} else {
- const remainingMembers = originalArray.length - index;
- returnString += `, +${remainingMembers}`;
- originalArray.splice(1); // exit early
+ returnString = currentMemberName;
}
- return returnString;
- }, '')
+ } else {
+ const remainingMembers = originalArray.length - index;
+ returnString += `, +${remainingMembers}`;
+ originalArray.splice(1); // exit early
+ }
+ return returnString;
+ }, '')
: '';
const otherMembers = channel
? Object.values(channel.state.members).filter(
- (member) => member.user?.id !== clientId,
- )
+ (member) => member.user?.id !== clientId,
+ )
: [];
const { viewInfo, pinUnpin, archiveUnarchive, leaveGroup, deleteConversation, cancel } =
@@ -287,11 +286,10 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
? otherMembers[0].user?.online
? 'Online'
: `Last Seen ${dayjs(otherMembers[0].user?.last_active).fromNow()}`
- : `${Object.keys(channel.state.members).length} Members, ${
- Object.values(channel.state.members).filter(
- (member) => !!member.user?.online,
- ).length
- } Online`}
+ : `${Object.keys(channel.state.members).length} Members, ${Object.values(channel.state.members).filter(
+ (member) => !!member.user?.online,
+ ).length
+ } Online`}
{
renderItem={({ item }) =>
item ? (
-
+
{item.name || item.id || ''}
@@ -332,7 +329,7 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
style={[
styles.row,
{
- borderTopColor: border,
+ borderTopColor: semantics.borderCoreDefault,
},
]}
>
@@ -347,7 +344,7 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
style={[
styles.row,
{
- borderTopColor: border,
+ borderTopColor: semantics.borderCoreDefault,
},
]}
>
@@ -364,7 +361,7 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
style={[
styles.row,
{
- borderTopColor: border,
+ borderTopColor: semantics.borderCoreDefault,
},
]}
>
@@ -379,7 +376,7 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
{otherMembers.length > 1 && (
-
+
@@ -392,7 +389,7 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
style={[
styles.row,
{
- borderTopColor: border,
+ borderTopColor: semantics.borderCoreDefault,
},
]}
>
@@ -409,8 +406,8 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
style={[
styles.lastRow,
{
- borderBottomColor: border,
- borderTopColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
+ borderTopColor: semantics.borderCoreDefault,
},
]}
>
diff --git a/examples/SampleApp/src/components/ConfirmationBottomSheet.tsx b/examples/SampleApp/src/components/ConfirmationBottomSheet.tsx
index 4f33bc9948..3eab535667 100644
--- a/examples/SampleApp/src/components/ConfirmationBottomSheet.tsx
+++ b/examples/SampleApp/src/components/ConfirmationBottomSheet.tsx
@@ -52,7 +52,8 @@ export const ConfirmationBottomSheet: React.FC = () => {
const {
theme: {
- colors: { accent_red, black, border, grey, white },
+ colors: { accent_red, black, grey, white },
+ semantics,
},
} = useTheme();
const inset = useSafeAreaInsets();
@@ -86,7 +87,7 @@ export const ConfirmationBottomSheet: React.FC = () => {
style={[
styles.actionButtonsContainer,
{
- borderTopColor: border,
+ borderTopColor: semantics.borderCoreDefault,
},
]}
>
diff --git a/examples/SampleApp/src/components/MessageInfoBottomSheet.tsx b/examples/SampleApp/src/components/MessageInfoBottomSheet.tsx
index a4ad65728b..d3a385a951 100644
--- a/examples/SampleApp/src/components/MessageInfoBottomSheet.tsx
+++ b/examples/SampleApp/src/components/MessageInfoBottomSheet.tsx
@@ -1,18 +1,18 @@
import React, { useMemo } from 'react';
import {
- Avatar,
BottomSheetModal,
useChatContext,
useMessageDeliveredData,
useMessageReadData,
useTheme,
+ UserAvatar,
} from 'stream-chat-react-native';
import { LocalMessage, UserResponse } from 'stream-chat';
import { FlatList, StyleSheet, Text, View } from 'react-native';
const renderUserItem = ({ item }: { item: UserResponse }) => (
-
+
{item.name ?? item.id}
);
diff --git a/examples/SampleApp/src/components/MessageSearch/MessageSearchList.tsx b/examples/SampleApp/src/components/MessageSearch/MessageSearchList.tsx
index b3e91999c3..ad2ec54ded 100644
--- a/examples/SampleApp/src/components/MessageSearch/MessageSearchList.tsx
+++ b/examples/SampleApp/src/components/MessageSearch/MessageSearchList.tsx
@@ -3,7 +3,7 @@ import { FlatList, StyleSheet, Text, TouchableOpacity, View } from 'react-native
import { NavigationProp, useNavigation } from '@react-navigation/native';
import dayjs from 'dayjs';
import calendar from 'dayjs/plugin/calendar';
-import { Avatar, Spinner, useTheme, useViewport } from 'stream-chat-react-native';
+import { Spinner, useTheme, useViewport, UserAvatar } from 'stream-chat-react-native';
import { DEFAULT_PAGINATION_LIMIT } from '../../utils/constants';
import type { MessageResponse } from 'stream-chat';
@@ -27,6 +27,8 @@ const styles = StyleSheet.create({
justifyContent: 'center',
},
itemContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
borderBottomWidth: 1,
flex: 1,
flexDirection: 'row',
@@ -70,7 +72,8 @@ export const MessageSearchList: React.FC = React.forward
} = props;
const {
theme: {
- colors: { black, border, grey, white_snow },
+ colors: { black, grey, white_snow },
+ semantics,
},
} = useTheme();
const { vw } = useViewport();
@@ -92,13 +95,11 @@ export const MessageSearchList: React.FC = React.forward
}}
>
- {`${
- messages.length >= DEFAULT_PAGINATION_LIMIT
- ? DEFAULT_PAGINATION_LIMIT
- : messages.length
- }${messages.length >= DEFAULT_PAGINATION_LIMIT ? '+ ' : ' '} result${
- messages.length === 1 ? '' : 's'
- }`}
+ {`${messages.length >= DEFAULT_PAGINATION_LIMIT
+ ? DEFAULT_PAGINATION_LIMIT
+ : messages.length
+ }${messages.length >= DEFAULT_PAGINATION_LIMIT ? '+ ' : ' '} result${messages.length === 1 ? '' : 's'
+ }`}
)}
@@ -128,15 +129,11 @@ export const MessageSearchList: React.FC = React.forward
messageId: item.id,
});
}}
- style={[styles.itemContainer, { borderBottomColor: border }]}
+ style={[styles.itemContainer, { borderBottomColor: semantics.borderCoreDefault }]}
testID='channel-preview-button'
>
-
+ {item.user ? : null}
+
({
timeLeftMs: state.timeLeftMs,
});
-export const MessageReminderHeader = ({ message }: MessageFooterProps) => {
+export const MessageReminderHeader = ({ message }: MessageHeaderProps) => {
const messageId = message?.id ?? '';
const reminder = useMessageReminder(messageId);
const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {};
const { t } = useTranslationContext();
+ const {
+ theme: { semantics },
+ } = useTheme();
+
const stopRefreshBoundaryMs = reminder?.timer.stopRefreshBoundaryMs;
const stopRefreshTimeStamp =
reminder?.remindAt && stopRefreshBoundaryMs
@@ -43,8 +50,8 @@ export const MessageReminderHeader = ({ message }: MessageFooterProps) => {
if (reminder.remindAt && timeLeftMs !== null) {
return (
-
-
+
+
{isBehindRefreshBoundary
? t('Due since {{ dueSince }}', {
dueSince: t('timestamp/ReminderNotification', {
@@ -62,6 +69,16 @@ export const MessageReminderHeader = ({ message }: MessageFooterProps) => {
}
};
+export const MessageHeader = () => {
+ const { message } = useMessageContext();
+ return (
+ <>
+
+
+ >
+ );
+};
+
const styles = StyleSheet.create({
headerContainer: {
flexDirection: 'row',
diff --git a/examples/SampleApp/src/components/ScreenHeader.tsx b/examples/SampleApp/src/components/ScreenHeader.tsx
index a36e48a285..b8cf024d68 100644
--- a/examples/SampleApp/src/components/ScreenHeader.tsx
+++ b/examples/SampleApp/src/components/ScreenHeader.tsx
@@ -118,7 +118,8 @@ export const ScreenHeader: React.FC = (props) => {
const {
theme: {
- colors: { black, border, grey, white },
+ colors: { black, grey, white },
+ semantics,
},
} = useTheme();
const insets = useSafeAreaInsets();
@@ -129,7 +130,7 @@ export const ScreenHeader: React.FC = (props) => {
styles.safeAreaContainer,
{
backgroundColor: white,
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreSubtle,
height: HEADER_CONTENT_HEIGHT + (inSafeArea ? 0 : insets.top),
},
style,
diff --git a/examples/SampleApp/src/components/SecretMenu.tsx b/examples/SampleApp/src/components/SecretMenu.tsx
index 0037645b82..aff708538f 100644
--- a/examples/SampleApp/src/components/SecretMenu.tsx
+++ b/examples/SampleApp/src/components/SecretMenu.tsx
@@ -26,6 +26,7 @@ export type NotificationConfigItem = { label: string; name: string; id: string }
export type MessageListImplementationConfigItem = { label: string; id: 'flatlist' | 'flashlist' };
export type MessageListModeConfigItem = { label: string; mode: 'default' | 'livestream' };
export type MessageListPruningConfigItem = { label: string; value: 100 | 500 | 1000 | undefined };
+export type MessageInputFloatingConfigItem = { label: string; value: boolean };
const messageListImplementationConfigItems: MessageListImplementationConfigItem[] = [
{ label: 'FlatList', id: 'flatlist' },
@@ -44,6 +45,11 @@ const messageListPruningConfigItems: MessageListPruningConfigItem[] = [
{ label: '1000 Messages', value: 1000 },
];
+const messageInputFloatingConfigItems: MessageInputFloatingConfigItem[] = [
+ { label: 'Normal', value: false },
+ { label: 'Floating', value: true },
+];
+
export const SlideInView = ({
visible,
children,
@@ -161,6 +167,23 @@ const SecretMenuMessageListImplementationConfigItem = ({
);
+const SecretMenuMessageInputFloatingConfigItem = ({
+ messageInputFloatingConfigItem,
+ storeMessageInputFloating,
+ isSelected,
+}: {
+ messageInputFloatingConfigItem: MessageInputFloatingConfigItem;
+ storeMessageInputFloating: (item: MessageInputFloatingConfigItem) => void;
+ isSelected: boolean;
+}) => (
+ storeMessageInputFloating(messageInputFloatingConfigItem)}
+ >
+ {messageInputFloatingConfigItem.label}
+
+);
+
const SecretMenuMessageListModeConfigItem = ({
messageListModeConfigItem,
storeMessageListMode,
@@ -218,6 +241,8 @@ export const SecretMenu = ({
const [selectedMessageListPruning, setSelectedMessageListPruning] = useState<
MessageListPruningConfigItem['value'] | null
>(null);
+ const [selectedMessageInputFloating, setSelectedMessageInputFloating] =
+ useState(false);
const {
theme: {
colors: { black, grey },
@@ -250,12 +275,19 @@ export const SecretMenu = ({
'@stream-rn-sampleapp-messagelist-pruning',
messageListPruningConfigItems[0],
);
+ const messageInputFloating = await AsyncStore.getItem(
+ '@stream-rn-sampleapp-messageinput-floating',
+ messageInputFloatingConfigItems[0],
+ );
setSelectedProvider(notificationProvider?.id ?? notificationConfigItems[0].id);
setSelectedMessageListImplementation(
messageListImplementation?.id ?? messageListImplementationConfigItems[0].id,
);
setSelectedMessageListMode(messageListMode?.mode ?? messageListModeConfigItems[0].mode);
setSelectedMessageListPruning(messageListPruning?.value);
+ setSelectedMessageInputFloating(
+ messageInputFloating?.value ?? messageInputFloatingConfigItems[0].value,
+ );
};
getSelectedConfig();
}, [notificationConfigItems]);
@@ -283,6 +315,11 @@ export const SecretMenu = ({
setSelectedMessageListPruning(item.value);
}, []);
+ const storeMessageInputFloating = useCallback(async (item: MessageInputFloatingConfigItem) => {
+ await AsyncStore.setItem('@stream-rn-sampleapp-messageinput-floating', item);
+ setSelectedMessageInputFloating(item.value);
+ }, []);
+
const removeAllDevices = useCallback(async () => {
const { devices } = await chatClient.getDevices(chatClient.userID);
for (const device of devices ?? []) {
@@ -335,6 +372,22 @@ export const SecretMenu = ({
+
+
+
+ Message Input Floating
+
+ {messageInputFloatingConfigItems.map((item) => (
+
+ ))}
+
+
+
diff --git a/examples/SampleApp/src/components/UnreadCountBadge.tsx b/examples/SampleApp/src/components/UnreadCountBadge.tsx
index 71d3a2694a..87c42c8ee2 100644
--- a/examples/SampleApp/src/components/UnreadCountBadge.tsx
+++ b/examples/SampleApp/src/components/UnreadCountBadge.tsx
@@ -1,33 +1,21 @@
import React, { useEffect, useState } from 'react';
-import { StyleSheet, Text, View } from 'react-native';
-import { useStateStore, useTheme } from 'stream-chat-react-native';
+import { BadgeNotification, useStateStore } from 'stream-chat-react-native';
import { useAppContext } from '../context/AppContext';
import { ThreadManagerState } from 'stream-chat';
-const styles = StyleSheet.create({
- unreadContainer: {
- alignItems: 'center',
- borderRadius: 8,
- justifyContent: 'center',
- },
- unreadText: {
- color: '#FFFFFF',
- fontSize: 11,
- fontWeight: '700',
- paddingHorizontal: 5,
- paddingVertical: 1,
- },
-});
-
const selector = (nextValue: ThreadManagerState) =>
- ({ unreadCount: nextValue.unreadThreadCount } as const);
+ ({ unreadCount: nextValue.unreadThreadCount }) as const;
export const ThreadsUnreadCountBadge: React.FC = () => {
const { chatClient } = useAppContext();
const { unreadCount } = useStateStore(chatClient?.threads?.state, selector) ?? { unreadCount: 0 };
- return ;
+ if (unreadCount === 0) {
+ return null;
+ }
+
+ return ;
};
export const ChannelsUnreadCountBadge: React.FC = () => {
@@ -59,26 +47,9 @@ export const ChannelsUnreadCountBadge: React.FC = () => {
};
}, [chatClient]);
- return ;
-};
-
-type UnreadCountBadgeProps = {
- unreadCount: number | undefined;
-};
-
-const UnreadCountBadge: React.FC = (props) => {
- const { unreadCount } = props;
- const {
- theme: {
- colors: { accent_red },
- },
- } = useTheme();
+ if (unreadCount === 0) {
+ return null;
+ }
- return (
-
- {!!unreadCount && (
- {unreadCount > 99 ? '99+' : unreadCount}
- )}
-
- );
+ return ;
};
diff --git a/examples/SampleApp/src/components/UserInfoOverlay.tsx b/examples/SampleApp/src/components/UserInfoOverlay.tsx
index 1d487b8abb..91659ececf 100644
--- a/examples/SampleApp/src/components/UserInfoOverlay.tsx
+++ b/examples/SampleApp/src/components/UserInfoOverlay.tsx
@@ -15,7 +15,6 @@ import Animated, {
withTiming,
} from 'react-native-reanimated';
import {
- Avatar,
CircleClose,
MessageIcon,
useChatContext,
@@ -23,6 +22,7 @@ import {
UserMinus,
useTheme,
useViewport,
+ UserAvatar,
} from 'stream-chat-react-native';
import { useAppOverlayContext } from '../context/AppOverlayContext';
@@ -35,8 +35,6 @@ import { SafeAreaView } from 'react-native-safe-area-context';
dayjs.extend(relativeTime);
-const avatarSize = 64;
-
const styles = StyleSheet.create({
avatarPresenceIndicator: {
right: 5,
@@ -77,10 +75,7 @@ const styles = StyleSheet.create({
fontWeight: '700',
},
userItemContainer: {
- marginHorizontal: 8,
- paddingBottom: 24,
- paddingTop: 16,
- width: 64,
+ paddingVertical: 16,
},
userName: {
fontSize: 12,
@@ -110,7 +105,8 @@ export const UserInfoOverlay = (props: UserInfoOverlayProps) => {
const {
theme: {
- colors: { accent_red, black, border, grey, white },
+ colors: { accent_red, black, grey, white },
+ semantics,
},
} = useTheme();
@@ -127,19 +123,19 @@ export const UserInfoOverlay = (props: UserInfoOverlayProps) => {
}
showScreen.value = show
? withTiming(1, {
- duration: 150,
- easing: Easing.in(Easing.ease),
- })
+ duration: 150,
+ easing: Easing.in(Easing.ease),
+ })
: withTiming(
- 0,
- {
- duration: 150,
- easing: Easing.out(Easing.ease),
- },
- () => {
- runOnJS(reset)();
- },
- );
+ 0,
+ {
+ duration: 150,
+ easing: Easing.out(Easing.ease),
+ },
+ () => {
+ runOnJS(reset)();
+ },
+ );
};
useEffect(() => {
@@ -185,12 +181,12 @@ export const UserInfoOverlay = (props: UserInfoOverlayProps) => {
translateY.value =
evt.velocityY > 1000
? withDecay({
- velocity: evt.velocityY,
- })
+ velocity: evt.velocityY,
+ })
: withTiming(screenHeight, {
- duration: 200,
- easing: Easing.out(Easing.ease),
- });
+ duration: 200,
+ easing: Easing.out(Easing.ease),
+ });
} else {
translateY.value = withTiming(0);
overlayOpacity.value = withTiming(1);
@@ -221,8 +217,8 @@ export const UserInfoOverlay = (props: UserInfoOverlayProps) => {
const self = channel
? Object.values(channel.state.members).find(
- (channelMember) => channelMember.user?.id === client.user?.id,
- )
+ (channelMember) => channelMember.user?.id === client.user?.id,
+ )
: undefined;
const { viewInfo, messageUser, removeFromGroup, cancel } = useUserInfoOverlayActions();
@@ -269,12 +265,11 @@ export const UserInfoOverlay = (props: UserInfoOverlayProps) => {
: `Last Seen ${dayjs(member.user?.last_active).fromNow()}`}
-
@@ -283,7 +278,7 @@ export const UserInfoOverlay = (props: UserInfoOverlayProps) => {
style={[
styles.row,
{
- borderTopColor: border,
+ borderTopColor: semantics.borderCoreDefault,
},
]}
>
@@ -298,7 +293,7 @@ export const UserInfoOverlay = (props: UserInfoOverlayProps) => {
style={[
styles.row,
{
- borderTopColor: border,
+ borderTopColor: semantics.borderCoreDefault,
},
]}
>
@@ -314,7 +309,7 @@ export const UserInfoOverlay = (props: UserInfoOverlayProps) => {
style={[
styles.row,
{
- borderTopColor: border,
+ borderTopColor: semantics.borderCoreDefault,
},
]}
>
@@ -332,8 +327,8 @@ export const UserInfoOverlay = (props: UserInfoOverlayProps) => {
style={[
styles.lastRow,
{
- borderBottomColor: border,
- borderTopColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
+ borderTopColor: semantics.borderCoreDefault,
},
]}
>
diff --git a/examples/SampleApp/src/components/UserSearch/UserGridItem.tsx b/examples/SampleApp/src/components/UserSearch/UserGridItem.tsx
index c61449e923..2fad413d0f 100644
--- a/examples/SampleApp/src/components/UserSearch/UserGridItem.tsx
+++ b/examples/SampleApp/src/components/UserSearch/UserGridItem.tsx
@@ -1,13 +1,10 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { TouchableOpacity } from '@gorhom/bottom-sheet';
-import { Avatar, Close, useTheme } from 'stream-chat-react-native';
+import { Close, useTheme, UserAvatar } from 'stream-chat-react-native';
import type { UserResponse } from 'stream-chat';
-
-const presenceIndicator = { cx: 7, cy: 7, r: 5 };
-
const styles = StyleSheet.create({
presenceIndicatorContainer: {
bottom: 0,
@@ -55,13 +52,8 @@ export const UserGridItem: React.FC = ({
} = useTheme();
return (
-
+
+
{removeButton && (
= ({
bg_gradient_end,
bg_gradient_start,
black,
- border,
grey,
grey_gainsboro,
white_smoke,
white_snow,
},
+ semantics,
},
} = useTheme();
const { vw } = useViewport();
@@ -199,11 +199,11 @@ export const UserSearchResults: React.FC = ({
styles.searchResultContainer,
{
backgroundColor: white_snow,
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
-
+
void;
switchUser: (userId?: string) => void;
messageListImplementation: MessageListImplementationConfigItem['id'];
+ messageInputFloating: MessageInputFloatingConfigItem['value'];
messageListMode: MessageListModeConfigItem['mode'];
messageListPruning: MessageListPruningConfigItem['value'];
};
diff --git a/examples/SampleApp/src/hooks/useStreamChatTheme.ts b/examples/SampleApp/src/hooks/useStreamChatTheme.ts
index f868e9ee2c..8a0ad4f44f 100644
--- a/examples/SampleApp/src/hooks/useStreamChatTheme.ts
+++ b/examples/SampleApp/src/hooks/useStreamChatTheme.ts
@@ -14,7 +14,6 @@ const getChatStyle = (colorScheme: ColorSchemeName): DeepPartial => ({
bg_user: '#17191C',
black: '#FFFFFF',
blue_alice: '#00193D',
- border: '#141924',
button_background: '#FFFFFF',
button_text: '#005FFF',
code_block: '#222222',
@@ -43,7 +42,6 @@ const getChatStyle = (colorScheme: ColorSchemeName): DeepPartial => ({
bg_gradient_start: '#FCFCFC',
black: '#000000',
blue_alice: '#E9F2FF',
- border: '#00000014', // 14 = 8% opacity; top: x=0, y=-1; bottom: x=0, y=1
button_background: '#005FFF',
button_text: '#FFFFFF',
grey: '#7A7A7A',
diff --git a/examples/SampleApp/src/icons/Bell.tsx b/examples/SampleApp/src/icons/Bell.tsx
index 0d8c355ffb..e2e669941a 100644
--- a/examples/SampleApp/src/icons/Bell.tsx
+++ b/examples/SampleApp/src/icons/Bell.tsx
@@ -7,7 +7,7 @@ import { IconProps } from '../utils/base';
export const Bell: React.FC = ({ height = 512, width = 512 }) => {
const {
theme: {
- colors: { grey },
+ semantics
},
} = useTheme();
@@ -15,7 +15,7 @@ export const Bell: React.FC = ({ height = 512, width = 512 }) => {
);
diff --git a/examples/SampleApp/src/icons/ShareLocationIcon.tsx b/examples/SampleApp/src/icons/ShareLocationIcon.tsx
index 79c1e13584..2680f19261 100644
--- a/examples/SampleApp/src/icons/ShareLocationIcon.tsx
+++ b/examples/SampleApp/src/icons/ShareLocationIcon.tsx
@@ -1,24 +1,19 @@
import Svg, { Path } from 'react-native-svg';
-import { useTheme } from 'stream-chat-react-native';
+import { ColorValue } from 'react-native';
// Icon for "Share Location" button, next to input box.
-export const ShareLocationIcon = () => {
- const {
- theme: {
- colors: { grey },
- },
- } = useTheme();
+export const ShareLocationIcon = ({ stroke }: { stroke: ColorValue }) => {
return (
-
)}
showUnreadCountBadge
@@ -121,11 +121,15 @@ export const ChannelScreen: React.FC = ({
params: { channel: channelFromProp, channelId, messageId },
},
}) => {
- const { chatClient, messageListImplementation, messageListMode, messageListPruning } =
- useAppContext();
- const { bottom } = useSafeAreaInsets();
const {
- theme: { colors },
+ chatClient,
+ messageListImplementation,
+ messageListMode,
+ messageListPruning,
+ messageInputFloating,
+ } = useAppContext();
+ const {
+ theme: { semantics, colors },
} = useTheme();
const { t } = useTranslationContext();
const { setThread } = useStreamChatContext();
@@ -205,10 +209,11 @@ export const ChannelScreen: React.FC = ({
chatClient,
t,
colors,
+ semantics,
handleMessageInfo,
});
},
- [chatClient, colors, t, handleMessageInfo],
+ [chatClient, t, colors, semantics, handleMessageInfo],
);
if (!channel || !chatClient) {
@@ -216,18 +221,19 @@ export const ChannelScreen: React.FC = ({
}
return (
-
+
null}
diff --git a/examples/SampleApp/src/screens/GroupChannelDetailsScreen.tsx b/examples/SampleApp/src/screens/GroupChannelDetailsScreen.tsx
index 0746bd158d..667c855364 100644
--- a/examples/SampleApp/src/screens/GroupChannelDetailsScreen.tsx
+++ b/examples/SampleApp/src/screens/GroupChannelDetailsScreen.tsx
@@ -11,10 +11,10 @@ import {
import { RouteProp, useNavigation } from '@react-navigation/native';
import { SafeAreaView } from 'react-native-safe-area-context';
import {
- Avatar,
useChannelPreviewDisplayName,
useOverlayContext,
useTheme,
+ UserAvatar,
} from 'stream-chat-react-native';
import { RoundButton } from '../components/RoundButton';
@@ -163,7 +163,8 @@ export const GroupChannelDetailsScreen: React.FC = ({
const { setOverlay } = useOverlayContext();
const {
theme: {
- colors: { accent_blue, accent_green, black, border, grey, white, white_smoke },
+ colors: { accent_blue, accent_green, black, grey, white, white_smoke },
+ semantics,
},
} = useTheme();
@@ -276,16 +277,16 @@ export const GroupChannelDetailsScreen: React.FC = ({
style={[
styles.memberContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
-
{member.user?.name}
@@ -306,7 +307,7 @@ export const GroupChannelDetailsScreen: React.FC = ({
style={[
styles.loadMoreButton,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
@@ -330,7 +331,7 @@ export const GroupChannelDetailsScreen: React.FC = ({
style={[
styles.changeNameContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
@@ -382,7 +383,7 @@ export const GroupChannelDetailsScreen: React.FC = ({
style={[
styles.actionContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
@@ -427,7 +428,7 @@ export const GroupChannelDetailsScreen: React.FC = ({
style={[
styles.actionContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
@@ -457,7 +458,7 @@ export const GroupChannelDetailsScreen: React.FC = ({
style={[
styles.actionContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
@@ -487,7 +488,7 @@ export const GroupChannelDetailsScreen: React.FC = ({
style={[
styles.actionContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
@@ -513,7 +514,7 @@ export const GroupChannelDetailsScreen: React.FC = ({
style={[
styles.actionContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
diff --git a/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx b/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx
index 7ecb9e9992..339ee4c998 100644
--- a/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx
+++ b/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx
@@ -118,7 +118,8 @@ export const NewDirectMessagingScreen: React.FC =
}) => {
const {
theme: {
- colors: { accent_blue, black, border, grey, white },
+ colors: { accent_blue, black, grey, white },
+ semantics,
},
} = useTheme();
const { chatClient } = useAppContext();
@@ -208,7 +209,7 @@ export const NewDirectMessagingScreen: React.FC =
styles.searchContainer,
{
backgroundColor: white,
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
diff --git a/examples/SampleApp/src/screens/NewGroupChannelAddMemberScreen.tsx b/examples/SampleApp/src/screens/NewGroupChannelAddMemberScreen.tsx
index eb963ed8ce..8897f77c36 100644
--- a/examples/SampleApp/src/screens/NewGroupChannelAddMemberScreen.tsx
+++ b/examples/SampleApp/src/screens/NewGroupChannelAddMemberScreen.tsx
@@ -77,7 +77,8 @@ export const NewGroupChannelAddMemberScreen: React.FC = ({ navigation })
const {
theme: {
- colors: { black, border, grey, white },
+ colors: { black, grey, white },
+ semantics,
},
} = useTheme();
@@ -111,7 +112,7 @@ export const NewGroupChannelAddMemberScreen: React.FC = ({ navigation })
styles.inputBoxContainer,
{
backgroundColor: white,
- borderColor: border,
+ borderColor: semantics.borderCoreDefault,
marginBottom: selectedUsers.length === 0 ? 8 : 16,
},
]}
diff --git a/examples/SampleApp/src/screens/NewGroupChannelAssignNameScreen.tsx b/examples/SampleApp/src/screens/NewGroupChannelAssignNameScreen.tsx
index 6a3a039338..270ce5e532 100644
--- a/examples/SampleApp/src/screens/NewGroupChannelAssignNameScreen.tsx
+++ b/examples/SampleApp/src/screens/NewGroupChannelAssignNameScreen.tsx
@@ -58,13 +58,13 @@ const ConfirmButton: React.FC = (props) => {
const { disabled, onPress } = props;
const {
theme: {
- colors: { accent_blue, grey },
+ semantics,
},
} = useTheme();
return (
-
+
);
};
@@ -86,7 +86,8 @@ export const NewGroupChannelAssignNameScreen: React.FC
diff --git a/examples/SampleApp/src/screens/OneOnOneChannelDetailScreen.tsx b/examples/SampleApp/src/screens/OneOnOneChannelDetailScreen.tsx
index d492bc795b..36fd1d7de2 100644
--- a/examples/SampleApp/src/screens/OneOnOneChannelDetailScreen.tsx
+++ b/examples/SampleApp/src/screens/OneOnOneChannelDetailScreen.tsx
@@ -1,13 +1,5 @@
import React, { useState } from 'react';
-import {
- Image,
- ScrollView,
- StyleSheet,
- Switch,
- Text,
- TouchableOpacity,
- View,
-} from 'react-native';
+import { Image, ScrollView, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { Delete, useTheme } from 'stream-chat-react-native';
import { useAppContext } from '../context/AppContext';
@@ -142,7 +134,8 @@ export const OneOnOneChannelDetailScreen: React.FC = ({
}) => {
const {
theme: {
- colors: { accent_green, accent_red, black, border, grey, white, white_smoke },
+ colors: { accent_green, accent_red, black, grey, white, white_smoke },
+ semantics,
},
} = useTheme();
const { chatClient } = useAppContext();
@@ -156,13 +149,13 @@ export const OneOnOneChannelDetailScreen: React.FC = ({
const user = member?.user;
const [muted, setMuted] = useState(
chatClient?.mutedUsers &&
- chatClient?.mutedUsers?.findIndex((mutedUser) => mutedUser.target.id === user?.id) > -1,
+ chatClient?.mutedUsers?.findIndex((mutedUser) => mutedUser.target.id === user?.id) > -1,
);
const [notificationsEnabled, setNotificationsEnabled] = useState(
chatClient?.mutedChannels &&
- chatClient.mutedChannels.findIndex(
- (mutedChannel) => mutedChannel.channel?.id === channel.id,
- ) > -1,
+ chatClient.mutedChannels.findIndex(
+ (mutedChannel) => mutedChannel.channel?.id === channel.id,
+ ) > -1,
);
/**
@@ -235,7 +228,7 @@ export const OneOnOneChannelDetailScreen: React.FC = ({
style={[
styles.userNameContainer,
{
- borderTopColor: border,
+ borderTopColor: semantics.borderCoreDefault,
},
]}
>
@@ -274,7 +267,7 @@ export const OneOnOneChannelDetailScreen: React.FC = ({
style={[
styles.actionContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
@@ -313,7 +306,7 @@ export const OneOnOneChannelDetailScreen: React.FC = ({
style={[
styles.actionContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
@@ -359,7 +352,7 @@ export const OneOnOneChannelDetailScreen: React.FC = ({
style={[
styles.actionContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
@@ -389,7 +382,7 @@ export const OneOnOneChannelDetailScreen: React.FC = ({
style={[
styles.actionContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
@@ -419,7 +412,7 @@ export const OneOnOneChannelDetailScreen: React.FC = ({
style={[
styles.actionContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
@@ -449,7 +442,7 @@ export const OneOnOneChannelDetailScreen: React.FC = ({
style={[
styles.actionContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
@@ -476,7 +469,7 @@ export const OneOnOneChannelDetailScreen: React.FC = ({
style={[
styles.actionContainer,
{
- borderBottomColor: border,
+ borderBottomColor: semantics.borderCoreDefault,
},
]}
>
diff --git a/examples/SampleApp/src/screens/SharedGroupsScreen.tsx b/examples/SampleApp/src/screens/SharedGroupsScreen.tsx
index c0c0894e97..8067c2407f 100644
--- a/examples/SampleApp/src/screens/SharedGroupsScreen.tsx
+++ b/examples/SampleApp/src/screens/SharedGroupsScreen.tsx
@@ -1,8 +1,7 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { NavigationProp, RouteProp, useNavigation } from '@react-navigation/native';
import {
- Avatar,
ChannelList,
ChannelListMessenger,
ChannelListMessengerProps,
@@ -12,6 +11,8 @@ import {
useChannelPreviewDisplayName,
useChannelsContext,
useTheme,
+ Avatar,
+ getInitialsFromName,
} from 'stream-chat-react-native';
import { ScreenHeader } from '../components/ScreenHeader';
@@ -66,6 +67,16 @@ const CustomPreview: React.FC = ({ channel }) => {
},
} = useTheme();
+ const displayAvatar = getChannelPreviewDisplayAvatar(channel, chatClient);
+
+ const placeholder = useMemo(() => {
+ if (displayAvatar?.name) {
+ return {getInitialsFromName(displayAvatar?.name)};
+ } else {
+ return ?;
+ }
+ }, [displayAvatar.name]);
+
if (!chatClient) {
return null;
}
@@ -74,8 +85,6 @@ const CustomPreview: React.FC = ({ channel }) => {
return null;
}
- const displayAvatar = getChannelPreviewDisplayAvatar(channel, chatClient);
-
const switchToChannel = () => {
navigation.reset({
index: 1,
@@ -106,17 +115,9 @@ const CustomPreview: React.FC = ({ channel }) => {
>
{displayAvatar.images ? (
-
+
) : (
-
+
)}
{name}
diff --git a/examples/SampleApp/src/screens/ThreadScreen.tsx b/examples/SampleApp/src/screens/ThreadScreen.tsx
index ddf81d1ec4..75fd66e7f3 100644
--- a/examples/SampleApp/src/screens/ThreadScreen.tsx
+++ b/examples/SampleApp/src/screens/ThreadScreen.tsx
@@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import { Platform, StyleSheet, View } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
+
import {
Channel,
MessageActionsParams,
@@ -26,6 +26,7 @@ import { useStreamChatContext } from '../context/StreamChatContext.tsx';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx';
import { MessageLocation } from '../components/LocationSharing/MessageLocation.tsx';
+import { useAppContext } from '../context/AppContext.ts';
const selector = (nextValue: ThreadState) => ({ parentMessage: nextValue.parentMessage }) as const;
@@ -64,7 +65,6 @@ const ThreadHeader: React.FC = ({ thread }) => {
return (
@@ -79,12 +79,14 @@ export const ThreadScreen: React.FC = ({
}) => {
const {
theme: {
+ semantics,
colors: { white },
},
} = useTheme();
const { client: chatClient } = useChatContext();
const { t } = useTranslationContext();
const { setThread } = useStreamChatContext();
+ const { messageInputFloating, messageListImplementation } = useAppContext();
const onPressMessage: NonNullable['onPressMessage']> = (
payload,
@@ -105,10 +107,11 @@ export const ThreadScreen: React.FC = ({
return channelMessageActions({
params,
chatClient,
+ semantics,
t,
});
},
- [chatClient, t],
+ [chatClient, semantics, t],
);
const onThreadDismount = useCallback(() => {
@@ -116,25 +119,24 @@ export const ThreadScreen: React.FC = ({
}, [setThread]);
return (
-
+
-
-
-
-
+
+
-
+
);
};
diff --git a/examples/SampleApp/src/screens/UserSelectorScreen.tsx b/examples/SampleApp/src/screens/UserSelectorScreen.tsx
index 0448c4ead6..49296412b4 100644
--- a/examples/SampleApp/src/screens/UserSelectorScreen.tsx
+++ b/examples/SampleApp/src/screens/UserSelectorScreen.tsx
@@ -90,7 +90,8 @@ type Props = {
export const UserSelectorScreen: React.FC = ({ navigation }) => {
const {
theme: {
- colors: { black, border, grey, grey_gainsboro, grey_whisper, white_snow },
+ colors: { black, grey, grey_gainsboro, grey_whisper, white_snow },
+ semantics,
},
} = useTheme();
const { switchUser } = useAppContext();
@@ -125,7 +126,7 @@ export const UserSelectorScreen: React.FC = ({ navigation }) => {
onPress={() => {
switchUser(u.id);
}}
- style={[styles.userContainer, { borderBottomColor: border }]}
+ style={[styles.userContainer, { borderBottomColor: semantics.borderCoreDefault }]}
testID={`user-selector-button-${u.id}`}
>
= ({ navigation }) => {
onPress={() => {
navigation.navigate('AdvancedUserSelectorScreen');
}}
- style={[styles.userContainer, { borderBottomColor: border }]}
+ style={[styles.userContainer, { borderBottomColor: semantics.borderCoreDefault }]}
>
void;
+ semantics: Theme['semantics'];
}) {
const { dismissOverlay, deleteForMeMessage } = params;
const actions = messageActions(params);
@@ -46,7 +48,8 @@ export function channelMessageActions({
},
actionType: reminder ? 'remove-from-later' : 'save-for-later',
title: reminder ? 'Remove from Later' : 'Save for Later',
- icon: ,
+ icon: ,
+ type: 'standard',
});
actions.push({
action: () => {
@@ -91,7 +94,8 @@ export function channelMessageActions({
},
actionType: reminder ? 'remove-reminder' : 'remind-me',
title: reminder ? 'Remove Reminder' : 'Remind Me',
- icon: ,
+ icon: ,
+ type: 'standard',
});
actions.push({
action: async () => {
@@ -111,8 +115,9 @@ export function channelMessageActions({
]);
},
actionType: 'deleteForMe',
- icon: ,
+ icon: ,
title: t('Delete for me'),
+ type: 'destructive',
});
actions.push({
@@ -121,8 +126,9 @@ export function channelMessageActions({
handleMessageInfo(params.message);
},
actionType: 'messageInfo',
- icon: ,
+ icon: ,
title: 'Message Info',
+ type: 'standard',
});
return actions;
diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock
index 5d203517a1..729973a1f4 100644
--- a/examples/SampleApp/yarn.lock
+++ b/examples/SampleApp/yarn.lock
@@ -28,6 +28,15 @@
js-tokens "^4.0.0"
picocolors "^1.1.1"
+"@babel/code-frame@^7.28.6":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.28.6.tgz#72499312ec58b1e2245ba4a4f550c132be4982f7"
+ integrity sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.28.5"
+ js-tokens "^4.0.0"
+ picocolors "^1.1.1"
+
"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.26.5":
version "7.26.8"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367"
@@ -111,13 +120,13 @@
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^3.0.2"
-"@babel/generator@^7.28.0":
- version "7.28.0"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.0.tgz#9cc2f7bd6eb054d77dc66c2664148a0c5118acd2"
- integrity sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==
+"@babel/generator@^7.28.6":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.6.tgz#48dcc65d98fcc8626a48f72b62e263d25fc3c3f1"
+ integrity sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==
dependencies:
- "@babel/parser" "^7.28.0"
- "@babel/types" "^7.28.0"
+ "@babel/parser" "^7.28.6"
+ "@babel/types" "^7.28.6"
"@jridgewell/gen-mapping" "^0.3.12"
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
@@ -409,12 +418,12 @@
dependencies:
"@babel/types" "^7.27.3"
-"@babel/parser@^7.28.0":
- version "7.28.0"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.0.tgz#979829fbab51a29e13901e5a80713dbcb840825e"
- integrity sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==
+"@babel/parser@^7.28.6":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd"
+ integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==
dependencies:
- "@babel/types" "^7.28.0"
+ "@babel/types" "^7.28.6"
"@babel/plugin-proposal-export-default-from@^7.24.7":
version "7.25.9"
@@ -577,7 +586,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.27.1"
-"@babel/plugin-transform-arrow-functions@^7.0.0-0":
+"@babel/plugin-transform-arrow-functions@7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a"
integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==
@@ -616,7 +625,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.25.9"
-"@babel/plugin-transform-class-properties@^7.0.0-0":
+"@babel/plugin-transform-class-properties@7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz#dd40a6a370dfd49d32362ae206ddaf2bb082a925"
integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==
@@ -632,17 +641,17 @@
"@babel/helper-create-class-features-plugin" "^7.25.9"
"@babel/helper-plugin-utils" "^7.25.9"
-"@babel/plugin-transform-classes@^7.0.0-0":
- version "7.28.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz#12fa46cffc32a6e084011b650539e880add8a0f8"
- integrity sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==
+"@babel/plugin-transform-classes@7.28.4":
+ version "7.28.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz#75d66175486788c56728a73424d67cbc7473495c"
+ integrity sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==
dependencies:
"@babel/helper-annotate-as-pure" "^7.27.3"
"@babel/helper-compilation-targets" "^7.27.2"
"@babel/helper-globals" "^7.28.0"
"@babel/helper-plugin-utils" "^7.27.1"
"@babel/helper-replace-supers" "^7.27.1"
- "@babel/traverse" "^7.28.0"
+ "@babel/traverse" "^7.28.4"
"@babel/plugin-transform-classes@^7.25.4":
version "7.25.9"
@@ -734,7 +743,7 @@
"@babel/helper-create-regexp-features-plugin" "^7.25.9"
"@babel/helper-plugin-utils" "^7.25.9"
-"@babel/plugin-transform-nullish-coalescing-operator@^7.0.0-0":
+"@babel/plugin-transform-nullish-coalescing-operator@7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz#4f9d3153bf6782d73dd42785a9d22d03197bc91d"
integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==
@@ -771,7 +780,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.25.9"
-"@babel/plugin-transform-optional-chaining@^7.0.0-0":
+"@babel/plugin-transform-optional-chaining@7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz#874ce3c4f06b7780592e946026eb76a32830454f"
integrity sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==
@@ -863,7 +872,7 @@
babel-plugin-polyfill-regenerator "^0.6.1"
semver "^6.3.1"
-"@babel/plugin-transform-shorthand-properties@^7.0.0-0":
+"@babel/plugin-transform-shorthand-properties@7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90"
integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==
@@ -892,7 +901,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.25.9"
-"@babel/plugin-transform-template-literals@^7.0.0-0":
+"@babel/plugin-transform-template-literals@7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8"
integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==
@@ -921,7 +930,7 @@
"@babel/helper-skip-transparent-expression-wrappers" "^7.27.1"
"@babel/plugin-syntax-typescript" "^7.27.1"
-"@babel/plugin-transform-unicode-regex@^7.0.0-0":
+"@babel/plugin-transform-unicode-regex@7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97"
integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==
@@ -937,7 +946,7 @@
"@babel/helper-create-regexp-features-plugin" "^7.25.9"
"@babel/helper-plugin-utils" "^7.25.9"
-"@babel/preset-typescript@^7.16.7":
+"@babel/preset-typescript@7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz#190742a6428d282306648a55b0529b561484f912"
integrity sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==
@@ -978,6 +987,15 @@
"@babel/parser" "^7.27.2"
"@babel/types" "^7.27.1"
+"@babel/template@^7.28.6":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57"
+ integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==
+ dependencies:
+ "@babel/code-frame" "^7.28.6"
+ "@babel/parser" "^7.28.6"
+ "@babel/types" "^7.28.6"
+
"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.9.tgz#4398f2394ba66d05d988b2ad13c219a2c857461a"
@@ -1017,17 +1035,17 @@
debug "^4.3.1"
globals "^11.1.0"
-"@babel/traverse@^7.28.0":
- version "7.28.0"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.0.tgz#518aa113359b062042379e333db18380b537e34b"
- integrity sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==
+"@babel/traverse@^7.28.4":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.6.tgz#871ddc79a80599a5030c53b1cc48cbe3a5583c2e"
+ integrity sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==
dependencies:
- "@babel/code-frame" "^7.27.1"
- "@babel/generator" "^7.28.0"
+ "@babel/code-frame" "^7.28.6"
+ "@babel/generator" "^7.28.6"
"@babel/helper-globals" "^7.28.0"
- "@babel/parser" "^7.28.0"
- "@babel/template" "^7.27.2"
- "@babel/types" "^7.28.0"
+ "@babel/parser" "^7.28.6"
+ "@babel/template" "^7.28.6"
+ "@babel/types" "^7.28.6"
debug "^4.3.1"
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.2", "@babel/types@^7.25.9", "@babel/types@^7.26.9", "@babel/types@^7.3.3":
@@ -1054,13 +1072,13 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
-"@babel/types@^7.28.0":
- version "7.28.2"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b"
- integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==
+"@babel/types@^7.28.6":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df"
+ integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
- "@babel/helper-validator-identifier" "^7.27.1"
+ "@babel/helper-validator-identifier" "^7.28.5"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
@@ -1572,18 +1590,18 @@
resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz#a73bab8eb491d7b8b7be2f0e6c310647835afe83"
integrity sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==
-"@gorhom/bottom-sheet@^5.1.6":
- version "5.1.6"
- resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-5.1.6.tgz#92365894ae4d4eefdbaa577408cfaf62463a9490"
- integrity sha512-0b5tQj4fTaZAjST1PnkCp0p7d8iRqMezibTcqc8Kkn3N23Vn6upORNTD1fH0bLfwRt6e0WnZ7DjAmq315lrcKQ==
+"@gorhom/bottom-sheet@5.1.8":
+ version "5.1.8"
+ resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-5.1.8.tgz#65547917f5b1dae5a1291dabd4ea8bfee09feba4"
+ integrity sha512-QuYIVjn3K9bW20n5bgOSjvxBYoWG4YQXiLGOheEAMgISuoT6sMcA270ViSkkb0fenPxcIOwzCnFNuxmr739T9A==
dependencies:
"@gorhom/portal" "1.0.14"
invariant "^2.2.4"
-"@gorhom/bottom-sheet@^5.1.8":
- version "5.1.8"
- resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-5.1.8.tgz#65547917f5b1dae5a1291dabd4ea8bfee09feba4"
- integrity sha512-QuYIVjn3K9bW20n5bgOSjvxBYoWG4YQXiLGOheEAMgISuoT6sMcA270ViSkkb0fenPxcIOwzCnFNuxmr739T9A==
+"@gorhom/bottom-sheet@^5.1.6":
+ version "5.1.6"
+ resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-5.1.6.tgz#92365894ae4d4eefdbaa577408cfaf62463a9490"
+ integrity sha512-0b5tQj4fTaZAjST1PnkCp0p7d8iRqMezibTcqc8Kkn3N23Vn6upORNTD1fH0bLfwRt6e0WnZ7DjAmq315lrcKQ==
dependencies:
"@gorhom/portal" "1.0.14"
invariant "^2.2.4"
@@ -4127,7 +4145,7 @@ content-type@~1.0.5:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
-convert-source-map@^2.0.0:
+convert-source-map@2.0.0, convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
@@ -6616,6 +6634,11 @@ lodash-es@4.17.21:
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+lodash-es@4.17.23:
+ version "4.17.23"
+ resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.23.tgz#58c4360fd1b5d33afc6c0bbd3d1149349b1138e0"
+ integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==
+
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
@@ -7629,10 +7652,10 @@ react-native-fast-image@^8.6.3:
resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz#6edc3f9190092a909d636d93eecbcc54a8822255"
integrity sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==
-react-native-gesture-handler@^2.26.0:
- version "2.26.0"
- resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.26.0.tgz#e8774c8cd90f7e72c0ecade0ac1b4f7160fcbd5f"
- integrity sha512-pfE1j9Vzu0qpWj/Aq1IK+cYnougN69mCKvWuq1rdNjH2zs1WIszF0Mum9/oGQTemgjyc/JgiqOOTgwcleAMAGg==
+react-native-gesture-handler@^2.30.0:
+ version "2.30.0"
+ resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.30.0.tgz#990c621fbeeefde853ececdcab7cbe1b621dbb8b"
+ integrity sha512-5YsnKHGa0X9C8lb5oCnKm0fLUPM6CRduvUUw2Bav4RIj/C3HcFh4RIUnF8wgG6JQWCL1//gRx4v+LVWgcIQdGA==
dependencies:
"@egjs/hammerjs" "^2.0.17"
hoist-non-react-statics "^3.3.0"
@@ -7648,16 +7671,16 @@ react-native-image-picker@^8.2.1:
resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-8.2.1.tgz#1ac7826563cbaa5d5298d9f2acc53c69805e5393"
integrity sha512-FBeGYJGFDjMdGCcyubDJgBAPCQ4L1D3hwLXyUU91jY9ahOZMTbluceVvRmrEKqnDPFJ0gF1NVhJ0nr1nROFLdg==
+react-native-is-edge-to-edge@1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz#64e10851abd9d176cbf2b40562f751622bde3358"
+ integrity sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==
+
react-native-is-edge-to-edge@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz#28947688f9fafd584e73a4f935ea9603bd9b1939"
integrity sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w==
-react-native-is-edge-to-edge@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz#64e10851abd9d176cbf2b40562f751622bde3358"
- integrity sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==
-
react-native-lightbox@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/react-native-lightbox/-/react-native-lightbox-0.7.0.tgz#e52b4d7fcc141f59d7b23f0180de535e35b20ec9"
@@ -7693,13 +7716,13 @@ react-native-nitro-sound@^0.2.9:
dependencies:
"@react-native-community/slider" "^5.0.1"
-react-native-reanimated@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-4.0.1.tgz#6cb8bca007baa18d75e0ef8b03e969d2777cd5e8"
- integrity sha512-SZmIpxVd1yijV1MA8KB9S9TUj6JpdU4THjVB0WCkfV9p6F8oR3YxO4e+GRKbNci3mODp7plW095LhjaCB9bqZQ==
+react-native-reanimated@^4.2.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-4.2.1.tgz#fbdee721bff0946a6e5ae67c8c38c37ca4a0a057"
+ integrity sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg==
dependencies:
- react-native-is-edge-to-edge "^1.2.1"
- semver "7.7.2"
+ react-native-is-edge-to-edge "1.2.1"
+ semver "7.7.3"
react-native-safe-area-context@^5.4.1:
version "5.4.1"
@@ -7729,6 +7752,11 @@ react-native-svg@^15.12.0:
css-tree "^1.1.3"
warn-once "0.1.1"
+react-native-teleport@^0.5.4:
+ version "0.5.4"
+ resolved "https://registry.yarnpkg.com/react-native-teleport/-/react-native-teleport-0.5.4.tgz#6af16e3984ee0fc002e5d6c6dae0ad40f22699c2"
+ integrity sha512-kiNbB27lJHAO7DdG7LlPi6DaFnYusVQV9rqp0q5WoRpnqKNckIvzXULox6QpZBstaTF/jkO+xmlkU+pnQqKSAQ==
+
react-native-url-polyfill@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz#db714520a2985cff1d50ab2e66279b9f91ffd589"
@@ -7741,21 +7769,22 @@ react-native-video@^6.16.1:
resolved "https://registry.yarnpkg.com/react-native-video/-/react-native-video-6.16.1.tgz#c4f5f71eac930a4ae4e2faadb22fc05d78b9b7fe"
integrity sha512-+G6tVVGbwFqNTyPivqb+PhQzWr5OudDQ1dgvBNyBRAgcS8rOcbwuS6oX+m8cxOsXHn1UT9ofQnjQEwkGOsvomg==
-react-native-worklets@^0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.4.1.tgz#563d39160195101d9cf236b54b383d6950508263"
- integrity sha512-QXAMZ8jz0sLEoNrc3ej050z6Sd+UJ/Gef4SACeMuoLRinwHIy4uel7XtMPJZMqKhFerkwXZ7Ips5vIjnNyPDBA==
- dependencies:
- "@babel/plugin-transform-arrow-functions" "^7.0.0-0"
- "@babel/plugin-transform-class-properties" "^7.0.0-0"
- "@babel/plugin-transform-classes" "^7.0.0-0"
- "@babel/plugin-transform-nullish-coalescing-operator" "^7.0.0-0"
- "@babel/plugin-transform-optional-chaining" "^7.0.0-0"
- "@babel/plugin-transform-shorthand-properties" "^7.0.0-0"
- "@babel/plugin-transform-template-literals" "^7.0.0-0"
- "@babel/plugin-transform-unicode-regex" "^7.0.0-0"
- "@babel/preset-typescript" "^7.16.7"
- convert-source-map "^2.0.0"
+react-native-worklets@^0.7.2:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.7.2.tgz#acfbfe4f8c7f3b2889e7f394e4fbd7e78e167134"
+ integrity sha512-DuLu1kMV/Uyl9pQHp3hehAlThoLw7Yk2FwRTpzASOmI+cd4845FWn3m2bk9MnjUw8FBRIyhwLqYm2AJaXDXsog==
+ dependencies:
+ "@babel/plugin-transform-arrow-functions" "7.27.1"
+ "@babel/plugin-transform-class-properties" "7.27.1"
+ "@babel/plugin-transform-classes" "7.28.4"
+ "@babel/plugin-transform-nullish-coalescing-operator" "7.27.1"
+ "@babel/plugin-transform-optional-chaining" "7.27.1"
+ "@babel/plugin-transform-shorthand-properties" "7.27.1"
+ "@babel/plugin-transform-template-literals" "7.27.1"
+ "@babel/plugin-transform-unicode-regex" "7.27.1"
+ "@babel/preset-typescript" "7.27.1"
+ convert-source-map "2.0.0"
+ semver "7.7.3"
react-native@^0.80.2:
version "0.80.2"
@@ -8032,16 +8061,21 @@ scheduler@0.26.0, scheduler@^0.26.0:
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337"
integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==
-semver@7.7.2, semver@^7.1.3, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.7.2:
- version "7.7.2"
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
- integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
+semver@7.7.3:
+ version "7.7.3"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946"
+ integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
semver@^6.3.0, semver@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
+semver@^7.1.3, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.7.2:
+ version "7.7.2"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
+ integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
+
send@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
@@ -8320,10 +8354,10 @@ stream-chat-react-native-core@8.1.0:
version "0.0.0"
uid ""
-stream-chat@^9.27.2:
- version "9.27.2"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.27.2.tgz#5b41173e513f3606c47c93f391693b589e663968"
- integrity sha512-OdALDzg8lO8CAdl8deydJ1+O4wJ7mM9dPLeCwDppq/OQ4aFIS9X38P+IdXPcOCsgSS97UoVUuxD2/excC5PEeg==
+stream-chat@^9.35.1:
+ version "9.35.1"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.35.1.tgz#d828854a9c27ea7e45e6642d9107966c6606f552"
+ integrity sha512-649sgO7+llFuW+y/Ja0K4d94aUC+EMxYUVo5mq5AFGT86vUAIXmRIMVHYHA/jw4MYoqfWAFrDK6L9Rhyn/eMkQ==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
diff --git a/examples/TypeScriptMessaging/App.tsx b/examples/TypeScriptMessaging/App.tsx
index 4eeb980f14..49399cb22b 100644
--- a/examples/TypeScriptMessaging/App.tsx
+++ b/examples/TypeScriptMessaging/App.tsx
@@ -37,7 +37,6 @@ const options = {
presence: true,
state: true,
watch: true,
- limit: 30,
};
I18nManager.forceRTL(false);
diff --git a/package/eslint.config.mjs b/package/eslint.config.mjs
index 5ea72ef943..a154ab2d98 100644
--- a/package/eslint.config.mjs
+++ b/package/eslint.config.mjs
@@ -75,7 +75,18 @@ export default tsEslint.config(
settings: {
'import/resolver': {
node: {
- extensions: ['.js', '.jsx', '.ts', '.tsx'],
+ extensions: [
+ '.js',
+ '.jsx',
+ '.ts',
+ '.tsx',
+ '.ios.ts',
+ '.android.ts',
+ '.web.ts',
+ '.ios.tsx',
+ '.android.tsx',
+ '.web.tsx',
+ ],
paths: ['src'],
},
},
@@ -127,31 +138,9 @@ export default tsEslint.config(
'no-var': 2,
'object-shorthand': 1,
'prefer-const': 1,
- 'react/jsx-sort-props': [
- 'error',
- {
- callbacksLast: false,
- ignoreCase: true,
- noSortAlphabetically: false,
- reservedFirst: false,
- shorthandFirst: false,
- shorthandLast: false,
- },
- ],
'react/prop-types': 0,
'require-await': 2,
semi: [1, 'always'],
- 'sort-imports': [
- 'error',
- {
- allowSeparatedGroups: true,
- ignoreCase: true,
- ignoreDeclarationSort: true,
- ignoreMemberSort: false,
- memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
- },
- ],
- 'sort-keys': ['error', 'asc', { caseSensitive: false, minKeys: 2, natural: false }],
'valid-typeof': 2,
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-empty-interface': 0,
diff --git a/package/foobar.db-journal b/package/foobar.db-journal
new file mode 100644
index 0000000000..334d055043
Binary files /dev/null and b/package/foobar.db-journal differ
diff --git a/package/jest-setup.js b/package/jest-setup.js
index 5b2987f3d0..d4f50afd40 100644
--- a/package/jest-setup.js
+++ b/package/jest-setup.js
@@ -1,7 +1,8 @@
/* global require */
-import { FlatList, View } from 'react-native';
+import rn, { FlatList, View } from 'react-native';
import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock.js';
+import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock';
import { registerNativeHandlers } from './src/native';
@@ -39,6 +40,13 @@ jest.mock('react-native-reanimated', () => {
});
jest.mock('@react-native-community/netinfo', () => mockRNCNetInfo);
+
+const BottomSheetMock = ({ handleComponent, children }) => (
+
+ {handleComponent()}
+ {children}
+
+);
jest.mock('@gorhom/bottom-sheet', () => {
const react = require('react-native');
return {
@@ -47,8 +55,9 @@ jest.mock('@gorhom/bottom-sheet', () => {
BottomSheetModal: react.View,
BottomSheetModalProvider: react.View,
BottomSheetScrollView: react.ScrollView,
- default: react.View,
+ default: BottomSheetMock,
TouchableOpacity: react.View,
+ useBottomSheetSpringConfigs: jest.fn(() => ({})),
};
});
jest.mock('@op-engineering/op-sqlite', () => {
@@ -65,3 +74,34 @@ jest.mock('react-native/Libraries/Components/RefreshControl/RefreshControl', ()
jest.mock('@shopify/flash-list', () => ({
FlashList: undefined,
}));
+
+jest.mock('react-native-teleport', () => {
+ const rn = require('react-native');
+ return {
+ Portal: rn.View,
+ PortalHost: rn.View,
+ PortalProvider: rn.View,
+ usePortal: jest.fn().mockReturnValue({ removePortal: jest.fn() }),
+ };
+});
+
+jest.mock('react-native-teleport', () => {
+ const rn = require('react-native');
+ return {
+ Portal: rn.View,
+ PortalHost: rn.View,
+ PortalProvider: rn.View,
+ usePortal: jest.fn().mockReturnValue({ removePortal: jest.fn() }),
+ };
+});
+
+jest.mock('react-native-safe-area-context', () => mockSafeAreaContext);
+
+jest.mock('./src/components/Message/utils/measureInWindow', () => ({
+ measureInWindow: jest.fn(async () => ({
+ x: 10,
+ y: 100,
+ w: 250,
+ h: 60,
+ })),
+}));
diff --git a/package/native-package/src/optionalDependencies/Audio.ts b/package/native-package/src/optionalDependencies/Audio.ts
index 8df64a62f2..d3340571f2 100644
--- a/package/native-package/src/optionalDependencies/Audio.ts
+++ b/package/native-package/src/optionalDependencies/Audio.ts
@@ -137,7 +137,6 @@ class _Audio {
startPlayer = async (uri, _, onPlaybackStatusUpdate) => {
try {
const playback = await audioRecorderPlayer.startPlayer(uri);
- console.log({ playback });
audioRecorderPlayer.addPlayBackListener((status) => {
onPlaybackStatusUpdate(status);
});
diff --git a/package/package.json b/package/package.json
index 0f4028fa85..ae2aa00e6b 100644
--- a/package/package.json
+++ b/package/package.json
@@ -68,19 +68,19 @@
]
},
"dependencies": {
- "@gorhom/bottom-sheet": "^5.1.8",
+ "@gorhom/bottom-sheet": "5.1.8",
"@ungap/structured-clone": "^1.3.0",
"dayjs": "1.11.13",
"emoji-regex": "^10.4.0",
"i18next": "^25.2.1",
"intl-pluralrules": "^2.0.1",
- "linkifyjs": "^4.3.1",
- "lodash-es": "4.17.21",
+ "linkifyjs": "^4.3.2",
+ "lodash-es": "4.17.23",
"mime-types": "^2.1.35",
"path": "0.12.7",
"react-native-markdown-package": "1.8.2",
"react-native-url-polyfill": "^2.0.0",
- "stream-chat": "^9.27.2",
+ "stream-chat": "^9.35.1",
"use-sync-external-store": "^1.5.0"
},
"peerDependencies": {
@@ -91,9 +91,11 @@
"emoji-mart": ">=5.4.0",
"react-native": ">=0.73.0",
"react-native-gesture-handler": ">=2.18.0",
+ "react-native-keyboard-controller": ">=1.20.2",
"react-native-reanimated": ">=3.16.0",
"react-native-safe-area-context": ">=5.4.1",
- "react-native-svg": ">=15.8.0"
+ "react-native-svg": ">=15.8.0",
+ "react-native-teleport": ">=0.5.4"
},
"peerDependenciesMeta": {
"@op-engineering/op-sqlite": {
@@ -107,6 +109,9 @@
},
"@emoji-mart/data": {
"optional": true
+ },
+ "react-native-keyboard-controller": {
+ "optional": true
}
},
"devDependencies": {
@@ -154,9 +159,11 @@
"react-native": "0.80.2",
"react-native-builder-bob": "0.40.11",
"react-native-gesture-handler": "^2.26.0",
+ "react-native-keyboard-controller": "^1.20.2",
"react-native-reanimated": "3.18.0",
"react-native-safe-area-context": "^5.6.1",
"react-native-svg": "15.12.0",
+ "react-native-teleport": "^0.5.4",
"react-test-renderer": "19.1.0",
"rimraf": "^6.0.1",
"typescript": "5.8.3",
diff --git a/package/src/components/Attachment/Attachment.tsx b/package/src/components/Attachment/Attachment.tsx
index 07ab1d6cda..ff72ea1509 100644
--- a/package/src/components/Attachment/Attachment.tsx
+++ b/package/src/components/Attachment/Attachment.tsx
@@ -1,105 +1,116 @@
-import React from 'react';
+import React, { useMemo } from 'react';
-import type { Attachment as AttachmentType } from 'stream-chat';
+import { StyleSheet } from 'react-native';
+
+import {
+ isAudioAttachment,
+ isFileAttachment,
+ isImageAttachment,
+ isVideoAttachment,
+ isVoiceRecordingAttachment,
+ type Attachment as AttachmentType,
+} from 'stream-chat';
+
+import { AudioAttachment as AudioAttachmentDefault } from './Audio';
+
+import { URLPreview as URLPreviewDefault } from './UrlPreview';
-import { AttachmentActions as AttachmentActionsDefault } from '../../components/Attachment/AttachmentActions';
-import { Card as CardDefault } from '../../components/Attachment/Card';
import { FileAttachment as FileAttachmentDefault } from '../../components/Attachment/FileAttachment';
import { Gallery as GalleryDefault } from '../../components/Attachment/Gallery';
import { Giphy as GiphyDefault } from '../../components/Attachment/Giphy';
+
+import { useTheme } from '../../contexts';
+import {
+ MessageContextValue,
+ useMessageContext,
+} from '../../contexts/messageContext/MessageContext';
import {
MessagesContextValue,
useMessagesContext,
} from '../../contexts/messagesContext/MessagesContext';
-import { isVideoPlayerAvailable } from '../../native';
+import { isSoundPackageAvailable, isVideoPlayerAvailable } from '../../native';
+import { primitives } from '../../theme';
import { FileTypes } from '../../types/types';
export type ActionHandler = (name: string, value: string) => void;
export type AttachmentPropsWithContext = Pick<
MessagesContextValue,
- | 'AttachmentActions'
- | 'Card'
+ | 'AudioAttachment'
| 'FileAttachment'
| 'Gallery'
- | 'giphyVersion'
| 'Giphy'
| 'isAttachmentEqual'
| 'UrlPreview'
| 'myMessageTheme'
-> & {
- /**
- * The attachment to render
- */
- attachment: AttachmentType;
-};
+> &
+ Pick & {
+ /**
+ * The attachment to render
+ */
+ attachment: AttachmentType;
+ /**
+ * The index of the attachment in the message
+ */
+ index?: number;
+ };
const AttachmentWithContext = (props: AttachmentPropsWithContext) => {
const {
attachment,
- AttachmentActions,
- Card,
+ AudioAttachment,
FileAttachment,
Gallery,
Giphy,
- giphyVersion,
UrlPreview,
+ index,
+ message,
} = props;
-
- const hasAttachmentActions = !!attachment.actions?.length;
+ const audioAttachmentStyles = useAudioAttachmentStyles();
if (attachment.type === FileTypes.Giphy || attachment.type === FileTypes.Imgur) {
- return ;
+ return ;
}
if (attachment.og_scrape_url || attachment.title_link) {
- return ;
+ return ;
}
- if (attachment.type === FileTypes.Image) {
- return (
- <>
-
- {hasAttachmentActions && (
-
- )}
- >
- );
+ if (isImageAttachment(attachment)) {
+ return ;
}
- if (attachment.type === FileTypes.Video && !attachment.og_scrape_url) {
+ // The `!attachment.og_scrape_url` is added for cases, where the url preview is not an image but a video.
+ if (isVideoAttachment(attachment) && !attachment.og_scrape_url) {
return isVideoPlayerAvailable() ? (
- <>
-
- {hasAttachmentActions && (
-
- )}
- >
+
) : (
);
}
- if (
- attachment.type === FileTypes.File ||
- attachment.type === FileTypes.Audio ||
- attachment.type === FileTypes.VoiceRecording
- ) {
+ if (isAudioAttachment(attachment) || isVoiceRecordingAttachment(attachment)) {
+ if (isSoundPackageAvailable()) {
+ return (
+
+ );
+ }
return ;
}
- if (hasAttachmentActions) {
- return (
- <>
-
- {/** TODO: Please rethink this, the fix is temporary. */}
-
- >
- );
- } else {
- return ;
+ if (isFileAttachment(attachment)) {
+ return ;
}
+
+ // TODO: Handle custom attachments
+ return ;
};
const areEqual = (prevProps: AttachmentPropsWithContext, nextProps: AttachmentPropsWithContext) => {
@@ -137,21 +148,7 @@ const MemoizedAttachment = React.memo(
areEqual,
) as typeof AttachmentWithContext;
-export type AttachmentProps = Partial<
- Pick<
- MessagesContextValue,
- | 'AttachmentActions'
- | 'Card'
- | 'FileAttachment'
- | 'Gallery'
- | 'Giphy'
- | 'giphyVersion'
- | 'myMessageTheme'
- | 'UrlPreview'
- | 'isAttachmentEqual'
- >
-> &
- Pick;
+export type AttachmentProps = Partial;
/**
* Attachment - The message attachment
@@ -159,52 +156,46 @@ export type AttachmentProps = Partial<
export const Attachment = (props: AttachmentProps) => {
const {
attachment,
- AttachmentActions: PropAttachmentActions,
- Card: PropCard,
+ AudioAttachment: PropAudioAttachment,
FileAttachment: PropFileAttachment,
Gallery: PropGallery,
Giphy: PropGiphy,
- giphyVersion: PropGiphyVersion,
myMessageTheme: PropMyMessageTheme,
UrlPreview: PropUrlPreview,
} = props;
const {
- AttachmentActions: ContextAttachmentActions,
- Card: ContextCard,
+ AudioAttachment: ContextAudioAttachment,
FileAttachment: ContextFileAttachment,
Gallery: ContextGallery,
Giphy: ContextGiphy,
- giphyVersion: ContextGiphyVersion,
isAttachmentEqual,
myMessageTheme: ContextMyMessageTheme,
UrlPreview: ContextUrlPreview,
} = useMessagesContext();
+ const { message } = useMessageContext();
+
if (!attachment) {
return null;
}
- const AttachmentActions =
- PropAttachmentActions || ContextAttachmentActions || AttachmentActionsDefault;
- const Card = PropCard || ContextCard || CardDefault;
+ const AudioAttachment = PropAudioAttachment || ContextAudioAttachment || AudioAttachmentDefault;
const FileAttachment = PropFileAttachment || ContextFileAttachment || FileAttachmentDefault;
const Gallery = PropGallery || ContextGallery || GalleryDefault;
const Giphy = PropGiphy || ContextGiphy || GiphyDefault;
- const UrlPreview = PropUrlPreview || ContextUrlPreview || CardDefault;
- const giphyVersion = PropGiphyVersion || ContextGiphyVersion;
+ const UrlPreview = PropUrlPreview || ContextUrlPreview || URLPreviewDefault;
const myMessageTheme = PropMyMessageTheme || ContextMyMessageTheme;
return (
{
/>
);
};
+
+const useAudioAttachmentStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const { isMyMessage, messageHasOnlySingleAttachment } = useMessageContext();
+
+ const showBackgroundTransparent = messageHasOnlySingleAttachment;
+
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ paddingVertical: primitives.spacingXs,
+ paddingLeft: primitives.spacingXs,
+ paddingRight: primitives.spacingSm,
+ borderWidth: 0,
+ backgroundColor: showBackgroundTransparent
+ ? 'transparent'
+ : isMyMessage
+ ? semantics.chatBgAttachmentOutgoing
+ : semantics.chatBgAttachmentIncoming,
+ },
+ playPauseButton: {
+ borderColor: isMyMessage
+ ? semantics.chatBorderOnChatOutgoing
+ : semantics.chatBorderOnChatIncoming,
+ },
+ speedSettingsButton: {
+ borderColor: isMyMessage
+ ? semantics.chatBorderOnChatOutgoing
+ : semantics.chatBorderOnChatIncoming,
+ },
+ durationText: {
+ color: semantics.chatTextIncoming,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ },
+ });
+ }, [semantics, isMyMessage, showBackgroundTransparent]);
+};
diff --git a/package/src/components/Attachment/AttachmentActions.tsx b/package/src/components/Attachment/AttachmentActions.tsx
deleted file mode 100644
index a860772f15..0000000000
--- a/package/src/components/Attachment/AttachmentActions.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import React from 'react';
-import {
- StyleProp,
- StyleSheet,
- Text,
- TextStyle,
- TouchableOpacity,
- View,
- ViewStyle,
-} from 'react-native';
-
-import type { Attachment } from 'stream-chat';
-
-import {
- MessageContextValue,
- useMessageContext,
-} from '../../contexts/messageContext/MessageContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-
-const styles = StyleSheet.create({
- actionButton: {
- borderRadius: 20,
- borderWidth: 1,
- paddingHorizontal: 10,
- paddingVertical: 5,
- },
- container: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- padding: 5,
- },
-});
-
-export type AttachmentActionsPropsWithContext = Pick &
- Pick & {
- styles?: Partial<{
- actionButton: StyleProp;
- buttonText: StyleProp;
- container: StyleProp;
- }>;
- };
-
-const AttachmentActionsWithContext = (props: AttachmentActionsPropsWithContext) => {
- const { actions, handleAction, styles: stylesProp = {} } = props;
-
- const {
- theme: {
- colors: { accent_blue, black, border, transparent, white },
- messageSimple: {
- actions: {
- button: {
- defaultBackgroundColor,
- defaultBorderColor,
- primaryBackgroundColor,
- primaryBorderColor,
- ...buttonStyle
- },
- buttonText: { defaultColor, primaryColor, ...buttonTextStyle },
- container,
- },
- },
- },
- } = useTheme();
-
- return (
-
- {actions?.map((action, index) => {
- const primary = action.style === 'primary';
-
- return (
- {
- if (action.name && action.value && handleAction) {
- handleAction(action.name, action.value);
- }
- }}
- style={[
- styles.actionButton,
- {
- backgroundColor: primary
- ? primaryBackgroundColor || accent_blue
- : defaultBackgroundColor || white,
- borderColor: primary
- ? primaryBorderColor || border
- : defaultBorderColor || transparent,
- },
- buttonStyle,
- stylesProp.actionButton,
- ]}
- testID={`attachment-actions-button-${action.name}`}
- >
-
- {action.text}
-
-
- );
- })}
-
- );
-};
-
-const areEqual = (
- prevProps: AttachmentActionsPropsWithContext,
- nextProps: AttachmentActionsPropsWithContext,
-) => {
- const { actions: prevActions } = prevProps;
- const { actions: nextActions } = nextProps;
-
- const actionsEqual = prevActions?.length === nextActions?.length;
-
- return actionsEqual;
-};
-
-const MemoizedAttachmentActions = React.memo(
- AttachmentActionsWithContext,
- areEqual,
-) as typeof AttachmentActionsWithContext;
-
-export type AttachmentActionsProps = Attachment &
- Partial>;
-
-/**
- * AttachmentActions - The actions you can take on an attachment.
- * Actions in combination with attachments can be used to build [commands](https://getstream.io/chat/docs/#channel_commands).
- */
-export const AttachmentActions = (props: AttachmentActionsProps) => {
- const { handleAction } = useMessageContext();
- return ;
-};
-
-AttachmentActions.displayName = 'AttachmentActions{messageSimple{actions}}';
diff --git a/package/src/components/Attachment/AudioAttachment.tsx b/package/src/components/Attachment/Audio/AudioAttachment.tsx
similarity index 50%
rename from package/src/components/Attachment/AudioAttachment.tsx
rename to package/src/components/Attachment/Audio/AudioAttachment.tsx
index 016c787058..a465ec40f2 100644
--- a/package/src/components/Attachment/AudioAttachment.tsx
+++ b/package/src/components/Attachment/Audio/AudioAttachment.tsx
@@ -1,32 +1,35 @@
import React, { RefObject, useEffect, useMemo } from 'react';
-import { I18nManager, Pressable, StyleSheet, Text, View } from 'react-native';
+import { I18nManager, StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle } from 'react-native';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import {
+ isLocalVoiceRecordingAttachment,
isVoiceRecordingAttachment,
LocalMessage,
AudioAttachment as StreamAudioAttachment,
VoiceRecordingAttachment as StreamVoiceRecordingAttachment,
} from 'stream-chat';
-import { useTheme } from '../../contexts';
-import { useStateStore } from '../../hooks';
-import { useAudioPlayerControl } from '../../hooks/useAudioPlayerControl';
-import { Audio, Pause, Play } from '../../icons';
+import { PlayPauseButton } from './PlayPauseButton';
+
+import { useTheme } from '../../../contexts';
+import { useStateStore } from '../../../hooks';
+import { useAudioPlayer } from '../../../hooks/useAudioPlayer';
import {
NativeHandlers,
SoundReturnType,
VideoPayloadData,
VideoProgressData,
VideoSeekResponse,
-} from '../../native';
-import { AudioPlayerState } from '../../state-store/audio-player';
-import { AudioConfig } from '../../types/types';
-import { getTrimmedAttachmentTitle } from '../../utils/getTrimmedAttachmentTitle';
-import { ProgressControl } from '../ProgressControl/ProgressControl';
-import { WaveProgressBar } from '../ProgressControl/WaveProgressBar';
+} from '../../../native';
+import { AudioPlayerState } from '../../../state-store/audio-player';
+import { primitives } from '../../../theme';
+import { AudioConfig } from '../../../types/types';
+import { ProgressControl } from '../../ProgressControl/ProgressControl';
+import { WaveProgressBar } from '../../ProgressControl/WaveProgressBar';
+import { SpeedSettingsButton } from '../../ui/SpeedSettingsButton';
const ONE_HOUR_IN_MILLISECONDS = 3600 * 1000;
const ONE_SECOND_IN_MILLISECONDS = 1000;
@@ -45,8 +48,8 @@ export type AudioAttachmentType = AudioConfig &
export type AudioAttachmentProps = {
item: AudioAttachmentType;
message?: LocalMessage;
- titleMaxLength?: number;
hideProgressBar?: boolean;
+ showTitle?: boolean;
/**
* If true, the speed settings button will be shown.
*/
@@ -56,21 +59,14 @@ export type AudioAttachmentProps = {
* If true, the audio attachment is in preview mode in the message input.
*/
isPreview?: boolean;
- /**
- * Callback to be called when the audio is loaded
- * @deprecated This is deprecated and will be removed in the future.
- */
- onLoad?: (index: string, duration: number) => void;
- /**
- * Callback to be called when the audio is played or paused
- * @deprecated This is deprecated and will be removed in the future.
- */
- onPlayPause?: (index: string, pausedStatus?: boolean) => void;
- /**
- * Callback to be called when the audio progresses
- * @deprecated This is deprecated and will be removed in the future.
- */
- onProgress?: (index: string, progress: number) => void;
+ containerStyle?: StyleProp;
+ indicator?: React.ReactNode;
+ styles?: {
+ container?: StyleProp;
+ playPauseButton?: StyleProp;
+ speedSettingsButton?: StyleProp;
+ durationText?: StyleProp;
+ };
};
const audioPlayerSelector = (state: AudioPlayerState) => ({
@@ -87,19 +83,22 @@ const audioPlayerSelector = (state: AudioPlayerState) => ({
*/
export const AudioAttachment = (props: AudioAttachmentProps) => {
const soundRef = React.useRef(null);
-
+ const styles = useStyles();
const {
hideProgressBar = false,
item,
message,
showSpeedSettings = false,
+ showTitle = true,
testID,
- titleMaxLength,
isPreview = false,
+ containerStyle,
+ styles: stylesProps,
+ indicator,
} = props;
const isVoiceRecording = isVoiceRecordingAttachment(item);
- const audioPlayer = useAudioPlayerControl({
+ const audioPlayer = useAudioPlayer({
duration: item.duration ?? 0,
mimeType: item.mime_type ?? '',
requester: isPreview
@@ -180,15 +179,14 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
theme: {
audioAttachment: {
container,
+ centerContainer,
+ audioInfo,
leftContainer,
- playPauseButton,
progressControlContainer,
progressDurationText,
rightContainer,
- speedChangeButton,
- speedChangeButtonText,
},
- colors: { accent_blue, black, grey_dark, grey_whisper, static_black, static_white, white },
+ semantics,
messageInput: {
fileAttachmentUploadPreview: { filenameText },
},
@@ -208,79 +206,81 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
return (
-
- {!isPlaying ? (
-
- ) : (
-
- )}
-
+ containerStyle={stylesProps?.playPauseButton}
+ />
-
-
- {isVoiceRecordingAttachment(item)
- ? 'Recording'
- : getTrimmedAttachmentTitle(item.title, titleMaxLength)}
-
-
-
- {progressDuration}
+
+ {showTitle ? (
+
+ {isVoiceRecordingAttachment(item) || isLocalVoiceRecordingAttachment(item)
+ ? 'Voice Message'
+ : item.title}
- {!hideProgressBar && (
-
- {item.waveform_data ? (
-
- ) : (
-
- )}
-
- )}
-
+ ) : null}
+
+ {indicator ? (
+ indicator
+ ) : (
+
+
+ {progressDuration}
+
+ {!hideProgressBar && (
+
+ {item.waveform_data ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ )}
{NativeHandlers.Sound?.Player && (
{
/>
)}
- {showSpeedSettings ? (
+ {showSpeedSettings && !indicator ? (
- {!isPlaying ? (
-
- ) : (
-
- {`x${currentPlaybackRate.toFixed(1)}`}
-
- )}
+
) : null}
);
};
-const styles = StyleSheet.create({
- audioInfo: {
- alignItems: 'center',
- flexDirection: 'row',
- },
- centerContainer: {
- flexGrow: 1,
- },
- container: {
- alignItems: 'center',
- borderRadius: 12,
- borderWidth: 1,
- flex: 1,
- flexDirection: 'row',
- paddingLeft: 8,
- paddingRight: 16,
- paddingVertical: 12,
- },
- filenameText: {
- fontSize: 14,
- fontWeight: 'bold',
- marginBottom: 8,
- },
- leftContainer: {
- marginRight: 8,
- },
- playPauseButton: {
- alignItems: 'center',
- borderRadius: 50,
- elevation: 4,
- justifyContent: 'center',
- marginRight: 8,
- padding: 4,
- shadowOffset: {
- height: 2,
- width: 0,
- },
- shadowOpacity: 0.23,
- shadowRadius: 2.62,
- },
- progressControlContainer: {
- flexGrow: 1,
- justifyContent: 'center',
- },
- progressDurationText: {
- fontSize: 12,
- marginRight: 8,
- },
- rightContainer: {
- marginLeft: 16,
- },
- speedChangeButton: {
- alignItems: 'center',
- alignSelf: 'center',
- borderRadius: 50,
- elevation: 4,
- justifyContent: 'center',
- paddingHorizontal: 8,
- paddingVertical: 4,
- shadowOffset: {
- height: 2,
- width: 0,
- },
- shadowOpacity: 0.23,
- shadowRadius: 2.62,
- },
- speedChangeButtonText: {
- fontSize: 12,
- },
-});
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ padding: primitives.spacingSm,
+ gap: primitives.spacingXs,
+ minWidth: 256, // TODO: Fix this
+ borderColor: semantics.borderCoreDefault,
+ borderWidth: 1,
+ },
+ audioInfo: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: primitives.spacingXxs,
+ },
+ centerContainer: {
+ gap: primitives.spacingXxs,
+ },
+ filenameText: {
+ color: semantics.textPrimary,
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ leftContainer: {
+ padding: primitives.spacingXxs,
+ },
+ progressControlContainer: {
+ flex: 1,
+ },
+ progressDurationText: {
+ color: semantics.textPrimary,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ rightContainer: {},
+ });
+ }, [semantics]);
+};
AudioAttachment.displayName = 'AudioAttachment{messageInput{audioAttachment}}';
diff --git a/package/src/components/Attachment/Audio/PlayPauseButton.tsx b/package/src/components/Attachment/Audio/PlayPauseButton.tsx
new file mode 100644
index 0000000000..b64dc490e8
--- /dev/null
+++ b/package/src/components/Attachment/Audio/PlayPauseButton.tsx
@@ -0,0 +1,72 @@
+import React, { useMemo } from 'react';
+import { Pressable, PressableProps, StyleProp, StyleSheet, ViewStyle } from 'react-native';
+
+import { useTheme } from '../../../contexts';
+import { NewPause } from '../../../icons/NewPause';
+import { NewPlay } from '../../../icons/NewPlay';
+import { primitives } from '../../../theme';
+import { buttonSizes } from '../../ui/Button/constants';
+
+export type PlayPauseButtonProps = PressableProps & {
+ /**
+ * If true, the button will be playing.
+ */
+ isPlaying: boolean;
+ /**
+ * The function to be called when the button is pressed.
+ */
+ onPress: () => void;
+ /**
+ * The style of the container.
+ */
+ containerStyle?: StyleProp;
+};
+
+export const PlayPauseButton = ({
+ isPlaying,
+ onPress,
+ containerStyle,
+ ...rest
+}: PlayPauseButtonProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const styles = useStyles();
+ return (
+ [
+ styles.container,
+ {
+ backgroundColor: pressed ? semantics.backgroundCorePressed : 'transparent',
+ },
+ containerStyle,
+ ]}
+ {...rest}
+ >
+ {isPlaying ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ borderRadius: primitives.radiusMax,
+ ...buttonSizes.md,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderWidth: 1,
+ borderColor: semantics.chatBorderOnChatIncoming,
+ },
+ });
+ }, [semantics]);
+};
diff --git a/package/src/components/Attachment/Audio/index.ts b/package/src/components/Attachment/Audio/index.ts
new file mode 100644
index 0000000000..e1aadc4c14
--- /dev/null
+++ b/package/src/components/Attachment/Audio/index.ts
@@ -0,0 +1,2 @@
+export * from './AudioAttachment';
+export * from './PlayPauseButton';
diff --git a/package/src/components/Attachment/Card.tsx b/package/src/components/Attachment/Card.tsx
deleted file mode 100644
index ed8582bc9d..0000000000
--- a/package/src/components/Attachment/Card.tsx
+++ /dev/null
@@ -1,339 +0,0 @@
-import React from 'react';
-import {
- Image,
- ImageStyle,
- Pressable,
- StyleProp,
- StyleSheet,
- Text,
- TextStyle,
- View,
- ViewStyle,
-} from 'react-native';
-
-import type { Attachment } from 'stream-chat';
-
-import { openUrlSafely } from './utils/openUrlSafely';
-
-import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext';
-
-import {
- MessageContextValue,
- useMessageContext,
-} from '../../contexts/messageContext/MessageContext';
-import {
- MessagesContextValue,
- useMessagesContext,
-} from '../../contexts/messagesContext/MessagesContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { Play } from '../../icons/Play';
-import { FileTypes } from '../../types/types';
-import { makeImageCompatibleUrl } from '../../utils/utils';
-import { ImageBackground } from '../UIComponents/ImageBackground';
-
-const styles = StyleSheet.create({
- authorName: { fontSize: 14.5, fontWeight: '600' },
- authorNameContainer: {
- borderTopRightRadius: 15,
- paddingHorizontal: 8,
- paddingTop: 8,
- },
- authorNameFooter: {
- fontSize: 14.5,
- fontWeight: '600',
- padding: 8,
- },
- authorNameMask: {
- bottom: 0,
- left: 2,
- position: 'absolute',
- },
- cardCover: {
- alignItems: 'center',
- borderRadius: 8,
- height: 140,
- justifyContent: 'center',
- marginHorizontal: 2,
- },
- cardFooter: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- padding: 10,
- },
- container: {
- overflow: 'hidden',
- width: 256,
- },
- description: {
- fontSize: 12,
- marginHorizontal: 8,
- },
- playButtonStyle: {
- alignItems: 'center',
- borderRadius: 50,
- elevation: 2,
- height: 36,
- justifyContent: 'center',
- width: 36,
- },
- title: {
- fontSize: 12,
- marginHorizontal: 8,
- },
-});
-
-export type CardPropsWithContext = Attachment &
- Pick &
- Pick &
- Pick<
- MessagesContextValue,
- 'additionalPressableProps' | 'CardCover' | 'CardFooter' | 'CardHeader' | 'myMessageTheme'
- > & {
- channelId: string | undefined;
- messageId: string | undefined;
- styles?: Partial<{
- authorName: StyleProp;
- authorNameContainer: StyleProp;
- authorNameFooter: StyleProp;
- authorNameFooterContainer: StyleProp;
- authorNameMask: StyleProp;
- cardCover: StyleProp;
- cardFooter: StyleProp;
- container: StyleProp;
- description: StyleProp;
- title: StyleProp;
- }>;
- };
-
-const CardWithContext = (props: CardPropsWithContext) => {
- const {
- additionalPressableProps,
- author_name,
- CardCover,
- CardFooter,
- CardHeader,
- image_url,
- ImageComponent = Image,
- og_scrape_url,
- onLongPress,
- onPress,
- onPressIn,
- preventPress,
- styles: stylesProp = {},
- text,
- thumb_url,
- title,
- type,
- } = props;
-
- const {
- theme: {
- colors: { accent_blue, black, blue_alice, static_black, static_white, transparent },
- messageSimple: {
- card: {
- authorName,
- authorNameContainer,
- authorNameFooter,
- authorNameFooterContainer,
- authorNameMask,
- container,
- cover,
- footer: { description, title: titleStyle, ...footerStyle },
- noURI,
- playButtonStyle: { roundedView },
- playIcon: { height, width },
- },
- },
- },
- } = useTheme();
-
- const uri = image_url || thumb_url;
-
- const defaultOnPress = () => openUrlSafely(og_scrape_url || uri);
-
- const isVideoCard = type === FileTypes.Video && og_scrape_url;
-
- return (
- {
- if (onLongPress) {
- onLongPress({
- additionalInfo: { url: og_scrape_url },
- emitter: 'card',
- event,
- });
- }
- }}
- onPress={(event) => {
- if (onPress) {
- onPress({
- additionalInfo: { url: og_scrape_url },
- defaultHandler: defaultOnPress,
- emitter: 'card',
- event,
- });
- }
- }}
- onPressIn={(event) => {
- if (onPressIn) {
- onPressIn({
- additionalInfo: { url: og_scrape_url },
- defaultHandler: defaultOnPress,
- emitter: 'card',
- event,
- });
- }
- }}
- style={[styles.container, container, stylesProp.container]}
- testID='card-attachment'
- {...additionalPressableProps}
- >
- {CardHeader && }
- {CardCover && }
- {uri && !CardCover && (
-
-
- {isVideoCard ? (
-
-
-
- ) : null}
-
- {author_name && (
-
-
-
- {author_name}
-
-
-
- )}
-
- )}
- {CardFooter ? (
-
- ) : (
-
-
- {!uri && author_name && (
-
- {author_name}
-
- )}
- {title && (
-
- {title}
-
- )}
- {text && (
-
- {text}
-
- )}
-
-
- )}
-
- );
-};
-
-const areEqual = (prevProps: CardPropsWithContext, nextProps: CardPropsWithContext) => {
- const { myMessageTheme: prevMyMessageTheme } = prevProps;
- const { myMessageTheme: nextMyMessageTheme } = nextProps;
-
- const messageThemeEqual =
- JSON.stringify(prevMyMessageTheme) === JSON.stringify(nextMyMessageTheme);
- if (!messageThemeEqual) {
- return false;
- }
-
- return true;
-};
-
-const MemoizedCard = React.memo(CardWithContext, areEqual) as typeof CardWithContext;
-
-export type CardProps = Attachment &
- Partial<
- Pick &
- Pick &
- Pick<
- MessagesContextValue,
- 'additionalPressableProps' | 'CardCover' | 'CardFooter' | 'CardHeader'
- >
- >;
-
-/**
- * UI component for card in attachments.
- */
-export const Card = (props: CardProps) => {
- const { ImageComponent } = useChatContext();
- const { message, onLongPress, onPress, onPressIn, preventPress } = useMessageContext();
- const { additionalPressableProps, CardCover, CardFooter, CardHeader, myMessageTheme } =
- useMessagesContext();
-
- return (
-
- );
-};
-
-Card.displayName = 'Card{messageSimple{card}}';
diff --git a/package/src/components/Attachment/FileAttachment.tsx b/package/src/components/Attachment/FileAttachment.tsx
index a4b830f717..1d7f675eb1 100644
--- a/package/src/components/Attachment/FileAttachment.tsx
+++ b/package/src/components/Attachment/FileAttachment.tsx
@@ -1,11 +1,11 @@
-import React from 'react';
-import { Pressable, StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle } from 'react-native';
+import React, { useMemo } from 'react';
+import { Pressable, StyleProp, StyleSheet, TextStyle, ViewStyle } from 'react-native';
import type { Attachment } from 'stream-chat';
+import { FilePreview } from './FilePreview';
import { openUrlSafely } from './utils/openUrlSafely';
-import { AttachmentActions as AttachmentActionsDefault } from '../../components/Attachment/AttachmentActions';
import { FileIcon as FileIconDefault } from '../../components/Attachment/FileIcon';
import {
MessageContextValue,
@@ -16,38 +16,16 @@ import {
useMessagesContext,
} from '../../contexts/messagesContext/MessagesContext';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { useViewport } from '../../hooks/useViewport';
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- borderRadius: 12,
- flexDirection: 'row',
- padding: 8,
- },
- details: {
- paddingLeft: 16,
- },
- size: {
- fontSize: 12,
- },
- title: {
- fontSize: 14,
- fontWeight: '700',
- },
-});
export type FileAttachmentPropsWithContext = Pick<
MessageContextValue,
'onLongPress' | 'onPress' | 'onPressIn' | 'preventPress'
> &
- Pick<
- MessagesContextValue,
- 'additionalPressableProps' | 'AttachmentActions' | 'FileAttachmentIcon'
- > & {
+ Pick & {
/** The attachment to render */
attachment: Attachment;
- attachmentSize?: number;
+ attachmentIconSize?: number;
+ // TODO: Think we really need a way to style the file preview using props if we have theme.
styles?: Partial<{
container: StyleProp;
details: StyleProp;
@@ -57,29 +35,19 @@ export type FileAttachmentPropsWithContext = Pick<
};
const FileAttachmentWithContext = (props: FileAttachmentPropsWithContext) => {
+ const styles = useStyles();
+
const {
additionalPressableProps,
attachment,
- AttachmentActions,
- attachmentSize,
- FileAttachmentIcon,
+ attachmentIconSize,
onLongPress,
onPress,
onPressIn,
preventPress,
- styles: stylesProp = {},
+ styles: stylesProp = styles,
} = props;
- const {
- theme: {
- colors: { black, grey, white },
- messageSimple: {
- file: { container, details, fileSize, title },
- },
- },
- } = useTheme();
- const { vw } = useViewport();
-
const defaultOnPress = () => openUrlSafely(attachment.asset_url);
return (
@@ -117,27 +85,11 @@ const FileAttachmentWithContext = (props: FileAttachmentPropsWithContext) => {
testID='file-attachment'
{...additionalPressableProps}
>
-
-
-
-
- {attachment.title}
-
-
- {getFileSizeDisplayText(attachment.file_size)}
-
-
-
- {attachment.actions?.length ? : null}
+
);
};
@@ -147,17 +99,12 @@ export type FileAttachmentProps = Partial {
const { onLongPress, onPress, onPressIn, preventPress } = useMessageContext();
- const {
- additionalPressableProps,
- AttachmentActions = AttachmentActionsDefault,
- FileAttachmentIcon = FileIconDefault,
- } = useMessagesContext();
+ const { additionalPressableProps, FileAttachmentIcon = FileIconDefault } = useMessagesContext();
return (
{
);
};
-export const getFileSizeDisplayText = (size?: number | string) => {
- if (!size) {
- return;
- }
- if (typeof size === 'string') {
- size = parseFloat(size);
- }
-
- if (size < 1000 * 1000) {
- return `${Math.floor(Math.floor(size / 10) / 100)} KB`;
- }
+FileAttachment.displayName = 'FileAttachment{messageSimple{file}}';
- return `${Math.floor(Math.floor(size / 10000) / 100)} MB`;
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const { isMyMessage, messageHasOnlySingleAttachment } = useMessageContext();
+ const showBackgroundTransparent = messageHasOnlySingleAttachment;
+
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ backgroundColor: showBackgroundTransparent
+ ? 'transparent'
+ : isMyMessage
+ ? semantics.chatBgAttachmentOutgoing
+ : semantics.chatBgAttachmentIncoming,
+ },
+ });
+ }, [showBackgroundTransparent, isMyMessage, semantics]);
};
-
-FileAttachment.displayName = 'FileAttachment{messageSimple{file}}';
diff --git a/package/src/components/Attachment/FileAttachmentGroup.tsx b/package/src/components/Attachment/FileAttachmentGroup.tsx
index 50388bf696..43b1cc501b 100644
--- a/package/src/components/Attachment/FileAttachmentGroup.tsx
+++ b/package/src/components/Attachment/FileAttachmentGroup.tsx
@@ -1,8 +1,6 @@
-import React, { useEffect, useState } from 'react';
+import React from 'react';
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
-import { Attachment, isAudioAttachment, isVoiceRecordingAttachment } from 'stream-chat';
-
import { Attachment as AttachmentDefault } from './Attachment';
import {
@@ -15,101 +13,18 @@ import {
useMessagesContext,
} from '../../contexts/messagesContext/MessagesContext';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { isSoundPackageAvailable } from '../../native';
+import { primitives } from '../../theme';
export type FileAttachmentGroupPropsWithContext = Pick &
- Pick & {
- /**
- * @deprecated Use message instead
- * The unique id for the message with file attachments
- */
- messageId: string;
+ Pick & {
styles?: Partial<{
attachmentContainer: StyleProp;
container: StyleProp;
}>;
};
-type FilesToDisplayType = Attachment & {
- duration: number;
- paused: boolean;
- progress: number;
-};
-
const FileAttachmentGroupWithContext = (props: FileAttachmentGroupPropsWithContext) => {
- const { Attachment, AudioAttachment, files, message, styles: stylesProp = {} } = props;
-
- const [filesToDisplay, setFilesToDisplay] = useState(() =>
- files.map((file) => ({ ...file, duration: file.duration || 0, paused: true, progress: 0 })),
- );
-
- useEffect(() => {
- setFilesToDisplay(
- files.map((file) => ({ ...file, duration: file.duration || 0, paused: true, progress: 0 })),
- );
- }, [files]);
-
- /**
- * Handler triggered when an audio is loaded in the message input. The initial state is defined for the audio here and the duration is set.
- * @param index - The index of the audio
- * @param duration - The duration of the audio
- *
- * @deprecated This is deprecated and will be removed in the future.
- * FIXME: Remove this in the next major version.
- */
- const onLoad = (index: string, duration: number) => {
- setFilesToDisplay((prevFilesToDisplay) =>
- prevFilesToDisplay.map((fileToDisplay, id) => ({
- ...fileToDisplay,
- duration: id.toString() === index ? duration : fileToDisplay.duration,
- })),
- );
- };
-
- /**
- * Handler which is triggered when the audio progresses/ the thumb is dragged in the progress control. The progressed duration is set here.
- * @param index - The index of the audio
- * @param progress - The progress of the audio
- *
- * @deprecated This is deprecated and will be removed in the future.
- * FIXME: Remove this in the next major version.
- */
- const onProgress = (index: string, progress: number) => {
- setFilesToDisplay((prevFilesToDisplay) =>
- prevFilesToDisplay.map((filesToDisplay, id) => ({
- ...filesToDisplay,
- progress: id.toString() === index ? progress : filesToDisplay.progress,
- })),
- );
- };
-
- /**
- * Handler which controls or sets the paused/played state of the audio.
- * @param index - The index of the audio
- * @param pausedStatus - The paused status of the audio
- *
- * @deprecated This is deprecated and will be removed in the future.
- * FIXME: Remove this in the next major version.
- */
- const onPlayPause = (index: string, pausedStatus?: boolean) => {
- if (pausedStatus === false) {
- // If the status is false we set the audio with the index as playing and the others as paused.
- setFilesToDisplay((prevFilesToDisplay) =>
- prevFilesToDisplay.map((fileToDisplay, id) => ({
- ...fileToDisplay,
- paused: id.toString() !== index,
- })),
- );
- } else {
- // If the status is true we simply set all the audio's paused state as true.
- setFilesToDisplay((prevFilesToDisplay) =>
- prevFilesToDisplay.map((fileToDisplay) => ({
- ...fileToDisplay,
- paused: true,
- })),
- );
- }
- };
+ const { Attachment, files, message, styles: stylesProp = {} } = props;
const {
theme: {
@@ -120,29 +35,13 @@ const FileAttachmentGroupWithContext = (props: FileAttachmentGroupPropsWithConte
} = useTheme();
return (
-
- {filesToDisplay.map((file, index) => (
+
+ {files.map((file, index) => (
- {(isAudioAttachment(file) || isVoiceRecordingAttachment(file)) &&
- isSoundPackageAvailable() ? (
-
- ) : (
-
- )}
+
))}
@@ -153,8 +52,13 @@ const areEqual = (
prevProps: FileAttachmentGroupPropsWithContext,
nextProps: FileAttachmentGroupPropsWithContext,
) => {
- const { files: prevFiles } = prevProps;
- const { files: nextFiles } = nextProps;
+ const { files: prevFiles, message: prevMessage } = prevProps;
+ const { files: nextFiles, message: nextMessage } = nextProps;
+
+ const messageEqual = prevMessage?.id === nextMessage?.id;
+ if (!messageEqual) {
+ return false;
+ }
return prevFiles.length === nextFiles.length;
};
@@ -164,13 +68,10 @@ const MemoizedFileAttachmentGroup = React.memo(
areEqual,
) as typeof FileAttachmentGroupWithContext;
-export type FileAttachmentGroupProps = Partial<
- Omit
-> &
- Pick;
+export type FileAttachmentGroupProps = Partial;
export const FileAttachmentGroup = (props: FileAttachmentGroupProps) => {
- const { files: propFiles, messageId } = props;
+ const { files: propFiles } = props;
const { files: contextFiles, message } = useMessageContext();
@@ -189,7 +90,6 @@ export const FileAttachmentGroup = (props: FileAttachmentGroupProps) => {
AudioAttachment,
files,
message,
- messageId,
}}
/>
);
@@ -197,7 +97,12 @@ export const FileAttachmentGroup = (props: FileAttachmentGroupProps) => {
const styles = StyleSheet.create({
container: {
- padding: 4,
+ alignItems: 'center',
+ gap: primitives.spacingXs,
+ },
+ item: {
+ borderRadius: primitives.radiusLg,
+ overflow: 'hidden',
},
});
diff --git a/package/src/components/Attachment/FilePreview.tsx b/package/src/components/Attachment/FilePreview.tsx
new file mode 100644
index 0000000000..a872b35e21
--- /dev/null
+++ b/package/src/components/Attachment/FilePreview.tsx
@@ -0,0 +1,104 @@
+import React, { useMemo } from 'react';
+import { StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle } from 'react-native';
+
+import type { Attachment } from 'stream-chat';
+
+import { FileIcon as FileIconDefault } from '../../components/Attachment/FileIcon';
+import {
+ MessagesContextValue,
+ useMessagesContext,
+} from '../../contexts/messagesContext/MessagesContext';
+import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { primitives } from '../../theme';
+import { getDurationLabelFromDuration, getFileSizeDisplayText } from '../../utils/utils';
+
+export type FilePreviewProps = Partial> & {
+ /** The attachment to render */
+ attachment: Attachment;
+ attachmentIconSize?: number;
+ // TODO: Think we really need a way to style the file preview using props if we have theme.
+ styles?: Partial<{
+ container: StyleProp;
+ details: StyleProp;
+ size: StyleProp;
+ title: StyleProp;
+ }>;
+ titleNumberOfLines?: number;
+ indicator?: React.ReactNode;
+};
+
+export const FilePreview = (props: FilePreviewProps) => {
+ const {
+ attachment,
+ attachmentIconSize,
+ styles: stylesProp = {},
+ titleNumberOfLines = 2,
+ indicator,
+ } = props;
+ const { FileAttachmentIcon = FileIconDefault } = useMessagesContext();
+
+ const styles = useStyles();
+
+ const {
+ theme: {
+ messageSimple: {
+ file: { container, details, fileSize, title },
+ },
+ },
+ } = useTheme();
+
+ return (
+
+
+
+
+ {attachment.title}
+
+ {indicator ? (
+ indicator
+ ) : (
+
+ {attachment.duration
+ ? getDurationLabelFromDuration(attachment.duration)
+ : getFileSizeDisplayText(attachment.file_size)}
+
+ )}
+
+
+ );
+};
+
+FilePreview.displayName = 'FilePreview{messageSimple{file}}';
+
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ padding: primitives.spacingSm,
+ gap: primitives.spacingSm,
+ width: 256, // TODO: Fix this
+ },
+ details: {
+ flexShrink: 1,
+ gap: primitives.spacingXxs,
+ },
+ size: {
+ color: semantics.textPrimary,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ title: {
+ color: semantics.textPrimary,
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ });
+ }, [semantics]);
+};
diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx
index 7da851ded1..ff41031538 100644
--- a/package/src/components/Attachment/Gallery.tsx
+++ b/package/src/components/Attachment/Gallery.tsx
@@ -7,7 +7,10 @@ import { GalleryImage } from './GalleryImage';
import { buildGallery } from './utils/buildGallery/buildGallery';
import type { Thumbnail } from './utils/buildGallery/types';
-import { getGalleryImageBorderRadius } from './utils/getGalleryImageBorderRadius';
+import {
+ GalleryImageBorderRadius,
+ getGalleryImageBorderRadius,
+} from './utils/getGalleryImageBorderRadius';
import { openUrlSafely } from './utils/openUrlSafely';
@@ -33,29 +36,25 @@ import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { useLoadingImage } from '../../hooks/useLoadingImage';
import { isVideoPlayerAvailable } from '../../native';
+import { primitives } from '../../theme';
import { FileTypes } from '../../types/types';
import { getUrlWithoutParams } from '../../utils/utils';
-export type GalleryPropsWithContext = Pick<
- ImageGalleryContextValue,
- 'setSelectedMessage' | 'setMessages'
-> &
+export type GalleryPropsWithContext = Pick &
Pick<
MessageContextValue,
- | 'alignment'
- | 'groupStyles'
| 'images'
| 'videos'
| 'onLongPress'
| 'onPress'
| 'onPressIn'
| 'preventPress'
- | 'threadList'
+ | 'message'
+ | 'messageContentOrder'
> &
Pick<
MessagesContextValue,
| 'additionalPressableProps'
- | 'legacyImageViewerSwipeBehaviour'
| 'VideoThumbnail'
| 'ImageLoadingIndicator'
| 'ImageLoadingFailedIndicator'
@@ -64,44 +63,26 @@ export type GalleryPropsWithContext = Pick<
> &
Pick & {
channelId: string | undefined;
- hasThreadReplies?: boolean;
- /**
- * `message` prop has been introduced here as part of `legacyImageViewerSwipeBehaviour` prop.
- * https://github.com/GetStream/stream-chat-react-native/commit/d5eac6193047916f140efe8e396a671675c9a63f
- * messageId and messageText may seem redundant now, but to avoid breaking change as part
- * of minor release, we are keeping those props.
- *
- * Also `message` type should ideally be imported from MessageContextValue and not be explicitely mentioned
- * here, but due to some circular dependencies within the SDK, it causes "excessive deep nesting" issue with
- * typescript within Channel component. We should take it as a mini-project and resolve all these circular imports.
- *
- * TODO: Fix circular dependencies of imports
- */
- message?: LocalMessage;
+ messageHasOnlyOneImage: boolean;
};
const GalleryWithContext = (props: GalleryPropsWithContext) => {
const {
additionalPressableProps,
- alignment,
- groupStyles,
- hasThreadReplies,
+ imageGalleryStateStore,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
ImageReloadIndicator,
images,
- legacyImageViewerSwipeBehaviour,
message,
onLongPress,
onPress,
onPressIn,
preventPress,
- setMessages,
setOverlay,
- setSelectedMessage,
- threadList,
videos,
VideoThumbnail,
+ messageHasOnlyOneImage = false,
} = props;
const { resizableCDNHosts } = useChatConfigContext();
@@ -122,6 +103,8 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
},
} = useTheme();
+ const styles = useStyles();
+
const sizeConfig = {
gridHeight,
gridWidth,
@@ -155,12 +138,16 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
return (
{
{
>
{rows.map((thumbnail, rowIndex) => {
const borderRadius = getGalleryImageBorderRadius({
- alignment,
colIndex,
- groupStyles,
- hasThreadReplies,
height,
invertedDirections,
- messageText: message?.text,
numOfColumns,
numOfRows,
rowIndex,
sizeConfig,
- threadList,
width,
+ messageHasOnlyOneImage,
});
- if (message === undefined) {
+ if (!message) {
return null;
}
@@ -204,13 +188,13 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
additionalPressableProps={additionalPressableProps}
borderRadius={borderRadius}
colIndex={colIndex}
+ imageGalleryStateStore={imageGalleryStateStore}
ImageLoadingFailedIndicator={ImageLoadingFailedIndicator}
ImageLoadingIndicator={ImageLoadingIndicator}
ImageReloadIndicator={ImageReloadIndicator}
imagesAndVideos={imagesAndVideos}
invertedDirections={invertedDirections || false}
key={rowIndex}
- legacyImageViewerSwipeBehaviour={legacyImageViewerSwipeBehaviour}
message={message}
numOfColumns={numOfColumns}
numOfRows={numOfRows}
@@ -219,9 +203,7 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
onPressIn={onPressIn}
preventPress={preventPress}
rowIndex={rowIndex}
- setMessages={setMessages}
setOverlay={setOverlay}
- setSelectedMessage={setSelectedMessage}
thumbnail={thumbnail}
VideoThumbnail={VideoThumbnail}
/>
@@ -235,12 +217,7 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
};
type GalleryThumbnailProps = {
- borderRadius: {
- borderBottomLeftRadius: number;
- borderBottomRightRadius: number;
- borderTopLeftRadius: number;
- borderTopRightRadius: number;
- };
+ borderRadius: GalleryImageBorderRadius;
colIndex: number;
imagesAndVideos: Attachment[];
invertedDirections: boolean;
@@ -252,13 +229,12 @@ type GalleryThumbnailProps = {
} & Pick<
MessagesContextValue,
| 'additionalPressableProps'
- | 'legacyImageViewerSwipeBehaviour'
| 'VideoThumbnail'
| 'ImageLoadingIndicator'
| 'ImageLoadingFailedIndicator'
| 'ImageReloadIndicator'
> &
- Pick &
+ Pick &
Pick &
Pick;
@@ -266,12 +242,12 @@ const GalleryThumbnail = ({
additionalPressableProps,
borderRadius,
colIndex,
+ imageGalleryStateStore,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
ImageReloadIndicator,
imagesAndVideos,
invertedDirections,
- legacyImageViewerSwipeBehaviour,
message,
numOfColumns,
numOfRows,
@@ -280,41 +256,30 @@ const GalleryThumbnail = ({
onPressIn,
preventPress,
rowIndex,
- setMessages,
setOverlay,
- setSelectedMessage,
thumbnail,
VideoThumbnail,
}: GalleryThumbnailProps) => {
const {
theme: {
- colors: { overlay },
messageSimple: {
- gallery: {
- image,
- imageBorderRadius,
- imageContainer,
- imageContainerStyle,
- moreImagesContainer,
- moreImagesText,
- },
+ gallery: { image, imageBorderRadius, imageContainer, moreImagesContainer, moreImagesText },
},
+ semantics,
},
} = useTheme();
const { t } = useTranslationContext();
+ const styles = useStyles();
const openImageViewer = () => {
- if (!legacyImageViewerSwipeBehaviour && message) {
- // Added if-else to keep the logic readable, instead of DRY.
- // if - legacyImageViewerSwipeBehaviour is disabled
- // else - legacyImageViewerSwipeBehaviour is enabled
- setMessages([message]);
- setSelectedMessage({ messageId: message.id, url: thumbnail.url });
- setOverlay('gallery');
- } else if (legacyImageViewerSwipeBehaviour) {
- setSelectedMessage({ messageId: message?.id, url: thumbnail.url });
- setOverlay('gallery');
+ if (!message) {
+ return;
}
+ imageGalleryStateStore.openImageGallery({
+ messages: [message],
+ selectedAttachmentUrl: thumbnail.url,
+ });
+ setOverlay('gallery');
};
const defaultOnPress = () => {
@@ -366,9 +331,8 @@ const GalleryThumbnail = ({
style={({ pressed }) => [
styles.imageContainer,
{
- height: thumbnail.height,
opacity: pressed ? 0.8 : 1,
- width: thumbnail.width,
+ flex: thumbnail.flex,
},
imageContainer,
]}
@@ -377,33 +341,25 @@ const GalleryThumbnail = ({
>
{thumbnail.type === FileTypes.Video ? (
) : (
-
-
-
+
)}
{colIndex === numOfColumns - 1 && rowIndex === numOfRows - 1 && imagesAndVideos.length > 4 ? (
@@ -444,16 +400,10 @@ const GalleryImageThumbnail = ({
},
} = useTheme();
+ const styles = useStyles();
+
return (
-
+
{isLoadingImageError ? (
<>
@@ -473,14 +423,7 @@ const GalleryImageThumbnail = ({
onLoadEnd={() => setTimeout(() => setLoadingImage(false), 0)}
onLoadStart={() => setLoadingImage(true)}
resizeMode={thumbnail.resizeMode}
- style={[
- borderRadius,
- {
- height: thumbnail.height - 1,
- width: thumbnail.width - 1,
- },
- gallery.image,
- ]}
+ style={[borderRadius, gallery.image]}
uri={thumbnail.url}
/>
{isLoadingImage && (
@@ -496,16 +439,12 @@ const GalleryImageThumbnail = ({
const areEqual = (prevProps: GalleryPropsWithContext, nextProps: GalleryPropsWithContext) => {
const {
- groupStyles: prevGroupStyles,
- hasThreadReplies: prevHasThreadReplies,
images: prevImages,
message: prevMessage,
myMessageTheme: prevMyMessageTheme,
videos: prevVideos,
} = prevProps;
const {
- groupStyles: nextGroupStyles,
- hasThreadReplies: nextHasThreadReplies,
images: nextImages,
message: nextMessage,
myMessageTheme: nextMyMessageTheme,
@@ -519,17 +458,6 @@ const areEqual = (prevProps: GalleryPropsWithContext, nextProps: GalleryPropsWit
return false;
}
- const groupStylesEqual =
- prevGroupStyles.length === nextGroupStyles.length && prevGroupStyles[0] === nextGroupStyles[0];
- if (!groupStylesEqual) {
- return false;
- }
-
- const hasThreadRepliesEqual = prevHasThreadReplies === nextHasThreadReplies;
- if (!hasThreadRepliesEqual) {
- return false;
- }
-
const imagesEqual =
prevImages.length === nextImages.length &&
prevImages.every(
@@ -571,9 +499,6 @@ export type GalleryProps = Partial;
export const Gallery = (props: GalleryProps) => {
const {
additionalPressableProps: propAdditionalPressableProps,
- alignment: propAlignment,
- groupStyles: propGroupStyles,
- hasThreadReplies,
ImageLoadingFailedIndicator: PropImageLoadingFailedIndicator,
ImageLoadingIndicator: PropImageLoadingIndicator,
ImageReloadIndicator: PropImageReloadIndicator,
@@ -585,31 +510,27 @@ export const Gallery = (props: GalleryProps) => {
onPressIn: propOnPressIn,
preventPress: propPreventPress,
setOverlay: propSetOverlay,
- setSelectedMessage: propSetSelectedMessage,
- threadList: propThreadList,
videos: propVideos,
VideoThumbnail: PropVideoThumbnail,
+ messageContentOrder: propMessageContentOrder,
} = props;
- const { setMessages, setSelectedMessage: contextSetSelectedMessage } = useImageGalleryContext();
+ const { imageGalleryStateStore } = useImageGalleryContext();
const {
- alignment: contextAlignment,
- groupStyles: contextGroupStyles,
images: contextImages,
message: contextMessage,
onLongPress: contextOnLongPress,
onPress: contextOnPress,
onPressIn: contextOnPressIn,
preventPress: contextPreventPress,
- threadList: contextThreadList,
videos: contextVideos,
+ messageContentOrder: contextMessageContentOrder,
} = useMessageContext();
const {
additionalPressableProps: contextAdditionalPressableProps,
ImageLoadingFailedIndicator: ContextImageLoadingFailedIndicator,
ImageLoadingIndicator: ContextImageLoadingIndicator,
ImageReloadIndicator: ContextImageReloadIndicator,
- legacyImageViewerSwipeBehaviour,
myMessageTheme: contextMyMessageTheme,
VideoThumbnail: ContextVideoThumnbnail,
} = useMessagesContext();
@@ -624,97 +545,109 @@ export const Gallery = (props: GalleryProps) => {
}
const additionalPressableProps = propAdditionalPressableProps || contextAdditionalPressableProps;
- const alignment = propAlignment || contextAlignment;
- const groupStyles = propGroupStyles || contextGroupStyles;
const onLongPress = propOnLongPress || contextOnLongPress;
const onPressIn = propOnPressIn || contextOnPressIn;
const onPress = propOnPress || contextOnPress;
const preventPress =
typeof propPreventPress === 'boolean' ? propPreventPress : contextPreventPress;
- const setSelectedMessage = propSetSelectedMessage || contextSetSelectedMessage;
const setOverlay = propSetOverlay || contextSetOverlay;
- const threadList = propThreadList || contextThreadList;
const VideoThumbnail = PropVideoThumbnail || ContextVideoThumnbnail;
const ImageLoadingFailedIndicator =
PropImageLoadingFailedIndicator || ContextImageLoadingFailedIndicator;
const ImageLoadingIndicator = PropImageLoadingIndicator || ContextImageLoadingIndicator;
const ImageReloadIndicator = PropImageReloadIndicator || ContextImageReloadIndicator;
const myMessageTheme = propMyMessageTheme || contextMyMessageTheme;
+ const messageContentOrder = propMessageContentOrder || contextMessageContentOrder;
+
+ const messageHasOnlyOneImage =
+ messageContentOrder?.length === 1 &&
+ messageContentOrder?.includes('gallery') &&
+ images.length === 1;
return (
);
};
-const styles = StyleSheet.create({
- errorTextSize: { fontSize: 10 },
- galleryContainer: {
- borderTopLeftRadius: 13,
- borderTopRightRadius: 13,
- flexDirection: 'row',
- flexWrap: 'wrap',
- overflow: 'hidden',
- },
- imageContainer: {
- alignItems: 'center',
- display: 'flex',
- flexDirection: 'row',
- justifyContent: 'center',
- padding: 1,
- },
- imageContainerStyle: { alignItems: 'center', flex: 1, justifyContent: 'center' },
- imageLoadingErrorIndicatorStyle: {
- bottom: 4,
- left: 4,
- position: 'absolute',
- },
- imageLoadingIndicatorContainer: {
- height: '100%',
- justifyContent: 'center',
- position: 'absolute',
- width: '100%',
- },
- imageLoadingIndicatorStyle: {
- alignItems: 'center',
- justifyContent: 'center',
- position: 'absolute',
- },
- imageReloadContainerStyle: {
- ...StyleSheet.absoluteFillObject,
- alignItems: 'center',
- justifyContent: 'center',
- },
- moreImagesContainer: {
- alignItems: 'center',
- justifyContent: 'center',
- margin: 1,
- },
- moreImagesText: { color: '#FFFFFF', fontSize: 26, fontWeight: '700' },
-});
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ errorTextSize: {
+ fontSize: primitives.typographyFontSizeXs,
+ lineHeight: primitives.typographyLineHeightTight,
+ fontWeight: primitives.typographyFontWeightRegular,
+ color: semantics.accentError,
+ },
+ galleryItemColumn: {
+ gap: primitives.spacingXxs,
+ flex: 1,
+ },
+ container: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ gap: primitives.spacingXxs,
+ },
+ imageContainer: {},
+ image: {
+ flex: 1,
+ },
+ imageLoadingErrorIndicatorStyle: {
+ bottom: 4,
+ left: 4,
+ position: 'absolute',
+ },
+ imageLoadingIndicatorContainer: {
+ height: '100%',
+ justifyContent: 'center',
+ position: 'absolute',
+ width: '100%',
+ },
+ imageLoadingIndicatorStyle: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ position: 'absolute',
+ },
+ imageReloadContainerStyle: {
+ ...StyleSheet.absoluteFillObject,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ moreImagesContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ moreImagesText: {
+ color: semantics.textOnAccent,
+ fontSize: primitives.typographyFontSize2xl,
+ lineHeight: primitives.typographyLineHeightRelaxed,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ },
+ });
+ }, [semantics]);
+};
Gallery.displayName = 'Gallery{messageSimple{gallery}}';
diff --git a/package/src/components/Attachment/GalleryImage.tsx b/package/src/components/Attachment/GalleryImage.tsx
index 75328eece0..f8e252c98f 100644
--- a/package/src/components/Attachment/GalleryImage.tsx
+++ b/package/src/components/Attachment/GalleryImage.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Image, ImageProps } from 'react-native';
+import { Image, ImageProps, StyleSheet } from 'react-native';
import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext';
@@ -18,6 +18,7 @@ export const GalleryImageWithContext = (props: GalleryImageWithContextProps) =>
{
return ;
};
+
+const styles = StyleSheet.create({
+ image: {
+ flex: 1,
+ },
+});
diff --git a/package/src/components/Attachment/Giphy.tsx b/package/src/components/Attachment/Giphy.tsx
deleted file mode 100644
index 8b71abb187..0000000000
--- a/package/src/components/Attachment/Giphy.tsx
+++ /dev/null
@@ -1,490 +0,0 @@
-import React from 'react';
-import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
-
-import type { Attachment } from 'stream-chat';
-
-import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext';
-
-import {
- ImageGalleryContextValue,
- useImageGalleryContext,
-} from '../../contexts/imageGalleryContext/ImageGalleryContext';
-import {
- MessageContextValue,
- useMessageContext,
-} from '../../contexts/messageContext/MessageContext';
-import {
- MessagesContextValue,
- useMessagesContext,
-} from '../../contexts/messagesContext/MessagesContext';
-import {
- OverlayContextValue,
- useOverlayContext,
-} from '../../contexts/overlayContext/OverlayContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { useLoadingImage } from '../../hooks/useLoadingImage';
-import { GiphyIcon, GiphyLightning } from '../../icons';
-
-import { makeImageCompatibleUrl } from '../../utils/utils';
-
-const styles = StyleSheet.create({
- actionsRow: {
- flexDirection: 'row',
- },
- buttonContainer: {
- alignItems: 'center',
- borderTopWidth: 1,
- flex: 1,
- justifyContent: 'center',
- },
- cancel: {
- fontSize: 14,
- fontWeight: '600',
- paddingVertical: 16,
- },
- centerChild: {
- alignItems: 'center',
- display: 'flex',
- justifyContent: 'center',
- },
- container: {
- borderRadius: 16,
- overflow: 'hidden',
- width: 270,
- },
- giphy: {
- alignSelf: 'center',
- borderRadius: 2,
- height: 170,
- maxWidth: 270,
- width: 270,
- },
- giphyContainer: {
- alignItems: 'center',
- borderRadius: 12,
- flexDirection: 'row',
- justifyContent: 'center',
- paddingHorizontal: 8,
- paddingVertical: 4,
- },
- giphyHeaderText: {
- fontSize: 16,
- fontWeight: '600',
- marginHorizontal: 8,
- },
- giphyHeaderTitle: {
- fontSize: 14,
- },
- giphyMask: {
- bottom: 8,
- left: 8,
- position: 'absolute',
- },
- giphyMaskText: {
- fontSize: 13,
- fontWeight: '600',
- },
- header: {
- alignItems: 'center',
- display: 'flex',
- flexDirection: 'row',
- padding: 8,
- },
- imageErrorIndicatorStyle: {
- alignItems: 'center',
- flex: 1,
- justifyContent: 'center',
- },
- imageLoadingIndicatorStyle: {
- alignItems: 'center',
- flex: 1,
- justifyContent: 'center',
- position: 'absolute',
- },
- selectionContainer: {
- borderBottomRightRadius: 0,
- borderRadius: 16,
- borderWidth: 1,
- overflow: 'hidden',
- width: 272,
- },
- selectionImageContainer: {
- alignSelf: 'center',
- margin: 1,
- },
- send: {
- fontSize: 14,
- fontWeight: '600',
- paddingVertical: 16,
- },
- shuffle: {
- fontSize: 14,
- fontWeight: '600',
- paddingVertical: 16,
- },
- shuffleButton: {
- alignItems: 'center',
- borderRadius: 16,
- borderWidth: 1,
- height: 32,
- justifyContent: 'center',
- width: 32,
- },
-});
-
-export type GiphyPropsWithContext = Pick<
- ImageGalleryContextValue,
- 'setSelectedMessage' | 'setMessages'
-> &
- Pick<
- MessageContextValue,
- | 'handleAction'
- | 'isMyMessage'
- | 'message'
- | 'onLongPress'
- | 'onPress'
- | 'onPressIn'
- | 'preventPress'
- > &
- Pick &
- Pick<
- MessagesContextValue,
- | 'giphyVersion'
- | 'additionalPressableProps'
- | 'ImageLoadingIndicator'
- | 'ImageLoadingFailedIndicator'
- > & {
- attachment: Attachment;
- } & Pick;
-
-const GiphyWithContext = (props: GiphyPropsWithContext) => {
- const {
- additionalPressableProps,
- attachment,
- giphyVersion,
- handleAction,
- ImageComponent = Image,
- ImageLoadingFailedIndicator,
- ImageLoadingIndicator,
- isMyMessage,
- message,
- onLongPress,
- onPress,
- onPressIn,
- preventPress,
- setMessages,
- setOverlay,
- setSelectedMessage,
- } = props;
-
- const { actions, giphy: giphyData, image_url, thumb_url, title, type } = attachment;
-
- const { isLoadingImage, isLoadingImageError, setLoadingImage, setLoadingImageError } =
- useLoadingImage();
-
- const {
- theme: {
- colors: { accent_blue, black, grey, grey_dark, grey_gainsboro, label_bg_transparent, white },
- messageSimple: {
- giphy: {
- buttonContainer,
- cancel,
- container,
- giphy,
- giphyContainer,
- giphyHeaderText,
- giphyHeaderTitle,
- giphyMask,
- giphyMaskText,
- header,
- selectionContainer,
- send,
- shuffle,
- },
- },
- },
- } = useTheme();
-
- let uri = image_url || thumb_url;
- const giphyDimensions: { height?: number; width?: number } = {};
-
- const defaultOnPress = () => {
- setMessages([message]);
- setSelectedMessage({ messageId: message.id, url: uri });
- setOverlay('gallery');
- };
-
- if (type === 'giphy' && giphyData) {
- const giphyVersionInfo = giphyData[giphyVersion];
- uri = giphyVersionInfo.url;
- giphyDimensions.height = parseFloat(giphyVersionInfo.height);
- giphyDimensions.width = parseFloat(giphyVersionInfo.width);
- }
-
- if (!uri) {
- return null;
- }
-
- return actions ? (
-
-
-
- Giphy
- {`/giphy ${title}`}
-
-
- {
- console.warn(error);
- setLoadingImage(false);
- setLoadingImageError(true);
- }}
- onLoadEnd={() => setLoadingImage(false)}
- onLoadStart={() => setLoadingImage(true)}
- resizeMode='contain'
- source={{ uri: makeImageCompatibleUrl(uri) }}
- style={[styles.giphy, giphyDimensions, giphy]}
- />
-
-
-
- {
- if (actions?.[2].name && actions?.[2].value && handleAction) {
- handleAction(actions[2].name, actions[2].value);
- }
- }}
- style={[
- styles.buttonContainer,
- { borderColor: grey_gainsboro, borderRightWidth: 1 },
- buttonContainer,
- ]}
- testID={`${actions?.[2].value}-action-button`}
- >
- {actions?.[2].text}
-
- {
- if (actions?.[1].name && actions?.[1].value && handleAction) {
- handleAction(actions[1].name, actions[1].value);
- }
- }}
- style={[
- styles.buttonContainer,
- { borderColor: grey_gainsboro, borderRightWidth: 1 },
- buttonContainer,
- ]}
- testID={`${actions?.[1].value}-action-button`}
- >
- {actions?.[1].text}
-
- {
- if (actions?.[0].name && actions?.[0].value && handleAction) {
- handleAction(actions[0].name, actions[0].value);
- }
- }}
- style={[styles.buttonContainer, { borderColor: grey_gainsboro }, buttonContainer]}
- testID={`${actions?.[0].value}-action-button`}
- >
- {actions?.[0].text}
-
-
-
-
- ) : (
- {
- if (onLongPress) {
- onLongPress({
- emitter: 'giphy',
- event,
- });
- }
- }}
- onPress={(event) => {
- if (onPress) {
- onPress({
- defaultHandler: defaultOnPress,
- emitter: 'giphy',
- event,
- });
- }
- }}
- onPressIn={(event) => {
- if (onPressIn) {
- onPressIn({
- emitter: 'giphy',
- event,
- });
- }
- }}
- testID='giphy-attachment'
- {...additionalPressableProps}
- >
-
- {
- console.warn(error);
- setLoadingImage(false);
- setLoadingImageError(true);
- }}
- onLoadEnd={() => setLoadingImage(false)}
- onLoadStart={() => setLoadingImage(true)}
- resizeMode='contain'
- source={{ uri: makeImageCompatibleUrl(uri) }}
- style={[styles.giphy, giphyDimensions, giphy]}
- testID='giphy-attachment-image'
- />
-
- {isLoadingImageError && (
-
-
-
- )}
- {isLoadingImage && (
-
-
-
- )}
-
-
-
-
- {type?.toUpperCase()}
-
-
-
-
-
- );
-};
-
-const areEqual = (prevProps: GiphyPropsWithContext, nextProps: GiphyPropsWithContext) => {
- const {
- attachment: { actions: prevActions, image_url: prevImageUrl, thumb_url: prevThumbUrl },
- giphyVersion: prevGiphyVersion,
- isMyMessage: prevIsMyMessage,
- message: prevMessage,
- } = prevProps;
- const {
- attachment: { actions: nextActions, image_url: nextImageUrl, thumb_url: nextThumbUrl },
- giphyVersion: nextGiphyVersion,
- isMyMessage: nextIsMyMessage,
- message: nextMessage,
- } = nextProps;
-
- const imageUrlEqual = prevImageUrl === nextImageUrl;
- if (!imageUrlEqual) {
- return false;
- }
-
- const thumbUrlEqual = prevThumbUrl === nextThumbUrl;
- if (!thumbUrlEqual) {
- return false;
- }
-
- const actionsEqual =
- Array.isArray(prevActions) === Array.isArray(nextActions) &&
- ((Array.isArray(prevActions) &&
- Array.isArray(nextActions) &&
- prevActions.length === nextActions.length) ||
- prevActions === nextActions);
- if (!actionsEqual) {
- return false;
- }
-
- const giphyVersionEqual = prevGiphyVersion === nextGiphyVersion;
- if (!giphyVersionEqual) {
- return false;
- }
-
- const isMyMessageEqual = prevIsMyMessage === nextIsMyMessage;
- if (!isMyMessageEqual) {
- return false;
- }
-
- const messageEqual =
- prevMessage?.id === nextMessage?.id &&
- `${prevMessage?.updated_at}` === `${nextMessage?.updated_at}`;
-
- if (!messageEqual) {
- return false;
- }
- return true;
-};
-
-const MemoizedGiphy = React.memo(GiphyWithContext, areEqual) as typeof GiphyWithContext;
-
-export type GiphyProps = Partial & {
- attachment: Attachment;
-};
-
-/**
- * UI component for card in attachments.
- */
-export const Giphy = (props: GiphyProps) => {
- const { handleAction, isMyMessage, message, onLongPress, onPress, onPressIn, preventPress } =
- useMessageContext();
- const { ImageComponent } = useChatContext();
- const { additionalPressableProps, giphyVersion } = useMessagesContext();
- const { setMessages, setSelectedMessage } = useImageGalleryContext();
- const { setOverlay } = useOverlayContext();
-
- const {
- ImageLoadingFailedIndicator: ContextImageLoadingFailedIndicator,
- ImageLoadingIndicator: ContextImageLoadingIndicator,
- } = useMessagesContext();
- const ImageLoadingFailedIndicator =
- ContextImageLoadingFailedIndicator || props.ImageLoadingFailedIndicator;
- const ImageLoadingIndicator = ContextImageLoadingIndicator || props.ImageLoadingIndicator;
-
- return (
-
- );
-};
-
-Giphy.displayName = 'Giphy{messageSimple{giphy}}';
diff --git a/package/src/components/Attachment/Giphy/Giphy.tsx b/package/src/components/Attachment/Giphy/Giphy.tsx
new file mode 100644
index 0000000000..0ce1a45788
--- /dev/null
+++ b/package/src/components/Attachment/Giphy/Giphy.tsx
@@ -0,0 +1,298 @@
+import React, { useMemo } from 'react';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import type { Attachment } from 'stream-chat';
+
+import { GiphyImage } from './GiphyImage';
+
+import { ChatContextValue, useChatContext } from '../../../contexts/chatContext/ChatContext';
+
+import {
+ ImageGalleryContextValue,
+ useImageGalleryContext,
+} from '../../../contexts/imageGalleryContext/ImageGalleryContext';
+import {
+ MessageContextValue,
+ useMessageContext,
+} from '../../../contexts/messageContext/MessageContext';
+import {
+ MessagesContextValue,
+ useMessagesContext,
+} from '../../../contexts/messagesContext/MessagesContext';
+import {
+ OverlayContextValue,
+ useOverlayContext,
+} from '../../../contexts/overlayContext/OverlayContext';
+import { useTheme } from '../../../contexts/themeContext/ThemeContext';
+import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
+
+import { NewEyeOpen } from '../../../icons/NewEyeOpen';
+import { primitives } from '../../../theme';
+
+export type GiphyPropsWithContext = Pick &
+ Pick<
+ MessageContextValue,
+ 'handleAction' | 'message' | 'onLongPress' | 'onPress' | 'onPressIn' | 'preventPress'
+ > &
+ Pick &
+ Pick & {
+ attachment: Attachment;
+ } & Pick;
+
+const GiphyWithContext = (props: GiphyPropsWithContext) => {
+ const {
+ additionalPressableProps,
+ attachment,
+ giphyVersion,
+ handleAction,
+ imageGalleryStateStore,
+ message,
+ onLongPress,
+ onPress,
+ onPressIn,
+ preventPress,
+ setOverlay,
+ } = props;
+
+ const { actions, image_url, thumb_url } = attachment;
+
+ const { t } = useTranslationContext();
+
+ const {
+ theme: {
+ messageSimple: {
+ giphy: {
+ actionButtonContainer,
+ actionButton,
+ actionButtonText,
+ container,
+ giphyHeaderText,
+ header,
+ },
+ },
+ semantics,
+ },
+ } = useTheme();
+
+ const styles = useStyles();
+
+ const uri = image_url || thumb_url;
+
+ const defaultOnPress = () => {
+ if (!uri) {
+ return;
+ }
+ imageGalleryStateStore.openImageGallery({ messages: [message], selectedAttachmentUrl: uri });
+ setOverlay('gallery');
+ };
+
+ if (!uri) {
+ return null;
+ }
+
+ return actions ? (
+
+
+
+ {t('Only visible to you')}
+
+
+
+ {actions.map((action) => {
+ return (
+ {
+ if (action?.name && action?.value && handleAction) {
+ handleAction(action.name, action.value);
+ }
+ }}
+ style={[styles.actionButton, actionButton]}
+ testID={`${action.value}-action-button`}
+ >
+
+ {action.text}
+
+
+ );
+ })}
+
+
+ ) : (
+ {
+ if (onLongPress) {
+ onLongPress({
+ emitter: 'giphy',
+ event,
+ });
+ }
+ }}
+ onPress={(event) => {
+ if (onPress) {
+ onPress({
+ defaultHandler: defaultOnPress,
+ emitter: 'giphy',
+ event,
+ });
+ }
+ }}
+ onPressIn={(event) => {
+ if (onPressIn) {
+ onPressIn({
+ emitter: 'giphy',
+ event,
+ });
+ }
+ }}
+ testID='giphy-attachment'
+ style={styles.container}
+ {...additionalPressableProps}
+ >
+
+
+ );
+};
+
+const areEqual = (prevProps: GiphyPropsWithContext, nextProps: GiphyPropsWithContext) => {
+ const {
+ attachment: { actions: prevActions, image_url: prevImageUrl, thumb_url: prevThumbUrl },
+ giphyVersion: prevGiphyVersion,
+ message: prevMessage,
+ } = prevProps;
+ const {
+ attachment: { actions: nextActions, image_url: nextImageUrl, thumb_url: nextThumbUrl },
+ giphyVersion: nextGiphyVersion,
+ message: nextMessage,
+ } = nextProps;
+
+ const imageUrlEqual = prevImageUrl === nextImageUrl;
+ if (!imageUrlEqual) {
+ return false;
+ }
+
+ const thumbUrlEqual = prevThumbUrl === nextThumbUrl;
+ if (!thumbUrlEqual) {
+ return false;
+ }
+
+ const actionsEqual =
+ Array.isArray(prevActions) === Array.isArray(nextActions) &&
+ ((Array.isArray(prevActions) &&
+ Array.isArray(nextActions) &&
+ prevActions.length === nextActions.length) ||
+ prevActions === nextActions);
+ if (!actionsEqual) {
+ return false;
+ }
+
+ const giphyVersionEqual = prevGiphyVersion === nextGiphyVersion;
+ if (!giphyVersionEqual) {
+ return false;
+ }
+
+ const messageEqual =
+ prevMessage?.id === nextMessage?.id &&
+ `${prevMessage?.updated_at}` === `${nextMessage?.updated_at}`;
+
+ if (!messageEqual) {
+ return false;
+ }
+ return true;
+};
+
+const MemoizedGiphy = React.memo(GiphyWithContext, areEqual) as typeof GiphyWithContext;
+
+export type GiphyProps = Partial & {
+ attachment: Attachment;
+};
+
+/**
+ * UI component for card in attachments.
+ */
+export const Giphy = (props: GiphyProps) => {
+ const { handleAction, message, onLongPress, onPress, onPressIn, preventPress } =
+ useMessageContext();
+ const { ImageComponent } = useChatContext();
+ const { additionalPressableProps, giphyVersion } = useMessagesContext();
+ const { imageGalleryStateStore } = useImageGalleryContext();
+ const { setOverlay } = useOverlayContext();
+
+ return (
+
+ );
+};
+
+Giphy.displayName = 'Giphy{messageSimple{giphy}}';
+
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ backgroundColor: semantics.chatBgOutgoing,
+ borderRadius: primitives.radiusLg,
+ maxWidth: 256, // TODO: Not sure how to fix this
+ overflow: 'hidden',
+ },
+ actionButtonContainer: {
+ flexDirection: 'row',
+ gap: primitives.spacingXs,
+ },
+ actionButton: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ paddingVertical: primitives.spacingSm,
+ },
+ actionButtonText: {
+ fontSize: primitives.typographyFontSizeMd,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightNormal,
+ color: semantics.buttonSecondaryText,
+ },
+ header: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ paddingHorizontal: primitives.spacingSm,
+ paddingVertical: primitives.spacingXs,
+ gap: primitives.spacingXs,
+ },
+ headerText: {
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ });
+ }, [semantics]);
+};
diff --git a/package/src/components/Attachment/Giphy/GiphyImage.tsx b/package/src/components/Attachment/Giphy/GiphyImage.tsx
new file mode 100644
index 0000000000..d3f4dc551a
--- /dev/null
+++ b/package/src/components/Attachment/Giphy/GiphyImage.tsx
@@ -0,0 +1,205 @@
+import React, { useMemo } from 'react';
+import { Image, StyleSheet, View } from 'react-native';
+
+import type { Attachment } from 'stream-chat';
+
+import { ChatContextValue, useChatContext } from '../../../contexts/chatContext/ChatContext';
+
+import {
+ MessagesContextValue,
+ useMessagesContext,
+} from '../../../contexts/messagesContext/MessagesContext';
+import { useTheme } from '../../../contexts/themeContext/ThemeContext';
+import { useLoadingImage } from '../../../hooks/useLoadingImage';
+
+import { makeImageCompatibleUrl } from '../../../utils/utils';
+import { GiphyBadge } from '../../ui/Badge/GiphyBadge';
+
+export type GiphyImagePropsWithContext = Pick &
+ Pick<
+ MessagesContextValue,
+ 'giphyVersion' | 'ImageLoadingIndicator' | 'ImageLoadingFailedIndicator'
+ > & {
+ attachment: Attachment;
+ /**
+ * Whether to render the preview image or the full image
+ */
+ preview?: boolean;
+ };
+
+const GiphyImageWithContext = (props: GiphyImagePropsWithContext) => {
+ const {
+ attachment,
+ giphyVersion,
+ ImageComponent = Image,
+ ImageLoadingFailedIndicator,
+ ImageLoadingIndicator,
+ preview = false,
+ } = props;
+
+ const { giphy: giphyData, image_url, thumb_url, type } = attachment;
+
+ const { isLoadingImage, isLoadingImageError, setLoadingImage, setLoadingImageError } =
+ useLoadingImage();
+
+ const {
+ theme: {
+ messageSimple: {
+ giphy: { giphyMask, giphy, imageIndicatorContainer },
+ },
+ },
+ } = useTheme();
+
+ const styles = useStyles();
+
+ const onLoadStart = () => {
+ setLoadingImage(true);
+ setLoadingImageError(false);
+ };
+
+ const onLoadEnd = () => {
+ setLoadingImage(false);
+ };
+
+ const onError = () => {
+ setLoadingImage(false);
+ setLoadingImageError(true);
+ };
+
+ let uri = image_url || thumb_url;
+ const giphyDimensions: { height?: number; width?: number } = {};
+
+ if (type === 'giphy' && giphyData) {
+ const giphyVersionInfo = giphyData[giphyVersion];
+ uri = giphyVersionInfo.url;
+ giphyDimensions.height = parseFloat(giphyVersionInfo.height);
+ giphyDimensions.width = parseFloat(giphyVersionInfo.width);
+ }
+
+ if (!uri) {
+ return null;
+ }
+
+ return (
+
+
+ {isLoadingImageError && (
+
+
+
+ )}
+ {isLoadingImage && (
+
+
+
+ )}
+
+
+
+
+ );
+};
+
+const areEqual = (prevProps: GiphyImagePropsWithContext, nextProps: GiphyImagePropsWithContext) => {
+ const {
+ attachment: { actions: prevActions, image_url: prevImageUrl, thumb_url: prevThumbUrl },
+ giphyVersion: prevGiphyVersion,
+ } = prevProps;
+ const {
+ attachment: { actions: nextActions, image_url: nextImageUrl, thumb_url: nextThumbUrl },
+ giphyVersion: nextGiphyVersion,
+ } = nextProps;
+
+ const imageUrlEqual = prevImageUrl === nextImageUrl;
+ if (!imageUrlEqual) {
+ return false;
+ }
+
+ const thumbUrlEqual = prevThumbUrl === nextThumbUrl;
+ if (!thumbUrlEqual) {
+ return false;
+ }
+
+ const actionsEqual =
+ Array.isArray(prevActions) === Array.isArray(nextActions) &&
+ ((Array.isArray(prevActions) &&
+ Array.isArray(nextActions) &&
+ prevActions.length === nextActions.length) ||
+ prevActions === nextActions);
+ if (!actionsEqual) {
+ return false;
+ }
+
+ const giphyVersionEqual = prevGiphyVersion === nextGiphyVersion;
+ if (!giphyVersionEqual) {
+ return false;
+ }
+
+ return true;
+};
+
+const MemoizedGiphyImage = React.memo(
+ GiphyImageWithContext,
+ areEqual,
+) as typeof GiphyImageWithContext;
+
+export type GiphyImageProps = Partial & {
+ attachment: Attachment;
+};
+
+/**
+ * UI component for card in attachments.
+ */
+export const GiphyImage = (props: GiphyImageProps) => {
+ const { ImageComponent } = useChatContext();
+ const { giphyVersion } = useMessagesContext();
+
+ const {
+ ImageLoadingFailedIndicator: ContextImageLoadingFailedIndicator,
+ ImageLoadingIndicator: ContextImageLoadingIndicator,
+ } = useMessagesContext();
+ const ImageLoadingFailedIndicator =
+ ContextImageLoadingFailedIndicator || props.ImageLoadingFailedIndicator;
+ const ImageLoadingIndicator = ContextImageLoadingIndicator || props.ImageLoadingIndicator;
+
+ return (
+
+ );
+};
+
+GiphyImage.displayName = 'GiphyImage';
+
+const useStyles = () => {
+ return useMemo(() => {
+ return StyleSheet.create({
+ giphyMask: {
+ bottom: 8,
+ left: 8,
+ position: 'absolute',
+ },
+ giphy: {
+ alignSelf: 'center',
+ },
+ imageContainer: {},
+ imageIndicatorContainer: {
+ ...StyleSheet.absoluteFillObject,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ });
+ }, []);
+};
diff --git a/package/src/components/Attachment/Giphy/index.ts b/package/src/components/Attachment/Giphy/index.ts
new file mode 100644
index 0000000000..0085c597ef
--- /dev/null
+++ b/package/src/components/Attachment/Giphy/index.ts
@@ -0,0 +1,2 @@
+export * from './Giphy';
+export * from './GiphyImage';
diff --git a/package/src/components/Attachment/UrlPreview/URLPreview.tsx b/package/src/components/Attachment/UrlPreview/URLPreview.tsx
new file mode 100644
index 0000000000..08d19af8e3
--- /dev/null
+++ b/package/src/components/Attachment/UrlPreview/URLPreview.tsx
@@ -0,0 +1,293 @@
+import React, { useMemo } from 'react';
+import {
+ Image,
+ ImageStyle,
+ Pressable,
+ StyleProp,
+ StyleSheet,
+ Text,
+ TextStyle,
+ View,
+ ViewStyle,
+} from 'react-native';
+
+import type { Attachment } from 'stream-chat';
+
+import { ChatContextValue, useChatContext } from '../../../contexts/chatContext/ChatContext';
+
+import {
+ MessageContextValue,
+ useMessageContext,
+} from '../../../contexts/messageContext/MessageContext';
+import {
+ MessagesContextValue,
+ useMessagesContext,
+} from '../../../contexts/messagesContext/MessagesContext';
+import { useTheme } from '../../../contexts/themeContext/ThemeContext';
+import { NewLink } from '../../../icons/NewLink';
+import { primitives } from '../../../theme';
+import { FileTypes } from '../../../types/types';
+import { makeImageCompatibleUrl } from '../../../utils/utils';
+import { VideoPlayIndicator } from '../../ui';
+import { ImageBackground } from '../../UIComponents/ImageBackground';
+import { openUrlSafely } from '../utils/openUrlSafely';
+
+export type URLPreviewPropsWithContext = Pick &
+ Pick &
+ Pick<
+ MessagesContextValue,
+ 'additionalPressableProps' | 'myMessageTheme' | 'isAttachmentEqual'
+ > & {
+ attachment: Attachment;
+ channelId: string | undefined;
+ messageId: string | undefined;
+ // TODO: Think of a better way to handle styles
+ styles?: Partial<{
+ cardCover: StyleProp;
+ cardFooter: StyleProp;
+ container: StyleProp;
+ description: StyleProp;
+ linkPreview: StyleProp;
+ linkPreviewText: StyleProp;
+ title: StyleProp;
+ }>;
+ };
+
+const URLPreviewWithContext = (props: URLPreviewPropsWithContext) => {
+ const {
+ attachment,
+ additionalPressableProps,
+ ImageComponent = Image,
+ onLongPress,
+ onPress,
+ onPressIn,
+ preventPress,
+ styles: stylesProp = {},
+ } = props;
+
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ const { image_url, og_scrape_url, text, thumb_url, title, type } = attachment;
+
+ const {
+ theme: {
+ messageSimple: {
+ card: {
+ container,
+ cover,
+ footer: { description, title: titleStyle, ...footerStyle },
+ linkPreview,
+ linkPreviewText,
+ },
+ },
+ },
+ } = useTheme();
+
+ const styles = useStyles();
+
+ const uri = image_url || thumb_url;
+
+ const defaultOnPress = () => openUrlSafely(og_scrape_url || uri);
+
+ const isVideoCard = type === FileTypes.Video && og_scrape_url;
+
+ return (
+ {
+ if (onLongPress) {
+ onLongPress({
+ additionalInfo: { url: og_scrape_url },
+ emitter: 'card',
+ event,
+ });
+ }
+ }}
+ onPress={(event) => {
+ if (onPress) {
+ onPress({
+ additionalInfo: { url: og_scrape_url },
+ defaultHandler: defaultOnPress,
+ emitter: 'card',
+ event,
+ });
+ }
+ }}
+ onPressIn={(event) => {
+ if (onPressIn) {
+ onPressIn({
+ additionalInfo: { url: og_scrape_url },
+ defaultHandler: defaultOnPress,
+ emitter: 'card',
+ event,
+ });
+ }
+ }}
+ style={[styles.container, container, stylesProp.container]}
+ testID='card-attachment'
+ {...additionalPressableProps}
+ >
+ {uri && (
+
+ {isVideoCard ? : null}
+
+ )}
+
+
+ {title ? (
+
+ {title}
+
+ ) : null}
+ {text ? (
+
+ {text}
+
+ ) : null}
+
+
+
+ {og_scrape_url}
+
+
+
+
+ );
+};
+
+const areEqual = (prevProps: URLPreviewPropsWithContext, nextProps: URLPreviewPropsWithContext) => {
+ const {
+ attachment: prevAttachment,
+ myMessageTheme: prevMyMessageTheme,
+ isAttachmentEqual,
+ } = prevProps;
+ const { attachment: nextAttachment, myMessageTheme: nextMyMessageTheme } = nextProps;
+ const attachmentEqual =
+ prevAttachment.image_url === nextAttachment.image_url &&
+ prevAttachment.thumb_url === nextAttachment.thumb_url &&
+ prevAttachment.type === nextAttachment.type &&
+ prevAttachment.og_scrape_url === nextAttachment.og_scrape_url &&
+ prevAttachment.text === nextAttachment.text &&
+ prevAttachment.title === nextAttachment.title;
+ if (!attachmentEqual) {
+ return false;
+ }
+
+ if (isAttachmentEqual) {
+ return isAttachmentEqual(prevAttachment, nextAttachment);
+ }
+
+ const messageThemeEqual =
+ JSON.stringify(prevMyMessageTheme) === JSON.stringify(nextMyMessageTheme);
+ if (!messageThemeEqual) {
+ return false;
+ }
+
+ return true;
+};
+
+const MemoizedURLPreview = React.memo(
+ URLPreviewWithContext,
+ areEqual,
+) as typeof URLPreviewWithContext;
+
+export type URLPreviewProps = Partial & {
+ attachment: Attachment;
+};
+
+/**
+ * UI component for card in attachments.
+ */
+export const URLPreview = (props: URLPreviewProps) => {
+ const { ImageComponent } = useChatContext();
+ const { message, onLongPress, onPress, onPressIn, preventPress } = useMessageContext();
+ const { additionalPressableProps, isAttachmentEqual, myMessageTheme } = useMessagesContext();
+
+ return (
+
+ );
+};
+
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const { isMyMessage } = useMessageContext();
+
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ container: {
+ maxWidth: 256, // TODO: Fix this
+ borderRadius: primitives.radiusLg,
+ backgroundColor: isMyMessage
+ ? semantics.chatBgAttachmentOutgoing
+ : semantics.chatBgAttachmentIncoming,
+ overflow: 'hidden',
+ },
+ cardCover: {
+ minWidth: 256,
+ minHeight: 144,
+ alignSelf: 'stretch',
+ },
+ cardFooter: {
+ justifyContent: 'space-between',
+ gap: primitives.spacingXxs,
+ padding: primitives.spacingSm,
+ },
+ title: {
+ color: semantics.chatTextIncoming,
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ description: {
+ color: semantics.chatTextIncoming,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ linkPreview: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: primitives.spacingXxs,
+ },
+ linkPreviewText: {
+ color: semantics.chatTextIncoming,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightTight,
+ flexShrink: 1,
+ },
+ }),
+ [isMyMessage, semantics],
+ );
+};
+
+URLPreview.displayName = 'URLPreview{messageSimple{card}}';
diff --git a/package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx b/package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx
new file mode 100644
index 0000000000..425944ce0c
--- /dev/null
+++ b/package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx
@@ -0,0 +1,302 @@
+import React, { useMemo } from 'react';
+import {
+ Image,
+ ImageStyle,
+ Pressable,
+ StyleProp,
+ StyleSheet,
+ Text,
+ TextStyle,
+ View,
+ ViewStyle,
+} from 'react-native';
+
+import type { Attachment } from 'stream-chat';
+
+import { ChatContextValue, useChatContext } from '../../../contexts/chatContext/ChatContext';
+
+import {
+ MessageContextValue,
+ useMessageContext,
+} from '../../../contexts/messageContext/MessageContext';
+import {
+ MessagesContextValue,
+ useMessagesContext,
+} from '../../../contexts/messagesContext/MessagesContext';
+import { useTheme } from '../../../contexts/themeContext/ThemeContext';
+import { NewLink } from '../../../icons/NewLink';
+import { primitives } from '../../../theme';
+import { FileTypes } from '../../../types/types';
+import { makeImageCompatibleUrl } from '../../../utils/utils';
+import { VideoPlayIndicator } from '../../ui';
+import { ImageBackground } from '../../UIComponents/ImageBackground';
+import { openUrlSafely } from '../utils/openUrlSafely';
+
+export type URLPreviewCompactPropsWithContext = Pick &
+ Pick &
+ Pick & {
+ attachment: Attachment;
+ channelId: string | undefined;
+ messageId: string | undefined;
+ styles?: Partial<{
+ cardCover: StyleProp;
+ cardFooter: StyleProp;
+ container: StyleProp;
+ description: StyleProp;
+ linkPreview: StyleProp;
+ linkPreviewText: StyleProp;
+ title: StyleProp;
+ }>;
+ };
+
+const URLPreviewCompactWithContext = (props: URLPreviewCompactPropsWithContext) => {
+ const {
+ attachment,
+ additionalPressableProps,
+ ImageComponent = Image,
+ onLongPress,
+ onPress,
+ onPressIn,
+ preventPress,
+ styles: stylesProp = {},
+ } = props;
+
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ const { image_url, og_scrape_url, text, thumb_url, title: titleText, type } = attachment;
+
+ const {
+ theme: {
+ messageSimple: {
+ compactUrlPreview: {
+ wrapper,
+ container,
+ cardCover,
+ cardFooter,
+ title,
+ description,
+ linkPreview,
+ linkPreviewText,
+ },
+ },
+ },
+ } = useTheme();
+
+ const styles = useStyles();
+
+ const uri = image_url || thumb_url;
+
+ const defaultOnPress = () => openUrlSafely(og_scrape_url || uri);
+
+ const isVideoCard = type === FileTypes.Video && og_scrape_url;
+
+ return (
+
+ {
+ if (onLongPress) {
+ onLongPress({
+ additionalInfo: { url: og_scrape_url },
+ emitter: 'card',
+ event,
+ });
+ }
+ }}
+ onPress={(event) => {
+ if (onPress) {
+ onPress({
+ additionalInfo: { url: og_scrape_url },
+ defaultHandler: defaultOnPress,
+ emitter: 'card',
+ event,
+ });
+ }
+ }}
+ onPressIn={(event) => {
+ if (onPressIn) {
+ onPressIn({
+ additionalInfo: { url: og_scrape_url },
+ defaultHandler: defaultOnPress,
+ emitter: 'card',
+ event,
+ });
+ }
+ }}
+ style={[styles.container, container, stylesProp.container]}
+ testID='card-attachment'
+ {...additionalPressableProps}
+ >
+ {uri && (
+
+ {isVideoCard ? : null}
+
+ )}
+
+ {title ? (
+
+ {titleText}
+
+ ) : null}
+ {text ? (
+
+ {text}
+
+ ) : null}
+
+
+
+ {og_scrape_url}
+
+
+
+
+
+ );
+};
+
+const areEqual = (
+ prevProps: URLPreviewCompactPropsWithContext,
+ nextProps: URLPreviewCompactPropsWithContext,
+) => {
+ const { attachment: prevAttachment, myMessageTheme: prevMyMessageTheme } = prevProps;
+ const { attachment: nextAttachment, myMessageTheme: nextMyMessageTheme } = nextProps;
+ const attachmentEqual =
+ prevAttachment.image_url === nextAttachment.image_url &&
+ prevAttachment.thumb_url === nextAttachment.thumb_url &&
+ prevAttachment.type === nextAttachment.type &&
+ prevAttachment.og_scrape_url === nextAttachment.og_scrape_url &&
+ prevAttachment.text === nextAttachment.text &&
+ prevAttachment.title === nextAttachment.title;
+ if (!attachmentEqual) {
+ return false;
+ }
+
+ const messageThemeEqual =
+ JSON.stringify(prevMyMessageTheme) === JSON.stringify(nextMyMessageTheme);
+ if (!messageThemeEqual) {
+ return false;
+ }
+
+ return true;
+};
+
+const MemoizedURLPreviewCompact = React.memo(
+ URLPreviewCompactWithContext,
+ areEqual,
+) as typeof URLPreviewCompactWithContext;
+
+export type URLPreviewCompactProps = Partial & {
+ attachment: Attachment;
+};
+
+/**
+ * UI component for card in attachments.
+ */
+export const URLPreviewCompact = (props: URLPreviewCompactProps) => {
+ const { ImageComponent } = useChatContext();
+ const { message, onLongPress, onPress, onPressIn, preventPress } = useMessageContext();
+ const { additionalPressableProps, myMessageTheme } = useMessagesContext();
+
+ return (
+
+ );
+};
+
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const { isMyMessage } = useMessageContext();
+
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ wrapper: {
+ paddingHorizontal: primitives.spacingXs,
+ paddingTop: primitives.spacingXs,
+ },
+ container: {
+ maxWidth: 256, // TODO: Fix this
+ borderRadius: primitives.radiusLg,
+ backgroundColor: isMyMessage
+ ? semantics.chatBgAttachmentOutgoing
+ : semantics.chatBgAttachmentIncoming,
+ overflow: 'hidden',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: primitives.spacingXs,
+ paddingLeft: primitives.spacingXs,
+ paddingRight: primitives.spacingSm,
+ paddingVertical: primitives.spacingXs,
+ },
+ cardCover: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ aspectRatio: 1 / 1,
+ height: 40,
+ width: 40,
+ borderRadius: primitives.radiusMd,
+ },
+ cardFooter: {
+ justifyContent: 'space-between',
+ flexShrink: 1,
+ },
+ title: {
+ color: semantics.chatTextIncoming,
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ description: {
+ color: semantics.chatTextIncoming,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ linkPreview: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: primitives.spacingXxs,
+ },
+ linkPreviewText: {
+ color: semantics.chatTextIncoming,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightTight,
+ flexShrink: 1,
+ },
+ }),
+ [isMyMessage, semantics],
+ );
+};
+
+URLPreviewCompact.displayName = 'URLPreviewCompact{messageSimple{urlPreviewCompact}}';
diff --git a/package/src/components/Attachment/UrlPreview/index.ts b/package/src/components/Attachment/UrlPreview/index.ts
new file mode 100644
index 0000000000..1ce8299804
--- /dev/null
+++ b/package/src/components/Attachment/UrlPreview/index.ts
@@ -0,0 +1,2 @@
+export * from './URLPreviewCompact';
+export * from './URLPreview';
diff --git a/package/src/components/Attachment/VideoThumbnail.tsx b/package/src/components/Attachment/VideoThumbnail.tsx
index ef0ceb7891..7c2a19f079 100644
--- a/package/src/components/Attachment/VideoThumbnail.tsx
+++ b/package/src/components/Attachment/VideoThumbnail.tsx
@@ -1,33 +1,15 @@
import React from 'react';
-import { ImageBackground, ImageStyle, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
+import { ImageBackground, ImageStyle, StyleProp, StyleSheet, ViewStyle } from 'react-native';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { Play } from '../../icons';
+import { VideoPlayIndicator } from '../ui';
const styles = StyleSheet.create({
container: {
alignItems: 'center',
- backgroundColor: 'black',
- display: 'flex',
- height: '100%',
justifyContent: 'center',
- width: '100%',
- },
- roundedView: {
- alignItems: 'center',
- borderRadius: 50,
- display: 'flex',
- elevation: 6,
- height: 36,
- justifyContent: 'center',
- shadowColor: '#000',
- shadowOffset: {
- height: 3,
- width: 0,
- },
- shadowOpacity: 0.27,
- shadowRadius: 4.65,
- width: 36,
+ flex: 1,
+ overflow: 'hidden',
},
});
@@ -40,9 +22,8 @@ export type VideoThumbnailProps = {
export const VideoThumbnail = (props: VideoThumbnailProps) => {
const {
theme: {
- colors: { static_black, static_white },
messageSimple: {
- videoThumbnail: { container, roundedView },
+ videoThumbnail: { container },
},
},
} = useTheme();
@@ -54,9 +35,7 @@ export const VideoThumbnail = (props: VideoThumbnailProps) => {
source={{ uri: thumb_url }}
style={[styles.container, container, style]}
>
-
-
-
+
);
};
diff --git a/package/src/components/Attachment/__tests__/Attachment.test.js b/package/src/components/Attachment/__tests__/Attachment.test.js
index 5b4f726516..6367153ee9 100644
--- a/package/src/components/Attachment/__tests__/Attachment.test.js
+++ b/package/src/components/Attachment/__tests__/Attachment.test.js
@@ -1,13 +1,12 @@
import React from 'react';
-import { fireEvent, render, waitFor } from '@testing-library/react-native';
+import { render, waitFor } from '@testing-library/react-native';
import { v4 as uuidv4 } from 'uuid';
import { MessageProvider } from '../../../contexts/messageContext/MessageContext';
import { MessagesProvider } from '../../../contexts/messagesContext/MessagesContext';
import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
import {
- generateAttachmentAction,
generateAudioAttachment,
generateFileAttachment,
generateImageAttachment,
@@ -18,10 +17,10 @@ import { generateMessage } from '../../../mock-builders/generator/message';
import { ImageLoadingFailedIndicator } from '../../Attachment/ImageLoadingFailedIndicator';
import { ImageLoadingIndicator } from '../../Attachment/ImageLoadingIndicator';
import { Attachment } from '../Attachment';
-import { AttachmentActions } from '../AttachmentActions';
jest.mock('../../../native.ts', () => ({
isVideoPlayerAvailable: jest.fn(() => false),
+ isSoundPackageAvailable: jest.fn(() => false),
}));
const getAttachmentComponent = (props) => {
@@ -36,11 +35,6 @@ const getAttachmentComponent = (props) => {
);
};
-const getActionComponent = (props) => (
-
- ;
-
-);
describe('Attachment', () => {
it('should render File component for "audio" type attachment', async () => {
@@ -90,40 +84,4 @@ describe('Attachment', () => {
expect(getByTestId('gallery-container')).toBeTruthy();
});
});
-
- it('should render AttachmentActions component if attachment has actions', async () => {
- const attachment = generateImageAttachment({
- actions: [generateAttachmentAction(), generateAttachmentAction()],
- title_link: null,
- });
- const { getByTestId } = render(getAttachmentComponent({ actionHandler: () => {}, attachment }));
-
- await waitFor(() => {
- expect(getByTestId('attachment-actions')).toBeTruthy();
- });
- });
-
- it('should call actionHandler on click', async () => {
- const handleAction = jest.fn();
- const action = generateAttachmentAction();
- const { getByTestId } = render(
- getActionComponent({
- actions: [action],
- handleAction,
- }),
- );
-
- await waitFor(() => getByTestId(`attachment-actions-button-${action.name}`));
-
- expect(getByTestId('attachment-actions')).toContainElement(
- getByTestId(`attachment-actions-button-${action.name}`),
- );
-
- fireEvent.press(getByTestId(`attachment-actions-button-${action.name}`));
- fireEvent.press(getByTestId(`attachment-actions-button-${action.name}`));
-
- await waitFor(() => {
- expect(handleAction).toHaveBeenCalledTimes(2);
- });
- });
});
diff --git a/package/src/components/Attachment/__tests__/Giphy.test.js b/package/src/components/Attachment/__tests__/Giphy.test.js
index f721eb2571..2aed7e6385 100644
--- a/package/src/components/Attachment/__tests__/Giphy.test.js
+++ b/package/src/components/Attachment/__tests__/Giphy.test.js
@@ -130,7 +130,7 @@ describe('Giphy', () => {
expect(imageProps.source.uri).toBe(specificSizedGiphyData.url);
};
checkImageProps(
- screen.getByTestId('giphy-attachment-image').props,
+ screen.getByLabelText('Giphy Attachment Image').props,
attachment.giphy.fixed_height,
);
});
@@ -146,7 +146,7 @@ describe('Giphy', () => {
expect(imageProps.source.uri).toBe(specificSizedGiphyData.url);
};
checkImageProps(
- screen.getByTestId('giphy-attachment-image').props,
+ screen.getByLabelText('Giphy Attachment Image').props,
attachment.giphy.original,
);
});
diff --git a/package/src/components/Attachment/__tests__/buildGallery.test.js b/package/src/components/Attachment/__tests__/buildGallery.test.js
index 5251a32eb9..eda9ee915c 100644
--- a/package/src/components/Attachment/__tests__/buildGallery.test.js
+++ b/package/src/components/Attachment/__tests__/buildGallery.test.js
@@ -1,16 +1,14 @@
-import { PixelRatio } from 'react-native';
-
import { generateImageAttachment } from '../../../mock-builders/generator/attachment';
import { buildGallery } from '../utils/buildGallery/buildGallery';
describe('buildGallery', () => {
const defaultSizeConfig = {
- gridHeight: 100,
- gridWidth: 200,
- maxHeight: 300,
- maxWidth: 350,
- minHeight: 10,
- minWidth: 20,
+ gridHeight: 192,
+ gridWidth: 256,
+ maxHeight: 192,
+ maxWidth: 256,
+ minHeight: 120,
+ minWidth: 120,
};
it('gallery size should not exceed max sizes provided by size config', () => {
@@ -41,8 +39,7 @@ describe('buildGallery', () => {
expect(width).toBeLessThanOrEqual(defaultSizeConfig.maxWidth);
expect(width).toBeGreaterThanOrEqual(defaultSizeConfig.minWidth);
- expect(thumbnailGrid[0][0].height).toBeLessThanOrEqual(height);
- expect(thumbnailGrid[0][0].width).toBeLessThanOrEqual(width);
+ expect(thumbnailGrid[0][0].flex).toBe(1);
}
});
});
@@ -107,9 +104,8 @@ describe('buildGallery', () => {
sizeConfig: defaultSizeConfig,
});
const t1 = tg1[0][0];
- expect(t1.url.includes(`&h=${PixelRatio.getPixelSizeForLayoutSize(t1.height)}`)).toBe(true);
- expect(t1.url.includes(`&w=${PixelRatio.getPixelSizeForLayoutSize(t1.width)}`)).toBe(true);
expect(t1.url.includes('&resize=clip')).toBe(true);
+ expect(t1.flex).toBe(1);
const smallImage = generateImageAttachment({
image_url:
diff --git a/package/src/components/Attachment/utils/buildGallery/buildGalleryOfSingleImage.ts b/package/src/components/Attachment/utils/buildGallery/buildGalleryOfSingleImage.ts
index 209d526645..c6d5b40935 100644
--- a/package/src/components/Attachment/utils/buildGallery/buildGalleryOfSingleImage.ts
+++ b/package/src/components/Attachment/utils/buildGallery/buildGalleryOfSingleImage.ts
@@ -84,6 +84,7 @@ export function buildGalleryOfSingleImage({
const thumbnail = buildThumbnail({
image,
resizableCDNHosts,
+ flex: 1,
...container,
});
diff --git a/package/src/components/Attachment/utils/buildGallery/buildGalleryOfTwoImages.ts b/package/src/components/Attachment/utils/buildGallery/buildGalleryOfTwoImages.ts
index 7240aaed92..b8212ae8c1 100644
--- a/package/src/components/Attachment/utils/buildGallery/buildGalleryOfTwoImages.ts
+++ b/package/src/components/Attachment/utils/buildGallery/buildGalleryOfTwoImages.ts
@@ -41,6 +41,7 @@ export function buildGalleryOfTwoImages({
});
}
+ // Both the images are portrait
if (!isLandscape1 && !isLandscape2) {
/**
* -----------
@@ -60,7 +61,7 @@ export function buildGalleryOfTwoImages({
}
return buildThumbnailGrid({
- grid: [[2, 1]],
+ grid: [[1, 1]],
images: isLandscape1 ? images : images.reverse(),
invertedDirections: true,
resizableCDNHosts,
diff --git a/package/src/components/Attachment/utils/buildGallery/buildThumbnail.ts b/package/src/components/Attachment/utils/buildGallery/buildThumbnail.ts
index 3132979a58..323a346b77 100644
--- a/package/src/components/Attachment/utils/buildGallery/buildThumbnail.ts
+++ b/package/src/components/Attachment/utils/buildGallery/buildThumbnail.ts
@@ -14,6 +14,7 @@ export type BuildThumbnailProps = Pick;
+ messageHasOnlyOneImage?: boolean;
+};
export function getGalleryImageBorderRadius({
- alignment,
colIndex,
- groupStyles,
- hasThreadReplies,
height,
invertedDirections,
- messageText,
numOfColumns,
numOfRows,
rowIndex,
sizeConfig,
- threadList,
width,
+ messageHasOnlyOneImage = false,
}: Params) {
- const groupStyle = `${alignment}_${groupStyles?.[0]?.toLowerCase?.()}`;
const isSingleImage = numOfColumns === 1 && numOfRows === 1;
const isImageSmallerThanMinContainerSize =
isSingleImage &&
@@ -46,26 +46,20 @@ export function getGalleryImageBorderRadius({
const topRightEdgeExposed =
(!invertedDirections && colIndex === numOfColumns - 1 && rowIndex === 0) ||
(invertedDirections && colIndex === 0 && rowIndex === numOfRows - 1);
- const bottomRightEdgeExposed = colIndex === numOfColumns && rowIndex === numOfRows - 1;
+ const bottomRightEdgeExposed = colIndex === numOfColumns - 1 && rowIndex === numOfRows - 1;
return {
- borderBottomLeftRadius:
- !isImageSmallerThanMinContainerSize &&
- bottomLeftEdgeExposed &&
- !messageText &&
- ((groupStyle !== 'left_bottom' && groupStyle !== 'left_single') ||
- (hasThreadReplies && !threadList))
- ? 14
- : 0,
- borderBottomRightRadius:
- !isImageSmallerThanMinContainerSize &&
- bottomRightEdgeExposed &&
- !messageText &&
- ((groupStyle !== 'right_bottom' && groupStyle !== 'right_single') ||
- (hasThreadReplies && !threadList))
- ? 14
- : 0,
- borderTopLeftRadius: !isImageSmallerThanMinContainerSize && topLeftEdgeExposed ? 14 : 0,
- borderTopRightRadius: !isImageSmallerThanMinContainerSize && topRightEdgeExposed ? 14 : 0,
+ borderTopLeftRadius: !isImageSmallerThanMinContainerSize && topLeftEdgeExposed ? 12 : 8,
+ borderTopRightRadius: !isImageSmallerThanMinContainerSize && topRightEdgeExposed ? 12 : 8,
+ borderBottomLeftRadius: messageHasOnlyOneImage
+ ? primitives.radiusNone
+ : !isImageSmallerThanMinContainerSize && bottomLeftEdgeExposed
+ ? primitives.radiusLg
+ : primitives.radiusMd,
+ borderBottomRightRadius: messageHasOnlyOneImage
+ ? primitives.radiusNone
+ : !isImageSmallerThanMinContainerSize && bottomRightEdgeExposed
+ ? primitives.radiusLg
+ : primitives.radiusMd,
};
}
diff --git a/package/src/components/AttachmentPicker/AttachmentPicker.tsx b/package/src/components/AttachmentPicker/AttachmentPicker.tsx
index c23af98dc6..e531f38195 100644
--- a/package/src/components/AttachmentPicker/AttachmentPicker.tsx
+++ b/package/src/components/AttachmentPicker/AttachmentPicker.tsx
@@ -1,307 +1,136 @@
-import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { BackHandler, Keyboard, Platform, StyleSheet } from 'react-native';
-
-import BottomSheetOriginal from '@gorhom/bottom-sheet';
-import type { BottomSheetHandleProps } from '@gorhom/bottom-sheet';
-
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import {
+ BackHandler,
+ EmitterSubscription,
+ Keyboard,
+ Platform,
+ View,
+ LayoutChangeEvent,
+} from 'react-native';
+
+import { useBottomSheetSpringConfigs } from '@gorhom/bottom-sheet';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
-import type { AttachmentPickerErrorProps } from './components/AttachmentPickerError';
-
-import { renderAttachmentPickerItem } from './components/AttachmentPickerItem';
-
import { useAttachmentPickerContext } from '../../contexts/attachmentPickerContext/AttachmentPickerContext';
-import { MessageInputContextValue } from '../../contexts/messageInputContext/MessageInputContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { useScreenDimensions } from '../../hooks/useScreenDimensions';
-import { NativeHandlers } from '../../native';
-import type { File } from '../../types/types';
+import { useStableCallback } from '../../hooks';
import { BottomSheet } from '../BottomSheetCompatibility/BottomSheet';
-import { BottomSheetFlatList } from '../BottomSheetCompatibility/BottomSheetFlatList';
+import { KeyboardControllerPackage } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView';
dayjs.extend(duration);
-const styles = StyleSheet.create({
- container: {
- flexGrow: 1,
- },
-});
-
-export type AttachmentPickerProps = Pick<
- MessageInputContextValue,
- | 'AttachmentPickerBottomSheetHandle'
- | 'attachmentPickerBottomSheetHandleHeight'
- | 'attachmentSelectionBarHeight'
- | 'attachmentPickerBottomSheetHeight'
-> & {
- /**
- * Custom UI component to render error component while opening attachment picker.
- *
- * **Default**
- * [AttachmentPickerError](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/AttachmentPickerError.tsx)
- */
- AttachmentPickerError: React.ComponentType;
- /**
- * Custom UI component to render error image for attachment picker
- *
- * **Default**
- * [AttachmentPickerErrorImage](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/AttachmentPickerErrorImage.tsx)
- */
- AttachmentPickerErrorImage: React.ComponentType;
- /**
- * Custom UI Component to render select more photos for selected gallery access in iOS.
- */
- AttachmentPickerIOSSelectMorePhotos: React.ComponentType;
- /**
- * Custom UI component to render overlay component, that shows up on top of [selected
- * image](https://github.com/GetStream/stream-chat-react-native/blob/main/screenshots/docs/1.png) (with tick mark)
- *
- * **Default**
- * [ImageOverlaySelectedComponent](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx)
- */
- ImageOverlaySelectedComponent: React.ComponentType;
- attachmentPickerErrorButtonText?: string;
- attachmentPickerErrorText?: string;
- numberOfAttachmentImagesToLoadPerCall?: number;
- numberOfAttachmentPickerImageColumns?: number;
+const SPRING_CONFIG = {
+ damping: 80,
+ overshootClamping: true,
+ restDisplacementThreshold: 0.1,
+ restSpeedThreshold: 0.1,
+ stiffness: 500,
+ duration: 200,
};
-export const AttachmentPicker = React.forwardRef(
- (props: AttachmentPickerProps, ref: React.ForwardedRef) => {
- const {
- AttachmentPickerBottomSheetHandle,
- attachmentPickerBottomSheetHandleHeight,
- attachmentPickerBottomSheetHeight,
- AttachmentPickerError,
- attachmentPickerErrorButtonText,
- AttachmentPickerErrorImage,
- attachmentPickerErrorText,
- AttachmentPickerIOSSelectMorePhotos,
- ImageOverlaySelectedComponent,
- numberOfAttachmentImagesToLoadPerCall,
- numberOfAttachmentPickerImageColumns,
- } = props;
-
- const {
- theme: {
- attachmentPicker: { bottomSheetContentContainer },
- colors: { white },
- },
- } = useTheme();
- const { closePicker, selectedPicker, setSelectedPicker, topInset } =
- useAttachmentPickerContext();
- const { vh: screenVh } = useScreenDimensions();
-
- const fullScreenHeight = screenVh(100);
-
- const [currentIndex, setCurrentIndex] = useState(-1);
- const endCursorRef = useRef(undefined);
- const [photoError, setPhotoError] = useState(false);
- const [iOSLimited, setIosLimited] = useState(false);
- const hasNextPageRef = useRef(true);
- const [loadingPhotos, setLoadingPhotos] = useState(false);
- const [photos, setPhotos] = useState([]);
- const attemptedToLoadPhotosOnOpenRef = useRef(false);
-
- const getMorePhotos = useCallback(async () => {
- if (
- hasNextPageRef.current &&
- !loadingPhotos &&
- currentIndex > -1 &&
- selectedPicker === 'images'
- ) {
- setPhotoError(false);
- setLoadingPhotos(true);
- const endCursor = endCursorRef.current;
- try {
- if (!NativeHandlers.getPhotos) {
- setPhotos([]);
- setIosLimited(false);
- return;
- }
- const results = await NativeHandlers.getPhotos({
- after: endCursor,
- first: numberOfAttachmentImagesToLoadPerCall ?? 60,
- });
- endCursorRef.current = results.endCursor;
- setPhotos((prevPhotos) =>
- endCursor ? [...prevPhotos, ...results.assets] : results.assets,
- );
- setIosLimited(results.iOSLimited);
- hasNextPageRef.current = !!results.hasNextPage;
- } catch (error) {
- setPhotoError(true);
- }
- setLoadingPhotos(false);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currentIndex, selectedPicker, loadingPhotos]);
-
- // we need to use ref here to avoid running effect when getMorePhotos changes
- const getMorePhotosRef = useRef(getMorePhotos);
- getMorePhotosRef.current = getMorePhotos;
-
- useEffect(() => {
- if (selectedPicker !== 'images') {
- return;
- }
-
- if (!NativeHandlers.oniOS14GalleryLibrarySelectionChange) {
- return;
+export const AttachmentPicker = () => {
+ const {
+ closePicker,
+ attachmentPickerStore,
+ AttachmentPickerSelectionBar,
+ AttachmentPickerContent,
+ attachmentPickerBottomSheetHeight,
+ bottomSheetRef: ref,
+ disableAttachmentPicker,
+ } = useAttachmentPickerContext();
+
+ const [currentIndex, setCurrentIndexInternal] = useState(-1);
+ const currentIndexRef = useRef(currentIndex);
+ const setCurrentIndex = useStableCallback((_: number, toIndex: number) => {
+ setCurrentIndexInternal(toIndex);
+ currentIndexRef.current = toIndex;
+ });
+
+ useEffect(() => {
+ const backAction = () => {
+ if (attachmentPickerStore.state.getLatestValue().selectedPicker) {
+ attachmentPickerStore.setSelectedPicker(undefined);
+ closePicker();
+ return true;
}
- // ios 14 library selection change event is fired when user reselects the images that are permitted to be
- // readable by the app
- const { unsubscribe } = NativeHandlers.oniOS14GalleryLibrarySelectionChange(() => {
- // we reset the cursor and has next page to true to facilitate fetching of the first page of photos again
- hasNextPageRef.current = true;
- endCursorRef.current = undefined;
- // fetch the first page of photos again
- getMorePhotosRef.current();
- });
- return unsubscribe;
- }, [selectedPicker]);
-
- useEffect(() => {
- const backAction = () => {
- if (selectedPicker) {
- setSelectedPicker(undefined);
- closePicker();
- return true;
- }
-
- return false;
- };
-
- const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction);
-
- return () => backHandler.remove();
- }, [selectedPicker, closePicker, setSelectedPicker]);
- useEffect(() => {
- const onKeyboardOpenHandler = () => {
- if (selectedPicker) {
- setSelectedPicker(undefined);
- }
- closePicker();
- };
- const keyboardShowEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
- const keyboardSubscription = Keyboard.addListener(keyboardShowEvent, onKeyboardOpenHandler);
+ return false;
+ };
- return () => {
- // Following if-else condition to avoid deprecated warning coming RN 0.65
- if (keyboardSubscription?.remove) {
- keyboardSubscription.remove();
- return;
- }
- // @ts-ignore
- else if (Keyboard.removeListener) {
- // @ts-ignore
- Keyboard.removeListener(keyboardShowEvent, onKeyboardOpenHandler);
- }
- };
- }, [closePicker, selectedPicker, setSelectedPicker]);
+ const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction);
- useEffect(() => {
- if (currentIndex < 0) {
- setSelectedPicker(undefined);
- if (!loadingPhotos) {
- endCursorRef.current = undefined;
- hasNextPageRef.current = true;
- attemptedToLoadPhotosOnOpenRef.current = false;
- setPhotoError(false);
- }
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currentIndex, loadingPhotos]);
+ return () => backHandler.remove();
+ }, [attachmentPickerStore, closePicker]);
- useEffect(() => {
- if (
- !attemptedToLoadPhotosOnOpenRef.current &&
- selectedPicker === 'images' &&
- endCursorRef.current === undefined &&
- currentIndex > -1 &&
- !loadingPhotos
- ) {
- getMorePhotos();
- // we do this only once on open for avoiding to request permissions in rationale dialog again and again on
- // Android
- attemptedToLoadPhotosOnOpenRef.current = true;
+ useEffect(() => {
+ const onKeyboardOpenHandler = () => {
+ if (attachmentPickerStore.state.getLatestValue().selectedPicker) {
+ attachmentPickerStore.setSelectedPicker(undefined);
}
- }, [currentIndex, selectedPicker, getMorePhotos, loadingPhotos]);
-
- const selectedPhotos = useMemo(
- () =>
- photos.map((asset) => ({
- asset,
- ImageOverlaySelectedComponent,
- numberOfAttachmentPickerImageColumns,
- })),
- [photos, ImageOverlaySelectedComponent, numberOfAttachmentPickerImageColumns],
- );
-
- const handleHeight = attachmentPickerBottomSheetHandleHeight;
+ closePicker();
+ };
+ let keyboardSubscription: EmitterSubscription | null = null;
+ if (KeyboardControllerPackage?.KeyboardEvents) {
+ keyboardSubscription = KeyboardControllerPackage.KeyboardEvents.addListener(
+ 'keyboardWillShow',
+ onKeyboardOpenHandler,
+ );
+ } else {
+ const keyboardShowEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
+ keyboardSubscription = Keyboard.addListener(keyboardShowEvent, onKeyboardOpenHandler);
+ }
+ return () => {
+ keyboardSubscription?.remove();
+ };
+ }, [attachmentPickerStore, closePicker]);
- const initialSnapPoint = attachmentPickerBottomSheetHeight;
+ useEffect(() => {
+ if (currentIndex < 0) {
+ attachmentPickerStore.setSelectedPicker(undefined);
+ }
+ }, [currentIndex, attachmentPickerStore]);
- const finalSnapPoint = fullScreenHeight - topInset;
+ const selectionBarRef = useRef(null);
- /**
- * Snap points changing cause a rerender of the position,
- * this is an issue if you are calling close on the bottom sheet.
- */
- const snapPoints = [initialSnapPoint, finalSnapPoint];
+ const initialSnapPoint = attachmentPickerBottomSheetHeight;
- const MemoizedAttachmentPickerBottomSheetHandle = useCallback(
- (props: BottomSheetHandleProps) =>
- /**
- * using `null` here instead of `style={{ opacity: photoError ? 0 : 1 }}`
- * as opacity is not an allowed style
- */
- !photoError && AttachmentPickerBottomSheetHandle ? (
-
- ) : null,
- [AttachmentPickerBottomSheetHandle, photoError],
- );
+ /**
+ * Snap points changing cause a rerender of the position,
+ * this is an issue if you are calling close on the bottom sheet.
+ */
+ const snapPoints = useMemo(() => [initialSnapPoint], [initialSnapPoint]);
+
+ const onAttachmentPickerSelectionBarLayout = useStableCallback((e: LayoutChangeEvent) => {
+ selectionBarRef.current = e.nativeEvent.layout.height;
+ });
+
+ const animationConfigs = useBottomSheetSpringConfigs(SPRING_CONFIG);
+
+ return (
+
+
+
+
+ {!disableAttachmentPicker ? (
+
+ ) : null}
+
+ );
+};
- return (
- <>
-
- {iOSLimited && }
- item.asset.uri}
- numColumns={numberOfAttachmentPickerImageColumns ?? 3}
- onEndReached={photoError ? undefined : getMorePhotos}
- renderItem={renderAttachmentPickerItem}
- testID={'attachment-picker-list'}
- />
-
- {selectedPicker === 'images' && photoError && (
-
- )}
- >
- );
- },
-);
+const RenderNull = () => null;
AttachmentPicker.displayName = 'AttachmentPicker';
diff --git a/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentMediaPicker.tsx b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentMediaPicker.tsx
new file mode 100644
index 0000000000..887a4d8080
--- /dev/null
+++ b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentMediaPicker.tsx
@@ -0,0 +1,184 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Linking, StyleSheet } from 'react-native';
+
+import { renderAttachmentPickerItem } from './AttachmentPickerItem';
+
+import { useAttachmentPickerContext, useTheme, useTranslationContext } from '../../../../contexts';
+
+import { useStableCallback } from '../../../../hooks';
+import { Picture } from '../../../../icons';
+
+import { NativeHandlers } from '../../../../native';
+import type { File } from '../../../../types/types';
+import { BottomSheetFlatList } from '../../../BottomSheetCompatibility/BottomSheetFlatList';
+import {
+ AttachmentPickerContentProps,
+ AttachmentPickerGenericContent,
+} from '../AttachmentPickerContent';
+
+export const IOS_LIMITED_DEEPLINK = '@getstream/ios-limited-button' as const;
+
+export type IosLimitedItemType = { uri: typeof IOS_LIMITED_DEEPLINK };
+
+export type PhotoContentItemType = File | IosLimitedItemType;
+
+export const isIosLimited = (item: PhotoContentItemType): item is IosLimitedItemType =>
+ 'uri' in item && item.uri === '@getstream/ios-limited-button';
+
+const keyExtractor = (item: PhotoContentItemType) => item.uri;
+
+const useMediaPickerStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return StyleSheet.create({
+ container: {
+ flexGrow: 1,
+ backgroundColor: semantics.composerBg,
+ },
+ });
+};
+
+export const AttachmentMediaPickerIcon = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ return ;
+};
+
+export const AttachmentMediaPicker = (props: AttachmentPickerContentProps) => {
+ const { t } = useTranslationContext();
+ const { numberOfAttachmentImagesToLoadPerCall, numberOfAttachmentPickerImageColumns } =
+ useAttachmentPickerContext();
+ const { height } = props;
+
+ const {
+ theme: {
+ attachmentPicker: { bottomSheetContentContainer },
+ },
+ } = useTheme();
+ const styles = useMediaPickerStyles();
+
+ const numberOfColumns = numberOfAttachmentPickerImageColumns ?? 3;
+
+ const endCursorRef = useRef(undefined);
+ const [photoError, setPhotoError] = useState(false);
+ const hasNextPageRef = useRef(true);
+ const loadingPhotosRef = useRef(false);
+ const [photos, setPhotos] = useState>([]);
+ const attemptedToLoadPhotosOnOpenRef = useRef(false);
+
+ const getMorePhotos = useStableCallback(async () => {
+ if (hasNextPageRef.current && !loadingPhotosRef.current) {
+ setPhotoError(false);
+ loadingPhotosRef.current = true;
+ const endCursor = endCursorRef.current;
+ try {
+ if (!NativeHandlers.getPhotos) {
+ setPhotos([]);
+ return;
+ }
+
+ const results = await NativeHandlers.getPhotos({
+ after: endCursor,
+ first: numberOfAttachmentImagesToLoadPerCall ?? 25,
+ });
+
+ endCursorRef.current = results.endCursor;
+ // skip updating if the sheet closed in the meantime, to avoid
+ // confusing the bottom sheet internals
+ setPhotos((prevPhotos) => {
+ if (endCursor) {
+ return [...prevPhotos, ...results.assets];
+ }
+
+ let assets: PhotoContentItemType[] = results.assets;
+
+ if (results.iOSLimited) {
+ assets = [{ uri: IOS_LIMITED_DEEPLINK }, ...assets];
+ }
+
+ for (let i = 0; i < results.assets.length; i++) {
+ if (assets[i].uri !== prevPhotos[i]?.uri) {
+ return assets;
+ }
+ }
+
+ return prevPhotos.slice(0, assets.length);
+ });
+ hasNextPageRef.current = !!results.hasNextPage;
+ } catch (error) {
+ setPhotoError(true);
+ }
+ loadingPhotosRef.current = false;
+ }
+ });
+
+ useEffect(() => {
+ if (!NativeHandlers.oniOS14GalleryLibrarySelectionChange) {
+ return;
+ }
+ // ios 14 library selection change event is fired when user reselects the images that are permitted to be
+ // readable by the app
+ const { unsubscribe } = NativeHandlers.oniOS14GalleryLibrarySelectionChange(() => {
+ // we reset the cursor and has next page to true to facilitate fetching of the first page of photos again
+ hasNextPageRef.current = true;
+ endCursorRef.current = undefined;
+ // fetch the first page of photos again
+ getMorePhotos();
+ });
+ return unsubscribe;
+ }, [getMorePhotos]);
+
+ useEffect(() => {
+ if (!loadingPhotosRef.current) {
+ endCursorRef.current = undefined;
+ hasNextPageRef.current = true;
+ attemptedToLoadPhotosOnOpenRef.current = false;
+ setPhotoError(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (
+ !attemptedToLoadPhotosOnOpenRef.current &&
+ endCursorRef.current === undefined &&
+ !loadingPhotosRef.current
+ ) {
+ getMorePhotos();
+ // we do this only once on open for avoiding to request permissions in rationale dialog again and again on
+ // Android
+ attemptedToLoadPhotosOnOpenRef.current = true;
+ }
+ }, [getMorePhotos]);
+
+ const openSettings = useStableCallback(async () => {
+ try {
+ await Linking.openSettings();
+ } catch (error) {
+ console.log(error);
+ }
+ });
+
+ return photoError ? (
+
+ ) : (
+
+ );
+};
diff --git a/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx
new file mode 100644
index 0000000000..6804f09564
--- /dev/null
+++ b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx
@@ -0,0 +1,228 @@
+import React from 'react';
+
+import { Alert, ImageBackground, StyleSheet, Text, View } from 'react-native';
+
+import { FileReference, isLocalImageAttachment, isLocalVideoAttachment } from 'stream-chat';
+
+import { isIosLimited, PhotoContentItemType } from './AttachmentMediaPicker';
+
+import { useAttachmentPickerContext } from '../../../../contexts';
+import { useAttachmentManagerState } from '../../../../contexts/messageInputContext/hooks/useAttachmentManagerState';
+import { useMessageComposer } from '../../../../contexts/messageInputContext/hooks/useMessageComposer';
+import { useMessageInputContext } from '../../../../contexts/messageInputContext/MessageInputContext';
+import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
+import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext';
+import { useViewport } from '../../../../hooks/useViewport';
+import { NewPlus } from '../../../../icons/NewPlus';
+import { NativeHandlers } from '../../../../native';
+import { primitives } from '../../../../theme';
+import type { File } from '../../../../types/types';
+import { BottomSheetTouchableOpacity } from '../../../BottomSheetCompatibility/BottomSheetTouchableOpacity';
+import { VideoAttachmentMetadataPill } from '../../../MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview';
+
+type AttachmentPickerItemType = {
+ asset: File;
+};
+
+const AttachmentVideo = (props: AttachmentPickerItemType) => {
+ const { asset } = props;
+ const { numberOfAttachmentPickerImageColumns, ImageOverlaySelectedComponent } =
+ useAttachmentPickerContext();
+ const { vw } = useViewport();
+ const { t } = useTranslationContext();
+ const messageComposer = useMessageComposer();
+ const { uploadNewFile } = useMessageInputContext();
+ const { attachmentManager } = messageComposer;
+ const { attachments, availableUploadSlots } = useAttachmentManagerState();
+
+ const selectedIndex = attachments.findIndex((attachment) =>
+ isLocalVideoAttachment(attachment)
+ ? (attachment.localMetadata.file as FileReference).uri === asset.uri
+ : false,
+ );
+
+ const {
+ theme: {
+ attachmentPicker: { image, imageOverlay },
+ },
+ } = useTheme();
+ const styles = useStyles();
+
+ const { duration: videoDuration, thumb_url } = asset;
+
+ const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2;
+
+ const onPressVideo = async () => {
+ if (selectedIndex !== -1) {
+ const attachment = attachments[selectedIndex];
+ if (attachment) {
+ attachmentManager.removeAttachments([attachment.localMetadata.id]);
+ }
+ } else {
+ if (!availableUploadSlots) {
+ Alert.alert(t('Maximum number of files reached'));
+ return;
+ }
+ await uploadNewFile(asset);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+const AttachmentImage = (props: AttachmentPickerItemType) => {
+ const { asset } = props;
+ const { numberOfAttachmentPickerImageColumns, ImageOverlaySelectedComponent } =
+ useAttachmentPickerContext();
+ const {
+ theme: {
+ attachmentPicker: { image, imageOverlay },
+ },
+ } = useTheme();
+ const styles = useStyles();
+ const { vw } = useViewport();
+ const { uploadNewFile } = useMessageInputContext();
+ const messageComposer = useMessageComposer();
+ const { attachmentManager } = messageComposer;
+ const { attachments, availableUploadSlots } = useAttachmentManagerState();
+ const selectedIndex = attachments.findIndex((attachment) =>
+ isLocalImageAttachment(attachment) ? attachment.localMetadata.previewUri === asset.uri : false,
+ );
+
+ const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2;
+
+ const { uri } = asset;
+
+ const onPressImage = async () => {
+ if (selectedIndex !== -1) {
+ const attachment = attachments[selectedIndex];
+ if (attachment) {
+ await attachmentManager.removeAttachments([attachment.localMetadata.id]);
+ }
+ } else {
+ if (!availableUploadSlots) {
+ Alert.alert('Maximum number of files reached');
+ return;
+ }
+ await uploadNewFile(asset);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+const AttachmentIosLimited = () => {
+ const { numberOfAttachmentPickerImageColumns } = useAttachmentPickerContext();
+ const { vw } = useViewport();
+ const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2;
+ const styles = useStyles();
+ return (
+
+
+ Add more
+
+ );
+};
+
+export const renderAttachmentPickerItem = ({ item }: { item: PhotoContentItemType }) => {
+ if (isIosLimited(item)) {
+ return ;
+ }
+ /**
+ * Expo Media Library - Result of asset type
+ * Native Android - Gives mime type(Eg: image/jpeg, video/mp4, etc.)
+ * Native iOS - Gives `image` or `video`
+ * Expo Android/iOS - Gives `photo` or `video`
+ **/
+ const isVideoType = item.type.includes('video');
+
+ if (isVideoType) {
+ return ;
+ }
+
+ return ;
+};
+
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return StyleSheet.create({
+ durationText: {
+ fontWeight: 'bold',
+ },
+ overlay: {
+ alignItems: 'flex-end',
+ flex: 1,
+ },
+ videoView: {
+ bottom: 5,
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ paddingHorizontal: 5,
+ position: 'absolute',
+ width: '100%',
+ },
+ iosLimitedContainer: {
+ margin: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: semantics.backgroundCoreSurfaceSubtle,
+ },
+ iosLimitedIcon: {
+ color: semantics.textTertiary,
+ },
+ iosLimitedText: {
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ fontSize: primitives.typographyFontSizeSm,
+ color: semantics.textTertiary,
+ },
+ });
+};
diff --git a/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/index.ts b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/index.ts
new file mode 100644
index 0000000000..a803c5a324
--- /dev/null
+++ b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/index.ts
@@ -0,0 +1,2 @@
+export * from './AttachmentMediaPicker';
+export * from './AttachmentPickerItem';
diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerBottomSheetHandle.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerBottomSheetHandle.tsx
deleted file mode 100644
index c48f63b785..0000000000
--- a/package/src/components/AttachmentPicker/components/AttachmentPickerBottomSheetHandle.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-import { StyleSheet, View, ViewStyle } from 'react-native';
-import Animated, { SharedValue, useAnimatedStyle } from 'react-native-reanimated';
-
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- height: 20,
- justifyContent: 'center',
- },
- handle: {
- borderRadius: 2,
- height: 4,
- width: 40,
- },
-});
-
-type Props = {
- animatedIndex: SharedValue;
-};
-
-export const AttachmentPickerBottomSheetHandle = ({ animatedIndex }: Props) => {
- const {
- theme: {
- attachmentPicker: {
- handle: { container, indicator },
- },
- colors: { black, white },
- },
- } = useTheme();
-
- const style = useAnimatedStyle(() => ({
- borderTopLeftRadius: animatedIndex.value > 0 ? 16 - animatedIndex.value * 16 : 16,
- borderTopRightRadius: animatedIndex.value > 0 ? 16 - animatedIndex.value * 16 : 16,
- }));
-
- return (
-
-
- {/* ^ 1A = 10% opacity */}
-
- );
-};
diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx
new file mode 100644
index 0000000000..639751e1e6
--- /dev/null
+++ b/package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx
@@ -0,0 +1,336 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { Linking, Platform, Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { FlatList } from 'react-native-gesture-handler';
+
+import { CommandSearchSource, CommandSuggestion } from 'stream-chat';
+
+import { AttachmentMediaPicker } from './AttachmentMediaPicker/AttachmentMediaPicker';
+
+import {
+ useAttachmentPickerContext,
+ useBottomSheetContext,
+ useMessageComposer,
+ useMessageInputContext,
+ useTranslationContext,
+} from '../../../contexts';
+import { useTheme } from '../../../contexts/themeContext/ThemeContext';
+import { useAttachmentPickerState, useStableCallback } from '../../../hooks';
+import { Camera, FilePickerIcon, IconProps, PollThumbnail, Recorder } from '../../../icons';
+import { primitives } from '../../../theme';
+import { CommandSuggestionItem } from '../../AutoCompleteInput/AutoCompleteSuggestionItem';
+import { Button } from '../../ui';
+
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: semantics.backgroundElevationElevation1,
+ paddingHorizontal: primitives.spacing2xl,
+ paddingBottom: primitives.spacing3xl,
+ },
+ infoContainer: { flex: 1, alignItems: 'center', justifyContent: 'center' },
+ text: {
+ fontSize: primitives.typographyFontSizeMd,
+ color: semantics.textSecondary,
+ marginTop: 8,
+ marginHorizontal: 24,
+ textAlign: 'center',
+ maxWidth: 200,
+ },
+ }),
+ [semantics.backgroundElevationElevation1, semantics.textSecondary],
+ );
+};
+
+export type AttachmentPickerGenericContentProps = {
+ Icon: React.ComponentType;
+ onPress: () => void;
+ height?: number;
+ buttonText?: string;
+ description?: string;
+};
+
+export const AttachmentPickerGenericContent = (props: AttachmentPickerGenericContentProps) => {
+ const { height, buttonText, Icon, description, onPress } = props;
+ const styles = useStyles();
+
+ const {
+ theme: {
+ semantics,
+ attachmentPicker: {
+ content: { container, text, infoContainer },
+ },
+ },
+ } = useTheme();
+
+ const ThemedIcon = useCallback(
+ () => ,
+ [Icon, semantics.textTertiary],
+ );
+
+ return (
+
+
+
+ {description}
+
+
+
+ );
+};
+
+const keyExtractor = (item: { id: string }) => item.id;
+
+const AttachmentCommandPickerItemUI = ({
+ item,
+ onPress,
+}: {
+ item: CommandSuggestion;
+ onPress: () => void;
+}) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ return (
+ ({
+ backgroundColor: pressed ? semantics.backgroundCorePressed : undefined,
+ borderRadius: primitives.radiusSm,
+ })}
+ onPress={onPress}
+ >
+
+
+ );
+};
+
+export const AttachmentCommandNativePickerItem = ({ item }: { item: CommandSuggestion }) => {
+ const messageComposer = useMessageComposer();
+ const { textComposer } = messageComposer;
+ const { inputBoxRef } = useMessageInputContext();
+ const { close } = useBottomSheetContext();
+
+ const handlePress = useCallback(() => {
+ textComposer.setCommand(item);
+ close(() => inputBoxRef.current?.focus());
+ }, [textComposer, item, close, inputBoxRef]);
+
+ return ;
+};
+
+export const AttachmentCommandPickerItem = ({ item }: { item: CommandSuggestion }) => {
+ const { disableAttachmentPicker } = useAttachmentPickerContext();
+
+ const messageComposer = useMessageComposer();
+ const { textComposer } = messageComposer;
+ const { inputBoxRef } = useMessageInputContext();
+
+ const handlePress = useCallback(() => {
+ textComposer.setCommand(item);
+ inputBoxRef.current?.focus();
+ }, [textComposer, item, inputBoxRef]);
+
+ if (disableAttachmentPicker) {
+ return ;
+ }
+
+ return ;
+};
+
+const renderItem = ({ item }: { item: CommandSuggestion }) => {
+ return ;
+};
+
+const useCommandPickerStyle = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ contentContainer: {
+ flexGrow: 1,
+ paddingHorizontal: primitives.spacingXxs,
+ paddingBottom: primitives.spacing2xl,
+ },
+ title: {
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ fontSize: primitives.typographyFontSizeMd,
+ color: semantics.textPrimary,
+ paddingHorizontal: primitives.spacingMd,
+ paddingBottom: primitives.spacingMd,
+ },
+ }),
+ [semantics.textPrimary],
+ );
+};
+
+export const AttachmentCommandPicker = () => {
+ const { t } = useTranslationContext();
+ const messageComposer = useMessageComposer();
+ const [commands] = useState(() => {
+ const commandsSearchSource = new CommandSearchSource(messageComposer.channel);
+ const result = commandsSearchSource.query('');
+
+ return result.items;
+ });
+ const styles = useCommandPickerStyle();
+
+ return (
+ <>
+ {t('Instant Commands')}
+
+ >
+ );
+};
+
+export const AttachmentPollPicker = (props: AttachmentPickerContentProps) => {
+ const { t } = useTranslationContext();
+ const { height } = props;
+ const { openPollCreationDialog, sendMessage } = useMessageInputContext();
+
+ const openPollCreationModal = useStableCallback(() => {
+ openPollCreationDialog?.({ sendMessage });
+ });
+
+ return (
+
+ );
+};
+
+export const AttachmentCameraPicker = (
+ props: AttachmentPickerContentProps & { videoOnly?: boolean },
+) => {
+ const [permissionDenied, setPermissionDenied] = useState(false);
+ const { t } = useTranslationContext();
+ const { height, videoOnly } = props;
+ const { takeAndUploadImage } = useMessageInputContext();
+
+ const openCameraPicker = useStableCallback(async () => {
+ const result = await takeAndUploadImage(
+ videoOnly ? 'video' : Platform.OS === 'android' ? 'image' : 'mixed',
+ );
+ if (result?.askToOpenSettings) {
+ setPermissionDenied(true);
+ }
+ });
+
+ const openSettings = useStableCallback(async () => {
+ try {
+ await Linking.openSettings();
+ } catch (error) {
+ console.log(error);
+ }
+ });
+
+ useEffect(() => {
+ openCameraPicker();
+ }, [openCameraPicker]);
+
+ return permissionDenied ? (
+
+ ) : (
+
+ );
+};
+
+export const AttachmentFilePicker = (props: AttachmentPickerContentProps) => {
+ const { t } = useTranslationContext();
+ const { height } = props;
+ const { pickFile } = useMessageInputContext();
+
+ return (
+
+ );
+};
+
+export type AttachmentPickerContentProps = Pick;
+
+export const AttachmentPickerContent = (props: AttachmentPickerContentProps) => {
+ const { height } = props;
+ const { selectedPicker } = useAttachmentPickerState();
+
+ // TODO V9: Think of a better way to do this. This is just a temporary fix.
+ const lastSelectedPickerRef = useRef(selectedPicker);
+ if (selectedPicker) {
+ lastSelectedPickerRef.current = selectedPicker;
+ }
+
+ if (lastSelectedPickerRef.current === 'images') {
+ return ;
+ }
+
+ if (lastSelectedPickerRef.current === 'files') {
+ return ;
+ }
+
+ if (lastSelectedPickerRef.current === 'camera-photo') {
+ return ;
+ }
+
+ if (lastSelectedPickerRef.current === 'camera-video') {
+ return ;
+ }
+
+ if (lastSelectedPickerRef.current === 'polls') {
+ return ;
+ }
+
+ if (lastSelectedPickerRef.current === 'commands') {
+ return ;
+ }
+
+ return null;
+};
diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerError.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerError.tsx
deleted file mode 100644
index 008da8c640..0000000000
--- a/package/src/components/AttachmentPicker/components/AttachmentPickerError.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import React from 'react';
-import { Linking, StyleSheet, Text, View } from 'react-native';
-
-import { useAttachmentPickerContext } from '../../../contexts/attachmentPickerContext/AttachmentPickerContext';
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
-
-const styles = StyleSheet.create({
- errorButtonText: {
- fontSize: 14,
- fontWeight: '600',
- marginHorizontal: 24,
- marginTop: 16,
- textAlign: 'center',
- },
- errorContainer: {
- alignItems: 'center',
- bottom: 0,
- justifyContent: 'center',
- left: 0,
- position: 'absolute',
- right: 0,
- },
- errorText: {
- fontSize: 14,
- marginHorizontal: 24,
- marginTop: 16,
- textAlign: 'center',
- },
-});
-
-export type AttachmentPickerErrorProps = {
- AttachmentPickerErrorImage: React.ComponentType;
- attachmentPickerBottomSheetHeight?: number;
- attachmentPickerErrorButtonText?: string;
- attachmentPickerErrorText?: string;
-};
-
-export const AttachmentPickerError = (props: AttachmentPickerErrorProps) => {
- const {
- attachmentPickerBottomSheetHeight,
- attachmentPickerErrorButtonText,
- AttachmentPickerErrorImage,
- attachmentPickerErrorText,
- } = props;
-
- const {
- theme: {
- attachmentPicker: { errorButtonText, errorContainer, errorText },
- colors: { accent_blue, grey, white_smoke },
- },
- } = useTheme();
- const { t } = useTranslationContext();
-
- const { closePicker, setSelectedPicker } = useAttachmentPickerContext();
-
- const openSettings = async () => {
- try {
- setSelectedPicker(undefined);
- closePicker();
- await Linking.openSettings();
- } catch (error) {
- console.log(error);
- }
- };
-
- return (
-
-
-
- {attachmentPickerErrorText ||
- t('Please enable access to your photos and videos so you can share them.')}
-
-
- {attachmentPickerErrorButtonText || t('Allow access to your Gallery')}
-
-
- );
-};
diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerErrorImage.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerErrorImage.tsx
deleted file mode 100644
index bad5c674ad..0000000000
--- a/package/src/components/AttachmentPicker/components/AttachmentPickerErrorImage.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { Picture } from '../../../icons';
-
-export const AttachmentPickerErrorImage = () => {
- const {
- theme: {
- colors: { grey_gainsboro },
- },
- } = useTheme();
-
- return ;
-};
diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerIOSSelectMorePhotos.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerIOSSelectMorePhotos.tsx
deleted file mode 100644
index 1654a8b52f..0000000000
--- a/package/src/components/AttachmentPicker/components/AttachmentPickerIOSSelectMorePhotos.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react';
-import { Pressable, StyleSheet, Text } from 'react-native';
-
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
-import { NativeHandlers } from '../../../native';
-
-export const AttachmentPickerIOSSelectMorePhotos = () => {
- const { t } = useTranslationContext();
- const {
- theme: {
- colors: { accent_blue, white },
- },
- } = useTheme();
-
- if (!NativeHandlers.iOS14RefreshGallerySelection) {
- return null;
- }
-
- return (
-
- {t('Select More Photos')}
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {},
- text: {
- fontSize: 16,
- paddingVertical: 10,
- textAlign: 'center',
- },
-});
diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx
deleted file mode 100644
index fb3bc84acb..0000000000
--- a/package/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-import React from 'react';
-
-import { Alert, ImageBackground, StyleSheet, Text, View } from 'react-native';
-
-import { FileReference, isLocalImageAttachment, isLocalVideoAttachment } from 'stream-chat';
-
-import { useAttachmentManagerState } from '../../../contexts/messageInputContext/hooks/useAttachmentManagerState';
-import { useMessageComposer } from '../../../contexts/messageInputContext/hooks/useMessageComposer';
-import { useMessageInputContext } from '../../../contexts/messageInputContext/MessageInputContext';
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
-import { useViewport } from '../../../hooks/useViewport';
-import { Recorder } from '../../../icons';
-import type { File } from '../../../types/types';
-import { getDurationLabelFromDuration } from '../../../utils/utils';
-import { BottomSheetTouchableOpacity } from '../../BottomSheetCompatibility/BottomSheetTouchableOpacity';
-
-type AttachmentPickerItemType = {
- asset: File;
- ImageOverlaySelectedComponent: React.ComponentType;
- numberOfAttachmentPickerImageColumns?: number;
-};
-
-const AttachmentVideo = (props: AttachmentPickerItemType) => {
- const { asset, ImageOverlaySelectedComponent, numberOfAttachmentPickerImageColumns } = props;
- const { vw } = useViewport();
- const { t } = useTranslationContext();
- const messageComposer = useMessageComposer();
- const { uploadNewFile } = useMessageInputContext();
- const { attachmentManager } = messageComposer;
- const { attachments, availableUploadSlots } = useAttachmentManagerState();
- const videoUploads = attachments.filter((attachment) => isLocalVideoAttachment(attachment));
-
- const selected = videoUploads.some(
- (attachment) => (attachment.localMetadata.file as FileReference).uri === asset.uri,
- );
-
- const {
- theme: {
- attachmentPicker: { durationText, image, imageOverlay },
- colors: { overlay, white },
- },
- } = useTheme();
-
- const { duration: videoDuration, thumb_url, uri } = asset;
-
- const durationLabel = videoDuration ? getDurationLabelFromDuration(videoDuration) : '00:00';
-
- const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2;
-
- const onPressVideo = async () => {
- if (selected) {
- const attachment = videoUploads.find(
- (attachment) => (attachment.localMetadata.file as FileReference).uri === uri,
- );
- if (attachment) {
- attachmentManager.removeAttachments([attachment.localMetadata.id]);
- }
- } else {
- if (!availableUploadSlots) {
- Alert.alert(t('Maximum number of files reached'));
- return;
- }
- await uploadNewFile(asset);
- }
- };
-
- return (
-
-
- {selected && (
-
-
-
- )}
-
-
- {videoDuration ? (
-
- {durationLabel}
-
- ) : null}
-
-
-
- );
-};
-
-const AttachmentImage = (props: AttachmentPickerItemType) => {
- const { asset, ImageOverlaySelectedComponent, numberOfAttachmentPickerImageColumns } = props;
- const {
- theme: {
- attachmentPicker: { image, imageOverlay },
- colors: { overlay },
- },
- } = useTheme();
- const { vw } = useViewport();
- const { uploadNewFile } = useMessageInputContext();
- const messageComposer = useMessageComposer();
- const { attachmentManager } = messageComposer;
- const { attachments, availableUploadSlots } = useAttachmentManagerState();
- const imageUploads = attachments.filter((attachment) => isLocalImageAttachment(attachment));
-
- const selected = imageUploads.some(
- (attachment) => attachment.localMetadata.previewUri === asset.uri,
- );
-
- const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2;
-
- const { uri } = asset;
-
- const onPressImage = async () => {
- if (selected) {
- const attachment = imageUploads.find(
- (attachment) => attachment.localMetadata.previewUri === uri,
- );
- if (attachment) {
- await attachmentManager.removeAttachments([attachment.localMetadata.id]);
- }
- } else {
- if (!availableUploadSlots) {
- Alert.alert('Maximum number of files reached');
- return;
- }
- await uploadNewFile(asset);
- }
- };
-
- return (
-
-
- {selected && (
-
-
-
- )}
-
-
- );
-};
-
-export const renderAttachmentPickerItem = ({ item }: { item: AttachmentPickerItemType }) => {
- const { asset, ImageOverlaySelectedComponent, numberOfAttachmentPickerImageColumns } = item;
-
- /**
- * Expo Media Library - Result of asset type
- * Native Android - Gives mime type(Eg: image/jpeg, video/mp4, etc.)
- * Native iOS - Gives `image` or `video`
- * Expo Android/iOS - Gives `photo` or `video`
- **/
- const isVideoType = asset.type.includes('video');
-
- if (isVideoType) {
- return (
-
- );
- }
-
- return (
-
- );
-};
-
-const styles = StyleSheet.create({
- durationText: {
- fontWeight: 'bold',
- },
- overlay: {
- alignItems: 'flex-end',
- flex: 1,
- },
- videoView: {
- bottom: 5,
- display: 'flex',
- flexDirection: 'row',
- justifyContent: 'space-between',
- paddingHorizontal: 5,
- position: 'absolute',
- width: '100%',
- },
-});
diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx
index f2bceb7e61..c43274a096 100644
--- a/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx
+++ b/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx
@@ -1,142 +1,51 @@
-import React from 'react';
-import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
+import React, { useMemo } from 'react';
+import { StyleSheet, View } from 'react-native';
-import { useAttachmentPickerContext } from '../../../contexts/attachmentPickerContext/AttachmentPickerContext';
-import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
-import { useMessageInputContext } from '../../../contexts/messageInputContext/MessageInputContext';
-import { useMessagesContext } from '../../../contexts/messagesContext/MessagesContext';
-import { useOwnCapabilitiesContext } from '../../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext';
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- flexDirection: 'row',
- paddingHorizontal: 6,
- },
- icon: {
- marginHorizontal: 12,
- },
-});
+import {
+ CameraPickerButton,
+ CommandsPickerButton,
+ FilePickerButton,
+ MediaPickerButton,
+ PollPickerButton,
+} from './AttachmentTypePickerButton';
-export const AttachmentPickerSelectionBar = () => {
- const { closePicker, selectedPicker, setSelectedPicker } = useAttachmentPickerContext();
+import { useTheme } from '../../../contexts/themeContext/ThemeContext';
+import { primitives } from '../../../theme';
+const useStyles = () => {
const {
- attachmentSelectionBarHeight,
- CameraSelectorIcon,
- CreatePollIcon,
- FileSelectorIcon,
- hasCameraPicker,
- hasFilePicker,
- hasImagePicker,
- ImageSelectorIcon,
- openPollCreationDialog,
- pickFile,
- sendMessage,
- takeAndUploadImage,
- VideoRecorderSelectorIcon,
- } = useMessageInputContext();
- const { threadList } = useChannelContext();
- const { hasCreatePoll } = useMessagesContext();
- const ownCapabilities = useOwnCapabilitiesContext();
+ theme: { semantics },
+ } = useTheme();
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ container: {
+ backgroundColor: semantics.composerBg,
+ paddingBottom: primitives.spacingSm,
+ paddingHorizontal: primitives.spacingMd,
+ alignItems: 'center',
+ flexDirection: 'row',
+ },
+ }),
+ [semantics.composerBg],
+ );
+};
+export const AttachmentPickerSelectionBar = () => {
const {
theme: {
- attachmentSelectionBar: { container, icon },
+ attachmentSelectionBar: { container },
},
} = useTheme();
-
- const setImagePicker = () => {
- if (selectedPicker === 'images') {
- setSelectedPicker(undefined);
- closePicker();
- } else {
- setSelectedPicker('images');
- }
- };
-
- const openFilePicker = () => {
- setSelectedPicker(undefined);
- closePicker();
- pickFile();
- };
-
- const openPollCreationModal = () => {
- setSelectedPicker(undefined);
- closePicker();
- openPollCreationDialog?.({ sendMessage });
- };
-
- const onCameraPickerPress = () => {
- setSelectedPicker(undefined);
- closePicker();
- takeAndUploadImage(Platform.OS === 'android' ? 'image' : 'mixed');
- };
-
- const onVideoRecorderPickerPress = () => {
- setSelectedPicker(undefined);
- closePicker();
- takeAndUploadImage('video');
- };
+ const styles = useStyles();
return (
-
- {hasImagePicker ? (
-
-
-
-
-
- ) : null}
- {hasFilePicker ? (
-
-
-
-
-
- ) : null}
- {hasCameraPicker ? (
-
-
-
-
-
- ) : null}
- {hasCameraPicker && Platform.OS === 'android' ? (
-
-
-
-
-
- ) : null}
- {!threadList && hasCreatePoll && ownCapabilities.sendPoll ? ( // do not allow poll creation in threads
-
-
-
-
-
- ) : null}
+
+
+
+
+
+
);
};
diff --git a/package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx b/package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx
new file mode 100644
index 0000000000..e98622fdc5
--- /dev/null
+++ b/package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx
@@ -0,0 +1,213 @@
+import React, { useCallback, useState } from 'react';
+
+import { Platform } from 'react-native';
+
+import { PressableProps } from 'react-native-gesture-handler';
+
+import { AttachmentCommandPicker } from './AttachmentPickerContent';
+
+import {
+ useAttachmentPickerContext,
+ useChannelContext,
+ useMessageInputContext,
+ useMessagesContext,
+ useOwnCapabilitiesContext,
+} from '../../../contexts';
+import { useStableCallback } from '../../../hooks';
+import { useAttachmentPickerState } from '../../../hooks/useAttachmentPickerState';
+import {
+ Camera,
+ Picture,
+ Recorder,
+ FilePickerIcon,
+ PollThumbnail,
+ CommandsIcon,
+ IconProps,
+} from '../../../icons';
+import { Button, ButtonProps } from '../../ui';
+import { BottomSheetModal } from '../../UIComponents';
+
+export type AttachmentTypePickerButtonProps = Pick & {
+ Icon: ButtonProps['LeadingIcon'];
+} & Pick;
+
+const hitSlop = { bottom: 15, top: 15 };
+
+export const AttachmentTypePickerButton = ({
+ testID,
+ selected,
+ onPress: onPressProp,
+ Icon,
+}: AttachmentTypePickerButtonProps) => {
+ const { disableAttachmentPicker } = useAttachmentPickerContext();
+ const ButtonIcon = useCallback(
+ (props: IconProps) => Icon && ,
+ [Icon],
+ );
+
+ const onPress = useStableCallback((event) =>
+ // @ts-expect-error FIXME: RNGH does not seem to expose PressableEvent
+ (!selected || disableAttachmentPicker) && onPressProp ? onPressProp(event) : null,
+ );
+
+ return (
+
+ );
+};
+
+export const MediaPickerButton = () => {
+ const { hasImagePicker, pickAndUploadImageFromNativePicker } = useMessageInputContext();
+ const { attachmentPickerStore, disableAttachmentPicker } = useAttachmentPickerContext();
+ const { selectedPicker } = useAttachmentPickerState();
+
+ const setImagePicker = useStableCallback(() => {
+ if (disableAttachmentPicker) {
+ pickAndUploadImageFromNativePicker();
+ } else {
+ attachmentPickerStore.setSelectedPicker('images');
+ }
+ });
+
+ return hasImagePicker ? (
+
+ ) : null;
+};
+
+export const CameraPickerButton = () => {
+ const { attachmentPickerStore, disableAttachmentPicker } = useAttachmentPickerContext();
+ const { selectedPicker } = useAttachmentPickerState();
+
+ const { hasCameraPicker, takeAndUploadImage } = useMessageInputContext();
+
+ const onCameraPickerPress = useStableCallback(() => {
+ if (disableAttachmentPicker) {
+ takeAndUploadImage(Platform.OS === 'android' ? 'image' : 'mixed');
+ } else {
+ attachmentPickerStore.setSelectedPicker('camera-photo');
+ }
+ });
+
+ const onVideoRecorderPickerPress = useStableCallback(() => {
+ if (disableAttachmentPicker) {
+ takeAndUploadImage('video');
+ } else {
+ attachmentPickerStore.setSelectedPicker('camera-video');
+ }
+ });
+
+ return hasCameraPicker ? (
+ <>
+
+ {Platform.OS === 'android' ? (
+
+ ) : null}
+ >
+ ) : null;
+};
+
+export const FilePickerButton = () => {
+ const { attachmentPickerStore, disableAttachmentPicker } = useAttachmentPickerContext();
+ const { selectedPicker } = useAttachmentPickerState();
+
+ const { hasFilePicker, pickFile } = useMessageInputContext();
+
+ const openFilePicker = useStableCallback(() => {
+ if (!disableAttachmentPicker) {
+ attachmentPickerStore.setSelectedPicker('files');
+ }
+ pickFile();
+ });
+
+ return hasFilePicker ? (
+
+ ) : null;
+};
+
+export const PollPickerButton = () => {
+ const { attachmentPickerStore, disableAttachmentPicker } = useAttachmentPickerContext();
+ const { selectedPicker } = useAttachmentPickerState();
+
+ const { threadList } = useChannelContext();
+ const { hasCreatePoll } = useMessagesContext();
+ const ownCapabilities = useOwnCapabilitiesContext();
+
+ const { openPollCreationDialog, sendMessage } = useMessageInputContext();
+
+ const openPollCreationModal = useStableCallback(() => {
+ if (!disableAttachmentPicker) {
+ attachmentPickerStore.setSelectedPicker('polls');
+ }
+ openPollCreationDialog?.({ sendMessage });
+ });
+
+ return !threadList && hasCreatePoll && ownCapabilities.sendPoll ? ( // do not allow poll creation in threads
+
+ ) : null;
+};
+
+export const CommandsPickerButton = () => {
+ const [showCommandsSheet, setShowCommandsSheet] = useState(false);
+ const { hasCommands } = useMessageInputContext();
+ const { attachmentPickerStore, disableAttachmentPicker } = useAttachmentPickerContext();
+ const { selectedPicker } = useAttachmentPickerState();
+
+ const setCommandsPicker = useStableCallback(() => {
+ if (disableAttachmentPicker) {
+ setShowCommandsSheet(true);
+ } else {
+ attachmentPickerStore.setSelectedPicker('commands');
+ }
+ });
+
+ const onClose = useStableCallback(() => setShowCommandsSheet(false));
+
+ return hasCommands ? (
+ <>
+
+ {showCommandsSheet ? (
+
+
+
+ ) : null}
+ >
+ ) : null;
+};
diff --git a/package/src/components/AttachmentPicker/components/CameraSelectorIcon.tsx b/package/src/components/AttachmentPicker/components/CameraSelectorIcon.tsx
deleted file mode 100644
index 6361cbac5e..0000000000
--- a/package/src/components/AttachmentPicker/components/CameraSelectorIcon.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { Camera } from '../../../icons';
-
-export const CameraSelectorIcon = () => {
- const {
- theme: {
- colors: { grey },
- },
- } = useTheme();
-
- return ;
-};
diff --git a/package/src/components/AttachmentPicker/components/FileSelectorIcon.tsx b/package/src/components/AttachmentPicker/components/FileSelectorIcon.tsx
deleted file mode 100644
index 1627088c7a..0000000000
--- a/package/src/components/AttachmentPicker/components/FileSelectorIcon.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { Folder } from '../../../icons';
-
-export const FileSelectorIcon = () => {
- const {
- theme: {
- colors: { grey },
- },
- } = useTheme();
-
- return ;
-};
diff --git a/package/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx b/package/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx
index 8d9308861c..8085efcbb7 100644
--- a/package/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx
+++ b/package/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx
@@ -2,11 +2,12 @@ import React from 'react';
import { StyleSheet, View } from 'react-native';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { Check } from '../../../icons';
+import { primitives } from '../../../theme';
+import { BadgeNotification } from '../../ui';
const styles = StyleSheet.create({
check: {
- borderRadius: 12,
+ borderRadius: primitives.radiusMax,
height: 24,
marginRight: 8,
marginTop: 8,
@@ -14,18 +15,26 @@ const styles = StyleSheet.create({
},
});
-export const ImageOverlaySelectedComponent = () => {
+export const ImageOverlaySelectedComponent = ({ index }: { index: number }) => {
const {
theme: {
+ semantics,
attachmentPicker: {
imageOverlaySelectedComponent: { check },
},
- colors: { white },
},
} = useTheme();
return (
-
-
+
+ {index !== -1 ? : null}
);
};
diff --git a/package/src/components/AttachmentPicker/components/ImageSelectorIcon.tsx b/package/src/components/AttachmentPicker/components/ImageSelectorIcon.tsx
deleted file mode 100644
index bc9cce8603..0000000000
--- a/package/src/components/AttachmentPicker/components/ImageSelectorIcon.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { Picture } from '../../../icons';
-
-type Props = {
- selectedPicker?: 'images';
-};
-
-export const ImageSelectorIcon = ({ selectedPicker }: Props) => {
- const {
- theme: {
- colors: { accent_blue, grey },
- },
- } = useTheme();
-
- return ;
-};
diff --git a/package/src/components/AttachmentPicker/components/VideoRecorderSelectorIcon.tsx b/package/src/components/AttachmentPicker/components/VideoRecorderSelectorIcon.tsx
deleted file mode 100644
index d1e675f854..0000000000
--- a/package/src/components/AttachmentPicker/components/VideoRecorderSelectorIcon.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { Recorder } from '../../../icons';
-
-export const VideoRecorderSelectorIcon = () => {
- const {
- theme: {
- colors: { grey },
- },
- } = useTheme();
-
- return ;
-};
diff --git a/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx b/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx
index a49bfe7c7e..b37232e496 100644
--- a/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx
+++ b/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx
@@ -3,7 +3,6 @@ import {
I18nManager,
TextInput as RNTextInput,
StyleSheet,
- TextInputContentSizeChangeEvent,
TextInputProps,
TextInputSelectionChangeEvent,
} from 'react-native';
@@ -26,6 +25,7 @@ import {
} from '../../contexts/translationContext/TranslationContext';
import { useStateStore } from '../../hooks/useStateStore';
+import { useCooldownRemaining } from '../MessageInput/hooks/useCooldownRemaining';
type AutoCompleteInputPropsWithContext = TextInputProps &
Pick &
@@ -35,7 +35,7 @@ type AutoCompleteInputPropsWithContext = TextInputProps &
* This is currently passed in from MessageInput to avoid rerenders
* that would happen if we put this in the MessageInputContext
*/
- cooldownActive?: boolean;
+ cooldownRemainingSeconds?: number;
TextInputComponent?: React.ComponentType<
TextInputProps & {
ref: React.Ref | undefined;
@@ -55,18 +55,29 @@ const configStateSelector = (state: MessageComposerConfig) => ({
});
const MAX_NUMBER_OF_LINES = 5;
+const LINE_HEIGHT = 20;
+const PADDING_VERTICAL = 12;
+
+const commandPlaceHolders: Record = {
+ giphy: 'Search GIFs',
+ ban: '@username',
+ unban: '@username',
+ mute: '@username',
+ unmute: '@username',
+};
const AutoCompleteInputWithContext = (props: AutoCompleteInputPropsWithContext) => {
+ const styles = useStyles();
const {
channel,
- cooldownActive = false,
+ cooldownRemainingSeconds,
setInputBoxRef,
t,
TextInputComponent = RNTextInput,
+ placeholder,
...rest
} = props;
const [localText, setLocalText] = useState('');
- const [textHeight, setTextHeight] = useState(0);
const messageComposer = useMessageComposer();
const { textComposer } = messageComposer;
const { command, text } = useStateStore(textComposer.state, textComposerStateSelector);
@@ -109,21 +120,20 @@ const AutoCompleteInputWithContext = (props: AutoCompleteInputPropsWithContext)
const {
theme: {
- colors: { black, grey },
messageInput: { inputBox },
+ semantics,
},
} = useTheme();
const placeholderText = useMemo(() => {
- return command ? t('Search') : cooldownActive ? t('Slow mode ON') : t('Send a message');
- }, [command, cooldownActive, t]);
-
- const handleContentSizeChange = useCallback(
- ({ nativeEvent: { contentSize } }: TextInputContentSizeChangeEvent) => {
- setTextHeight(contentSize.height);
- },
- [],
- );
+ return placeholder
+ ? placeholder
+ : command
+ ? commandPlaceHolders[command.name ?? '']
+ : cooldownRemainingSeconds
+ ? t('Slow mode, wait {{seconds}}s...', { seconds: cooldownRemainingSeconds })
+ : t('Send a message');
+ }, [command, cooldownRemainingSeconds, t, placeholder]);
return (
{
- const { channel: prevChannel, cooldownActive: prevCooldownActive, t: prevT } = prevProps;
- const { channel: nextChannel, cooldownActive: nextCooldownActive, t: nextT } = nextProps;
+ const {
+ channel: prevChannel,
+ cooldownRemainingSeconds: prevCooldownRemainingSeconds,
+ t: prevT,
+ } = prevProps;
+ const {
+ channel: nextChannel,
+ cooldownRemainingSeconds: nextCooldownRemainingSeconds,
+ t: nextT,
+ } = nextProps;
const tEqual = prevT === nextT;
if (!tEqual) {
return false;
}
- const cooldownActiveEqual = prevCooldownActive === nextCooldownActive;
- if (!cooldownActiveEqual) {
+ const cooldownRemainingSecondsEqual =
+ prevCooldownRemainingSeconds === nextCooldownRemainingSeconds;
+ if (!cooldownRemainingSecondsEqual) {
return false;
}
@@ -188,6 +206,7 @@ export const AutoCompleteInput = (props: AutoCompleteInputProps) => {
const { setInputBoxRef } = useMessageInputContext();
const { t } = useTranslationContext();
const { channel } = useChannelContext();
+ const cooldownRemainingSeconds = useCooldownRemaining();
return (
{
channel,
setInputBoxRef,
t,
+ cooldownRemainingSeconds,
}}
{...props}
/>
);
};
-const styles = StyleSheet.create({
- inputBox: {
- flex: 1,
- fontSize: 16,
- includeFontPadding: false, // for android vertical text centering
- paddingVertical: 12,
- textAlignVertical: 'center', // for android vertical text centering
- },
-});
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ inputBox: {
+ color: semantics.inputTextDefault,
+ flex: 1,
+ fontSize: 16,
+ includeFontPadding: false, // for android vertical text centering
+ lineHeight: 20,
+ paddingLeft: 16,
+ paddingVertical: 12,
+ textAlignVertical: 'center', // for android vertical text centering
+ },
+ });
+ }, [semantics]);
+};
AutoCompleteInput.displayName = 'AutoCompleteInput{messageInput{inputBox}}';
diff --git a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionCommandIcon.tsx b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionCommandIcon.tsx
index 3f492e9705..0cbfcf0ca6 100644
--- a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionCommandIcon.tsx
+++ b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionCommandIcon.tsx
@@ -9,24 +9,25 @@ import { Flag, GiphyIcon, Imgur, Lightning, Mute, Sound, UserAdd, UserDelete } f
export const SuggestionCommandIcon = ({ name }: { name: CommandVariants }) => {
const {
theme: {
+ semantics,
colors: { white },
},
} = useTheme();
if (name === 'ban') {
- return ;
+ return ;
} else if (name === 'flag') {
- return ;
+ return ;
} else if (name === 'giphy') {
- return ;
+ return ;
} else if (name === 'imgur') {
- return ;
+ return ;
} else if (name === 'mute') {
- return ;
+ return ;
} else if (name === 'unban') {
- return ;
+ return ;
} else if (name === 'unmute') {
- return ;
+ return ;
} else {
return ;
}
@@ -35,7 +36,6 @@ export const SuggestionCommandIcon = ({ name }: { name: CommandVariants }) => {
export const AutoCompleteSuggestionCommandIcon = ({ name }: { name: CommandVariants }) => {
const {
theme: {
- colors: { accent_blue },
messageInput: {
suggestions: {
command: { iconContainer },
@@ -45,15 +45,7 @@ export const AutoCompleteSuggestionCommandIcon = ({ name }: { name: CommandVaria
} = useTheme();
return (
-
+
);
diff --git a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionItem.tsx b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionItem.tsx
index ba0ce85893..afc86d1c68 100644
--- a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionItem.tsx
+++ b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionItem.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useMemo } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { CommandSuggestion, TextComposerSuggestion, UserSuggestion } from 'stream-chat';
@@ -8,9 +8,10 @@ import { AutoCompleteSuggestionCommandIcon } from './AutoCompleteSuggestionComma
import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { AtMentions } from '../../icons/AtMentions';
+import { primitives } from '../../theme';
import type { Emoji } from '../../types/types';
-import { Avatar } from '../Avatar/Avatar';
+import { UserAvatar } from '../ui/Avatar/UserAvatar';
export type AutoCompleteSuggestionItemProps = {
itemProps: TextComposerSuggestion;
@@ -18,20 +19,22 @@ export type AutoCompleteSuggestionItemProps = {
};
export const MentionSuggestionItem = (item: UserSuggestion) => {
- const { id, image, name, online } = item;
+ const { id, name, online } = item;
const {
theme: {
colors: { accent_blue, black },
messageInput: {
suggestions: {
- mention: { avatarSize, column, container: mentionContainer, name: nameStyle },
+ mention: { column, container: mentionContainer, name: nameStyle },
},
},
},
} = useTheme();
+ const styles = useStyles();
+
return (
-
+
{name || id}
@@ -54,6 +57,8 @@ export const EmojiSuggestionItem = (item: Emoji) => {
},
},
} = useTheme();
+ const styles = useStyles();
+
return (
@@ -78,9 +83,10 @@ export const CommandSuggestionItem = (item: CommandSuggestion) => {
},
},
} = useTheme();
+ const styles = useStyles();
return (
-
+
{name ? : null}
{(name || '').replace(/^\w/, (char) => char.toUpperCase())}
@@ -167,36 +173,54 @@ export const AutoCompleteSuggestionItem = (props: AutoCompleteSuggestionItemProp
);
-const styles = StyleSheet.create({
- args: {
- fontSize: 14,
- },
- column: {
- flex: 1,
- justifyContent: 'space-evenly',
- paddingLeft: 8,
- },
- container: {
- alignItems: 'center',
- flexDirection: 'row',
- paddingHorizontal: 16,
- paddingVertical: 8,
- },
- name: {
- fontSize: 14,
- fontWeight: 'bold',
- paddingBottom: 2,
- },
- tag: {
- fontSize: 12,
- fontWeight: '600',
- },
- text: {
- fontSize: 14,
- },
- title: {
- fontSize: 14,
- fontWeight: 'bold',
- paddingHorizontal: 8,
- },
-});
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ args: {
+ fontSize: primitives.typographyFontSizeMd,
+ color: semantics.textTertiary,
+ },
+ column: {
+ flex: 1,
+ justifyContent: 'space-evenly',
+ paddingLeft: 8,
+ },
+ container: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ },
+ commandContainer: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ paddingHorizontal: primitives.spacingSm,
+ paddingVertical: primitives.spacingXs,
+ },
+ name: {
+ fontSize: 14,
+ fontWeight: 'bold',
+ paddingBottom: 2,
+ },
+ tag: {
+ fontSize: 12,
+ fontWeight: '600',
+ },
+ text: {
+ fontSize: 14,
+ },
+ title: {
+ fontSize: primitives.typographyFontSizeMd,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ color: semantics.textPrimary,
+ width: 80,
+ },
+ }),
+ [semantics.textPrimary, semantics.textTertiary],
+ );
+};
diff --git a/package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.js b/package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.js
index c3591fc026..945581876e 100644
--- a/package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.js
+++ b/package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.js
@@ -113,29 +113,6 @@ describe('AutoCompleteInput', () => {
});
});
- it('should style the text input with maxHeight that is set by the layout', async () => {
- const channelProps = { channel };
- const props = { numberOfLines: 10 };
-
- renderComponent({ channelProps, client, props });
-
- const { queryByTestId } = screen;
-
- const input = queryByTestId('auto-complete-text-input');
-
- act(() => {
- fireEvent(input, 'contentSizeChange', {
- nativeEvent: {
- contentSize: { height: 100 },
- },
- });
- });
-
- await waitFor(() => {
- expect(input.props.style[1].maxHeight).toBe(1000);
- });
- });
-
it('should call the textComposer setSelection when the onSelectionChange is triggered', async () => {
const { textComposer } = channel.messageComposer;
@@ -166,12 +143,12 @@ describe('AutoCompleteInput', () => {
// TODO: Add a test for command
it.each([
- { cooldownActive: false, result: 'Send a message' },
- { cooldownActive: true, result: 'Slow mode ON' },
+ { cooldownRemainingSeconds: undefined, result: 'Send a message' },
+ { cooldownRemainingSeconds: 10, result: 'Slow mode, wait 10s...' },
])('should have the placeholderText as Slow mode ON when cooldown is active', async (data) => {
const channelProps = { channel };
const props = {
- cooldownActive: data.cooldownActive,
+ cooldownRemainingSeconds: data.cooldownRemainingSeconds,
};
renderComponent({ channelProps, client, props });
diff --git a/package/src/components/Avatar/Avatar.tsx b/package/src/components/Avatar/Avatar.tsx
deleted file mode 100644
index bf7f1235d2..0000000000
--- a/package/src/components/Avatar/Avatar.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-import React, { useCallback, useMemo } from 'react';
-import {
- Image,
- ImageProps,
- ImageStyle,
- StyleProp,
- StyleSheet,
- View,
- ViewStyle,
-} from 'react-native';
-import Svg, { Circle, CircleProps } from 'react-native-svg';
-
-import { useChatConfigContext } from '../../contexts/chatConfigContext/ChatConfigContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { useLoadingImage } from '../../hooks/useLoadingImage';
-import { getResizedImageUrl } from '../../utils/getResizedImageUrl';
-
-const randomImageBaseUrl = 'https://getstream.io/random_png/';
-const randomSvgBaseUrl = 'https://getstream.io/random_svg/';
-const streamCDN = 'stream-io-cdn.com';
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- justifyContent: 'center',
- overflow: 'hidden',
- },
- presenceIndicatorContainer: {
- height: 12,
- position: 'absolute',
- right: 0,
- top: 0,
- width: 12,
- },
-});
-
-const getInitials = (fullName: string) =>
- fullName
- .split(' ')
- .slice(0, 2)
- .map((name) => name.charAt(0))
- .join('');
-
-export type AvatarProps = {
- /** size in pixels */
- size: number;
- containerStyle?: StyleProp;
- /** image url */
- image?: string;
- ImageComponent?: React.ComponentType;
- /** name of the picture, used for fallback */
- imageStyle?: StyleProp;
- name?: string;
- online?: boolean;
- presenceIndicator?: CircleProps;
- presenceIndicatorContainerStyle?: StyleProp;
- testID?: string;
-};
-
-/**
- * Avatar - A round avatar image with fallback to user's initials.
- */
-export const Avatar = (props: AvatarProps) => {
- const {
- containerStyle,
- image: imageProp,
- ImageComponent = Image,
- imageStyle,
- name,
- online,
- presenceIndicator: presenceIndicatorProp,
- presenceIndicatorContainerStyle,
- size,
- testID,
- } = props;
- const { resizableCDNHosts } = useChatConfigContext();
- const {
- theme: {
- avatar: { container, image, presenceIndicator, presenceIndicatorContainer },
- colors: { accent_green, white },
- },
- } = useTheme();
-
- const { isLoadingImageError, setLoadingImageError } = useLoadingImage();
-
- const onError = useCallback(() => {
- setLoadingImageError(true);
- }, [setLoadingImageError]);
-
- const uri = useMemo(() => {
- let imageUrl;
- if (
- !imageProp ||
- imageProp.includes(randomImageBaseUrl) ||
- imageProp.includes(randomSvgBaseUrl)
- ) {
- if (imageProp?.includes(streamCDN)) {
- imageUrl = imageProp;
- } else {
- imageUrl = `${randomImageBaseUrl}${name ? `?name=${getInitials(name)}&size=${size}` : ''}`;
- }
- } else {
- imageUrl = getResizedImageUrl({
- height: size,
- resizableCDNHosts,
- url: imageProp,
- width: size,
- });
- }
-
- return imageUrl;
- }, [imageProp, name, size, resizableCDNHosts]);
-
- return (
-
-
- {isLoadingImageError ? (
-
- ) : (
-
- )}
-
- {online && (
-
-
-
-
-
- )}
-
- );
-};
-
-Avatar.displayName = 'Avatar{avatar}';
diff --git a/package/src/components/Avatar/__tests__/Avatar.test.js b/package/src/components/Avatar/__tests__/Avatar.test.js
deleted file mode 100644
index 1af16be805..0000000000
--- a/package/src/components/Avatar/__tests__/Avatar.test.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import React from 'react';
-
-import { render, waitFor } from '@testing-library/react-native';
-
-import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
-import { Avatar } from '../Avatar';
-
-describe('Avatar', () => {
- it('should render an image with no name and default size', async () => {
- const { queryByTestId } = render(
-
-
- ,
- );
-
- await waitFor(() => {
- expect(queryByTestId('avatar-image')).toBeTruthy();
- });
- });
-
- it('should render an image with name and default size', async () => {
- const { queryByTestId } = render(
-
-
- ,
- );
-
- await waitFor(() => {
- expect(queryByTestId('avatar-image')).toBeTruthy();
- });
- });
-
- it('should render an image with custom size', async () => {
- const { queryByTestId } = render(
-
-
- ,
- );
-
- await waitFor(() => {
- expect(queryByTestId('avatar-image')).toBeTruthy();
- });
- });
-});
diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx
index 723cd9b907..c547cbda03 100644
--- a/package/src/components/Channel/Channel.tsx
+++ b/package/src/components/Channel/Channel.tsx
@@ -1,5 +1,5 @@
import React, { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { KeyboardAvoidingViewProps, StyleSheet, Text, View } from 'react-native';
+import { StyleSheet, Text, View } from 'react-native';
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
@@ -42,16 +42,10 @@ import { useCreateTypingContext } from './hooks/useCreateTypingContext';
import { useMessageListPagination } from './hooks/useMessageListPagination';
import { useTargetedMessage } from './hooks/useTargetedMessage';
-import { CameraSelectorIcon as DefaultCameraSelectorIcon } from '../../components/AttachmentPicker/components/CameraSelectorIcon';
-import { FileSelectorIcon as DefaultFileSelectorIcon } from '../../components/AttachmentPicker/components/FileSelectorIcon';
-import { ImageSelectorIcon as DefaultImageSelectorIcon } from '../../components/AttachmentPicker/components/ImageSelectorIcon';
-import { VideoRecorderSelectorIcon as DefaultVideoRecorderSelectorIcon } from '../../components/AttachmentPicker/components/VideoRecorderSelectorIcon';
-import { CreatePollIcon as DefaultCreatePollIcon } from '../../components/Poll/components/CreatePollIcon';
import {
AttachmentPickerContextValue,
AttachmentPickerProvider,
- MessageContextValue,
-} from '../../contexts';
+} from '../../contexts/attachmentPickerContext/AttachmentPickerContext';
import {
AudioPlayerContextProps,
AudioPlayerProvider,
@@ -61,6 +55,7 @@ import type { UseChannelStateValue } from '../../contexts/channelsStateContext/u
import { useChannelState } from '../../contexts/channelsStateContext/useChannelState';
import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext';
import { MessageComposerProvider } from '../../contexts/messageComposerContext/MessageComposerContext';
+import { MessageContextValue } from '../../contexts/messageContext/MessageContext';
import {
InputMessageInputContextValue,
MessageInputProvider,
@@ -88,18 +83,11 @@ import {
useTranslationContext,
} from '../../contexts/translationContext/TranslationContext';
import { TypingProvider } from '../../contexts/typingContext/TypingContext';
-import { useStableCallback, useViewport } from '../../hooks';
+import { useStableCallback } from '../../hooks';
import { useAppStateListener } from '../../hooks/useAppStateListener';
import { useAttachmentPickerBottomSheet } from '../../hooks/useAttachmentPickerBottomSheet';
import { usePrunableMessageList } from '../../hooks/usePrunableMessageList';
-import {
- LOLReaction,
- LoveReaction,
- ThumbsDownReaction,
- ThumbsUpReaction,
- WutReaction,
-} from '../../icons';
import {
isDocumentPickerAvailable,
isImageMediaLibraryAvailable,
@@ -110,6 +98,7 @@ import {
ChannelUnreadStateStore,
ChannelUnreadStateStoreType,
} from '../../state-store/channel-unread-state';
+import { MessageInputHeightStore } from '../../state-store/message-input-height-store';
import { FileTypes } from '../../types/types';
import { addReactionToLocalState } from '../../utils/addReactionToLocalState';
import { compressedImageURI } from '../../utils/compressImage';
@@ -122,9 +111,7 @@ import {
ReactionData,
} from '../../utils/utils';
import { Attachment as AttachmentDefault } from '../Attachment/Attachment';
-import { AttachmentActions as AttachmentActionsDefault } from '../Attachment/AttachmentActions';
-import { AudioAttachment as AudioAttachmentDefault } from '../Attachment/AudioAttachment';
-import { Card as CardDefault } from '../Attachment/Card';
+import { AudioAttachment as AudioAttachmentDefault } from '../Attachment/Audio';
import { FileAttachment as FileAttachmentDefault } from '../Attachment/FileAttachment';
import { FileAttachmentGroup as FileAttachmentGroupDefault } from '../Attachment/FileAttachmentGroup';
import { FileIcon as FileIconDefault } from '../Attachment/FileIcon';
@@ -133,12 +120,10 @@ import { Giphy as GiphyDefault } from '../Attachment/Giphy';
import { ImageLoadingFailedIndicator as ImageLoadingFailedIndicatorDefault } from '../Attachment/ImageLoadingFailedIndicator';
import { ImageLoadingIndicator as ImageLoadingIndicatorDefault } from '../Attachment/ImageLoadingIndicator';
import { ImageReloadIndicator as ImageReloadIndicatorDefault } from '../Attachment/ImageReloadIndicator';
+import { URLPreview as URLPreviewDefault } from '../Attachment/UrlPreview';
import { VideoThumbnail as VideoThumbnailDefault } from '../Attachment/VideoThumbnail';
-import { AttachmentPicker, AttachmentPickerProps } from '../AttachmentPicker/AttachmentPicker';
-import { AttachmentPickerBottomSheetHandle as DefaultAttachmentPickerBottomSheetHandle } from '../AttachmentPicker/components/AttachmentPickerBottomSheetHandle';
-import { AttachmentPickerError as DefaultAttachmentPickerError } from '../AttachmentPicker/components/AttachmentPickerError';
-import { AttachmentPickerErrorImage as DefaultAttachmentPickerErrorImage } from '../AttachmentPicker/components/AttachmentPickerErrorImage';
-import { AttachmentPickerIOSSelectMorePhotos as DefaultAttachmentPickerIOSSelectMorePhotos } from '../AttachmentPicker/components/AttachmentPickerIOSSelectMorePhotos';
+import { AttachmentPicker } from '../AttachmentPicker/AttachmentPicker';
+import { AttachmentPickerContent as DefaultAttachmentPickerContent } from '../AttachmentPicker/components/AttachmentPickerContent';
import { AttachmentPickerSelectionBar as DefaultAttachmentPickerSelectionBar } from '../AttachmentPicker/components/AttachmentPickerSelectionBar';
import { ImageOverlaySelectedComponent as DefaultImageOverlaySelectedComponent } from '../AttachmentPicker/components/ImageOverlaySelectedComponent';
import { AutoCompleteSuggestionHeader as AutoCompleteSuggestionHeaderDefault } from '../AutoCompleteInput/AutoCompleteSuggestionHeader';
@@ -150,16 +135,19 @@ import {
LoadingErrorProps,
} from '../Indicators/LoadingErrorIndicator';
import { LoadingIndicator as LoadingIndicatorDefault } from '../Indicators/LoadingIndicator';
-import { KeyboardCompatibleView as KeyboardCompatibleViewDefault } from '../KeyboardCompatibleView/KeyboardCompatibleView';
+import {
+ KeyboardCompatibleView as KeyboardCompatibleViewDefault,
+ KeyboardCompatibleViewProps,
+} from '../KeyboardCompatibleView/KeyboardControllerAvoidingView';
import { Message as MessageDefault } from '../Message/Message';
import { MessageAvatar as MessageAvatarDefault } from '../Message/MessageSimple/MessageAvatar';
import { MessageBlocked as MessageBlockedDefault } from '../Message/MessageSimple/MessageBlocked';
import { MessageBounce as MessageBounceDefault } from '../Message/MessageSimple/MessageBounce';
import { MessageContent as MessageContentDefault } from '../Message/MessageSimple/MessageContent';
import { MessageDeleted as MessageDeletedDefault } from '../Message/MessageSimple/MessageDeleted';
-import { MessageEditedTimestamp as MessageEditedTimestampDefault } from '../Message/MessageSimple/MessageEditedTimestamp';
import { MessageError as MessageErrorDefault } from '../Message/MessageSimple/MessageError';
import { MessageFooter as MessageFooterDefault } from '../Message/MessageSimple/MessageFooter';
+import { MessageHeader as MessageHeaderDefault } from '../Message/MessageSimple/MessageHeader';
import { MessagePinnedHeader as MessagePinnedHeaderDefault } from '../Message/MessageSimple/MessagePinnedHeader';
import { MessageReplies as MessageRepliesDefault } from '../Message/MessageSimple/MessageReplies';
import { MessageRepliesAvatars as MessageRepliesAvatarsDefault } from '../Message/MessageSimple/MessageRepliesAvatars';
@@ -170,26 +158,27 @@ import { MessageTimestamp as MessageTimestampDefault } from '../Message/MessageS
import { ReactionListBottom as ReactionListBottomDefault } from '../Message/MessageSimple/ReactionList/ReactionListBottom';
import { ReactionListTop as ReactionListTopDefault } from '../Message/MessageSimple/ReactionList/ReactionListTop';
import { StreamingMessageView as DefaultStreamingMessageView } from '../Message/MessageSimple/StreamingMessageView';
-import { AttachButton as AttachButtonDefault } from '../MessageInput/AttachButton';
-import { AttachmentUploadPreviewList as AttachmentUploadPreviewDefault } from '../MessageInput/AttachmentUploadPreviewList';
-import { CommandsButton as CommandsButtonDefault } from '../MessageInput/CommandsButton';
-import { AttachmentUploadProgressIndicator as AttachmentUploadProgressIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator';
+import { AttachmentUploadPreviewList as AttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList';
+import { FileUploadInProgressIndicator as FileUploadInProgressIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator';
+import { FileUploadRetryIndicator as FileUploadRetryIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator';
+import { FileUploadNotSupportedIndicator as FileUploadNotSupportedIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator';
+import { ImageUploadInProgressIndicator as ImageUploadInProgressIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator';
+import { ImageUploadRetryIndicator as ImageUploadRetryIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator';
+import { ImageUploadNotSupportedIndicator as ImageUploadNotSupportedIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator';
import { AudioAttachmentUploadPreview as AudioAttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview';
import { FileAttachmentUploadPreview as FileAttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview';
import { ImageAttachmentUploadPreview as ImageAttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview';
+import { VideoAttachmentUploadPreview as VideoAttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview';
import { AudioRecorder as AudioRecorderDefault } from '../MessageInput/components/AudioRecorder/AudioRecorder';
import { AudioRecordingButton as AudioRecordingButtonDefault } from '../MessageInput/components/AudioRecorder/AudioRecordingButton';
import { AudioRecordingInProgress as AudioRecordingInProgressDefault } from '../MessageInput/components/AudioRecorder/AudioRecordingInProgress';
import { AudioRecordingLockIndicator as AudioRecordingLockIndicatorDefault } from '../MessageInput/components/AudioRecorder/AudioRecordingLockIndicator';
import { AudioRecordingPreview as AudioRecordingPreviewDefault } from '../MessageInput/components/AudioRecorder/AudioRecordingPreview';
import { AudioRecordingWaveform as AudioRecordingWaveformDefault } from '../MessageInput/components/AudioRecorder/AudioRecordingWaveform';
-import { CommandInput as CommandInputDefault } from '../MessageInput/components/CommandInput';
-import { InputEditingStateHeader as InputEditingStateHeaderDefault } from '../MessageInput/components/InputEditingStateHeader';
-import { InputReplyStateHeader as InputReplyStateHeaderDefault } from '../MessageInput/components/InputReplyStateHeader';
-import { CooldownTimer as CooldownTimerDefault } from '../MessageInput/CooldownTimer';
-import { InputButtons as InputButtonsDefault } from '../MessageInput/InputButtons';
-import { MoreOptionsButton as MoreOptionsButtonDefault } from '../MessageInput/MoreOptionsButton';
-import { SendButton as SendButtonDefault } from '../MessageInput/SendButton';
+import { InputButtons as InputButtonsDefault } from '../MessageInput/components/InputButtons';
+import { AttachButton as AttachButtonDefault } from '../MessageInput/components/InputButtons/AttachButton';
+import { CooldownTimer as CooldownTimerDefault } from '../MessageInput/components/OutputButtons/CooldownTimer';
+import { SendButton as SendButtonDefault } from '../MessageInput/components/OutputButtons/SendButton';
import { SendMessageDisallowedIndicator as SendMessageDisallowedIndicatorDefault } from '../MessageInput/SendMessageDisallowedIndicator';
import { ShowThreadMessageInChannelButton as ShowThreadMessageInChannelButtonDefault } from '../MessageInput/ShowThreadMessageInChannelButton';
import { StopMessageStreamingButton as DefaultStopMessageStreamingButton } from '../MessageInput/StopMessageStreamingButton';
@@ -204,10 +193,15 @@ import { StickyHeader as StickyHeaderDefault } from '../MessageList/StickyHeader
import { TypingIndicator as TypingIndicatorDefault } from '../MessageList/TypingIndicator';
import { TypingIndicatorContainer as TypingIndicatorContainerDefault } from '../MessageList/TypingIndicatorContainer';
import { UnreadMessagesNotification as UnreadMessagesNotificationDefault } from '../MessageList/UnreadMessagesNotification';
+import { Emoji } from '../MessageMenu/EmojiPickerList';
+import { emojis } from '../MessageMenu/emojis';
import { MessageActionList as MessageActionListDefault } from '../MessageMenu/MessageActionList';
import { MessageActionListItem as MessageActionListItemDefault } from '../MessageMenu/MessageActionListItem';
import { MessageMenu as MessageMenuDefault } from '../MessageMenu/MessageMenu';
-import { MessageReactionPicker as MessageReactionPickerDefault } from '../MessageMenu/MessageReactionPicker';
+import {
+ MessageReactionPicker as MessageReactionPickerDefault,
+ toUnicodeScalarString,
+} from '../MessageMenu/MessageReactionPicker';
import { MessageUserReactions as MessageUserReactionsDefault } from '../MessageMenu/MessageUserReactions';
import { MessageUserReactionsAvatar as MessageUserReactionsAvatarDefault } from '../MessageMenu/MessageUserReactionsAvatar';
import { MessageUserReactionsItem as MessageUserReactionsItemDefault } from '../MessageMenu/MessageUserReactionsItem';
@@ -228,25 +222,34 @@ const styles = StyleSheet.create({
export const reactionData: ReactionData[] = [
{
- Icon: LoveReaction,
- type: 'love',
- },
- {
- Icon: ThumbsUpReaction,
+ Icon: ({ size = 12 }: { size?: number }) => ,
type: 'like',
+ isMain: true,
},
{
- Icon: ThumbsDownReaction,
- type: 'sad',
+ Icon: ({ size = 12 }: { size?: number }) => ,
+ type: 'haha',
+ isMain: true,
},
{
- Icon: LOLReaction,
- type: 'haha',
+ Icon: ({ size = 12 }: { size?: number }) => ,
+ type: 'love',
+ isMain: true,
},
{
- Icon: WutReaction,
+ Icon: ({ size = 12 }: { size?: number }) => ,
type: 'wow',
+ isMain: true,
},
+ {
+ Icon: ({ size = 12 }: { size?: number }) => ,
+ type: 'sad',
+ isMain: true,
+ },
+ ...emojis.map((emoji) => ({
+ Icon: ({ size = 12 }: { size?: number }) => ,
+ type: toUnicodeScalarString(emoji),
+ })),
];
/**
@@ -268,20 +271,19 @@ const debounceOptions = {
};
export type ChannelPropsWithContext = Pick &
- Partial<
- Pick
- > &
Partial<
Pick<
- AttachmentPickerProps,
- | 'AttachmentPickerError'
- | 'AttachmentPickerErrorImage'
- | 'AttachmentPickerIOSSelectMorePhotos'
+ AttachmentPickerContextValue,
+ | 'bottomInset'
+ | 'topInset'
+ | 'disableAttachmentPicker'
| 'ImageOverlaySelectedComponent'
+ | 'numberOfAttachmentPickerImageColumns'
+ | 'AttachmentPickerIOSSelectMorePhotos'
| 'attachmentPickerErrorButtonText'
| 'attachmentPickerErrorText'
| 'numberOfAttachmentImagesToLoadPerCall'
- | 'numberOfAttachmentPickerImageColumns'
+ | 'AttachmentPickerContent'
>
> &
Partial<
@@ -311,12 +313,7 @@ export type ChannelPropsWithContext = Pick &
MessagesContextValue,
| 'additionalPressableProps'
| 'Attachment'
- | 'AttachmentActions'
| 'AudioAttachment'
- | 'Card'
- | 'CardCover'
- | 'CardFooter'
- | 'CardHeader'
| 'customMessageSwipeAction'
| 'DateHeader'
| 'deletedMessagesVisibilityType'
@@ -329,7 +326,6 @@ export type ChannelPropsWithContext = Pick &
| 'FlatList'
| 'forceAlignMessages'
| 'Gallery'
- | 'getMessagesGroupStyles'
| 'getMessageGroupStyle'
| 'Giphy'
| 'giphyVersion'
@@ -349,7 +345,6 @@ export type ChannelPropsWithContext = Pick &
| 'InlineDateSeparator'
| 'InlineUnreadIndicator'
| 'isAttachmentEqual'
- | 'legacyImageViewerSwipeBehaviour'
| 'ImageLoadingFailedIndicator'
| 'ImageLoadingIndicator'
| 'ImageReloadIndicator'
@@ -364,7 +359,6 @@ export type ChannelPropsWithContext = Pick &
| 'MessageContent'
| 'messageContentOrder'
| 'MessageDeleted'
- | 'MessageEditedTimestamp'
| 'MessageError'
| 'MessageFooter'
| 'MessageHeader'
@@ -415,7 +409,7 @@ export type ChannelPropsWithContext = Pick &
/**
* Additional props passed to keyboard avoiding view
*/
- additionalKeyboardAvoidingViewProps?: Partial;
+ additionalKeyboardAvoidingViewProps?: Partial;
/**
* When true, disables the KeyboardCompatibleView wrapper
*
@@ -471,7 +465,7 @@ export type ChannelPropsWithContext = Pick &
* When true, messageList will be scrolled at first unread message, when opened.
*/
initialScrollToFirstUnreadMessage?: boolean;
- keyboardBehavior?: KeyboardAvoidingViewProps['behavior'];
+ keyboardBehavior?: KeyboardCompatibleViewProps['behavior'];
/**
* Custom wrapper component that handles height adjustment of Channel component when keyboard is opened or dismissed
* Default component (accepts the same props): [KeyboardCompatibleView](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/KeyboardCompatibleView/KeyboardCompatibleView.tsx)
@@ -491,7 +485,7 @@ export type ChannelPropsWithContext = Pick &
* />
* ```
*/
- KeyboardCompatibleView?: React.ComponentType;
+ KeyboardCompatibleView?: React.ComponentType;
keyboardVerticalOffset?: number;
/**
* Custom loading error indicator to override the Stream default
@@ -543,9 +537,8 @@ export type ChannelPropsWithContext = Pick &
>;
const ChannelWithContext = (props: PropsWithChildren) => {
- const { vh } = useViewport();
-
const {
+ disableAttachmentPicker = !isImageMediaLibraryAvailable(),
additionalKeyboardAvoidingViewProps,
additionalPressableProps,
additionalTextInputProps,
@@ -554,15 +547,12 @@ const ChannelWithContext = (props: PropsWithChildren) =
asyncMessagesLockDistance = 50,
asyncMessagesMinimumPressDuration = 500,
asyncMessagesMultiSendEnabled = true,
- asyncMessagesSlideToCancelDistance = 100,
+ asyncMessagesSlideToCancelDistance = 75,
AttachButton = AttachButtonDefault,
Attachment = AttachmentDefault,
- AttachmentActions = AttachmentActionsDefault,
- AttachmentPickerBottomSheetHandle = DefaultAttachmentPickerBottomSheetHandle,
- attachmentPickerBottomSheetHandleHeight = 20,
- attachmentPickerBottomSheetHeight = vh(45),
+ attachmentPickerBottomSheetHeight = disableAttachmentPicker ? 72 : 333,
AttachmentPickerSelectionBar = DefaultAttachmentPickerSelectionBar,
- attachmentSelectionBarHeight = 52,
+ attachmentSelectionBarHeight = 72,
AudioAttachment = AudioAttachmentDefault,
AudioAttachmentUploadPreview = AudioAttachmentUploadPreviewDefault,
AudioRecorder = AudioRecorderDefault,
@@ -574,37 +564,24 @@ const ChannelWithContext = (props: PropsWithChildren) =
AutoCompleteSuggestionHeader = AutoCompleteSuggestionHeaderDefault,
AutoCompleteSuggestionItem = AutoCompleteSuggestionItemDefault,
AutoCompleteSuggestionList = AutoCompleteSuggestionListDefault,
- AttachmentPickerError = DefaultAttachmentPickerError,
- AttachmentPickerErrorImage = DefaultAttachmentPickerErrorImage,
- AttachmentPickerIOSSelectMorePhotos = DefaultAttachmentPickerIOSSelectMorePhotos,
AttachmentUploadPreviewList = AttachmentUploadPreviewDefault,
ImageOverlaySelectedComponent = DefaultImageOverlaySelectedComponent,
attachmentPickerErrorButtonText,
attachmentPickerErrorText,
- numberOfAttachmentImagesToLoadPerCall = 60,
+ numberOfAttachmentImagesToLoadPerCall = 25,
numberOfAttachmentPickerImageColumns = 3,
-
+ giphyVersion = 'fixed_height',
bottomInset = 0,
- CameraSelectorIcon = DefaultCameraSelectorIcon,
- FileSelectorIcon = DefaultFileSelectorIcon,
- CreatePollIcon = DefaultCreatePollIcon,
- ImageSelectorIcon = DefaultImageSelectorIcon,
- VideoRecorderSelectorIcon = DefaultVideoRecorderSelectorIcon,
- Card = CardDefault,
- CardCover,
- CardFooter,
- CardHeader,
channel,
children,
client,
- CommandsButton = CommandsButtonDefault,
compressImageQuality,
CooldownTimer = CooldownTimerDefault,
CreatePollContent,
+ createPollOptionGap,
customMessageSwipeAction,
DateHeader = DateHeaderDefault,
deletedMessagesVisibilityType = 'always',
- disableAttachmentPicker = !isImageMediaLibraryAvailable(),
disableKeyboardCompatibleView = false,
disableTypingIndicator,
dismissKeyboardOnMessageTouch = true,
@@ -623,13 +600,17 @@ const ChannelWithContext = (props: PropsWithChildren) =
FileAttachmentUploadPreview = FileAttachmentUploadPreviewDefault,
FileAttachmentGroup = FileAttachmentGroupDefault,
FileAttachmentIcon = FileIconDefault,
+ FileUploadInProgressIndicator = FileUploadInProgressIndicatorDefault,
+ FileUploadRetryIndicator = FileUploadRetryIndicatorDefault,
+ FileUploadNotSupportedIndicator = FileUploadNotSupportedIndicatorDefault,
+ ImageUploadInProgressIndicator = ImageUploadInProgressIndicatorDefault,
+ ImageUploadRetryIndicator = ImageUploadRetryIndicatorDefault,
+ ImageUploadNotSupportedIndicator = ImageUploadNotSupportedIndicatorDefault,
FlatList = NativeHandlers.FlatList,
forceAlignMessages,
Gallery = GalleryDefault,
- getMessagesGroupStyles,
getMessageGroupStyle,
Giphy = GiphyDefault,
- giphyVersion = 'fixed_height',
handleAttachButtonPress,
handleBan,
handleCopy,
@@ -661,15 +642,11 @@ const ChannelWithContext = (props: PropsWithChildren) =
InlineUnreadIndicator = InlineUnreadIndicatorDefault,
Input,
InputButtons = InputButtonsDefault,
- InputEditingStateHeader = InputEditingStateHeaderDefault,
- CommandInput = CommandInputDefault,
- InputReplyStateHeader = InputReplyStateHeaderDefault,
isAttachmentEqual,
isMessageAIGenerated = () => false,
keyboardBehavior,
KeyboardCompatibleView = KeyboardCompatibleViewDefault,
keyboardVerticalOffset,
- legacyImageViewerSwipeBehaviour = false,
LoadingErrorIndicator = LoadingErrorIndicatorDefault,
LoadingIndicator = LoadingIndicatorDefault,
loadingMore: loadingMoreProp,
@@ -691,15 +668,15 @@ const ChannelWithContext = (props: PropsWithChildren) =
'files',
'poll',
'ai_text',
- 'text',
'attachments',
+ 'text',
'location',
],
MessageDeleted = MessageDeletedDefault,
- MessageEditedTimestamp = MessageEditedTimestampDefault,
MessageError = MessageErrorDefault,
+ messageInputFloating = false,
MessageFooter = MessageFooterDefault,
- MessageHeader,
+ MessageHeader = MessageHeaderDefault,
messageId,
MessageList = MessageListDefault,
MessageLocation,
@@ -719,7 +696,6 @@ const ChannelWithContext = (props: PropsWithChildren) =
MessageUserReactions = MessageUserReactionsDefault,
MessageUserReactionsAvatar = MessageUserReactionsAvatarDefault,
MessageUserReactionsItem = MessageUserReactionsItemDefault,
- MoreOptionsButton = MoreOptionsButtonDefault,
myMessageTheme,
NetworkDownIndicator = NetworkDownIndicatorDefault,
// TODO: Think about this one
@@ -757,13 +733,13 @@ const ChannelWithContext = (props: PropsWithChildren) =
TypingIndicator = TypingIndicatorDefault,
TypingIndicatorContainer = TypingIndicatorContainerDefault,
UnreadMessagesNotification = UnreadMessagesNotificationDefault,
- AttachmentUploadProgressIndicator = AttachmentUploadProgressIndicatorDefault,
- UrlPreview = CardDefault,
- VideoAttachmentUploadPreview = FileAttachmentUploadPreviewDefault,
+ UrlPreview = URLPreviewDefault,
+ VideoAttachmentUploadPreview = VideoAttachmentUploadPreviewDefault,
VideoThumbnail = VideoThumbnailDefault,
isOnline,
maximumMessageLimit,
initializeOnMount = true,
+ AttachmentPickerContent = DefaultAttachmentPickerContent,
} = props;
const { thread: threadProps, threadInstance } = threadFromProps;
@@ -785,6 +761,8 @@ const ChannelWithContext = (props: PropsWithChildren) =
const [threadHasMore, setThreadHasMore] = useState(true);
const [threadLoadingMore, setThreadLoadingMore] = useState(false);
const [channelUnreadStateStore] = useState(new ChannelUnreadStateStore());
+ const [messageInputHeightStore] = useState(new MessageInputHeightStore());
+ // TODO: Think if we can remove this and just rely on the channelUnreadStateStore everywhere.
const setChannelUnreadState = useCallback(
(data: ChannelUnreadStateStoreType['channelUnreadState']) => {
channelUnreadStateStore.channelUnreadState = data;
@@ -1147,7 +1125,8 @@ const ChannelWithContext = (props: PropsWithChildren) =
if (channelMessagesState?.messages) {
await channel?.watch({
messages: {
- limit: channelMessagesState.messages.length + 30,
+ // Do we want to reduce this to the default as well ?
+ limit: channelMessagesState.messages.length,
},
});
channel.offlineMode = false;
@@ -1730,49 +1709,46 @@ const ChannelWithContext = (props: PropsWithChildren) =
}
});
- const attachmentPickerProps = useMemo(
+ const handleClosePicker = useStableCallback(() => closePicker(bottomSheetRef));
+ const handleOpenPicker = useStableCallback(() => openPicker(bottomSheetRef));
+
+ const attachmentPickerContext = useMemo(
() => ({
- AttachmentPickerBottomSheetHandle,
- attachmentPickerBottomSheetHandleHeight,
+ bottomInset,
+ bottomSheetRef,
+ closePicker: handleClosePicker,
+ disableAttachmentPicker,
+ openPicker: handleOpenPicker,
+ topInset,
+ ImageOverlaySelectedComponent,
+ AttachmentPickerSelectionBar,
+ numberOfAttachmentPickerImageColumns,
attachmentPickerBottomSheetHeight,
- AttachmentPickerError,
attachmentPickerErrorButtonText,
- AttachmentPickerErrorImage,
attachmentPickerErrorText,
- AttachmentPickerIOSSelectMorePhotos,
attachmentSelectionBarHeight,
- ImageOverlaySelectedComponent,
numberOfAttachmentImagesToLoadPerCall,
- numberOfAttachmentPickerImageColumns,
+ AttachmentPickerContent,
}),
[
- AttachmentPickerBottomSheetHandle,
- attachmentPickerBottomSheetHandleHeight,
+ bottomInset,
+ bottomSheetRef,
+ handleClosePicker,
+ disableAttachmentPicker,
+ handleOpenPicker,
+ topInset,
+ ImageOverlaySelectedComponent,
+ AttachmentPickerSelectionBar,
+ numberOfAttachmentPickerImageColumns,
attachmentPickerBottomSheetHeight,
- AttachmentPickerError,
attachmentPickerErrorButtonText,
- AttachmentPickerErrorImage,
attachmentPickerErrorText,
- AttachmentPickerIOSSelectMorePhotos,
attachmentSelectionBarHeight,
- ImageOverlaySelectedComponent,
numberOfAttachmentImagesToLoadPerCall,
- numberOfAttachmentPickerImageColumns,
+ AttachmentPickerContent,
],
);
- const attachmentPickerContext = useMemo(
- () => ({
- bottomInset,
- bottomSheetRef,
- closePicker: () => closePicker(bottomSheetRef),
- disableAttachmentPicker,
- openPicker: () => openPicker(bottomSheetRef),
- topInset,
- }),
- [bottomInset, bottomSheetRef, closePicker, openPicker, topInset, disableAttachmentPicker],
- );
-
const ownCapabilitiesContext = useCreateOwnCapabilitiesContext({
channel,
overrideCapabilities: overrideOwnCapabilities,
@@ -1780,7 +1756,6 @@ const ChannelWithContext = (props: PropsWithChildren) =
const channelContext = useCreateChannelContext({
channel,
- channelUnreadState: channelUnreadStateStore.channelUnreadState,
channelUnreadStateStore,
disabled: !!channel?.data?.frozen,
EmptyStateIndicator,
@@ -1836,13 +1811,10 @@ const ChannelWithContext = (props: PropsWithChildren) =
asyncMessagesMultiSendEnabled,
asyncMessagesSlideToCancelDistance,
AttachButton,
- AttachmentPickerBottomSheetHandle,
- attachmentPickerBottomSheetHandleHeight,
attachmentPickerBottomSheetHeight,
AttachmentPickerSelectionBar,
attachmentSelectionBarHeight,
AttachmentUploadPreviewList,
- AttachmentUploadProgressIndicator,
AudioAttachmentUploadPreview,
AudioRecorder,
audioRecordingEnabled,
@@ -1853,30 +1825,30 @@ const ChannelWithContext = (props: PropsWithChildren) =
AutoCompleteSuggestionHeader,
AutoCompleteSuggestionItem,
AutoCompleteSuggestionList,
- CameraSelectorIcon,
channelId,
- CommandInput,
- CommandsButton,
compressImageQuality,
CooldownTimer,
CreatePollContent,
- CreatePollIcon,
+ createPollOptionGap,
doFileUploadRequest,
editMessage,
FileAttachmentUploadPreview,
- FileSelectorIcon,
+ FileUploadInProgressIndicator,
+ FileUploadRetryIndicator,
+ FileUploadNotSupportedIndicator,
+ ImageUploadInProgressIndicator,
+ ImageUploadRetryIndicator,
+ ImageUploadNotSupportedIndicator,
handleAttachButtonPress,
hasCameraPicker,
hasCommands: hasCommands ?? !!clientChannelConfig?.commands?.length,
hasFilePicker,
hasImagePicker,
ImageAttachmentUploadPreview,
- ImageSelectorIcon,
Input,
InputButtons,
- InputEditingStateHeader,
- InputReplyStateHeader,
- MoreOptionsButton,
+ messageInputFloating,
+ messageInputHeightStore,
openPollCreationDialog,
SendButton,
sendMessage,
@@ -1886,7 +1858,6 @@ const ChannelWithContext = (props: PropsWithChildren) =
StartAudioRecordingButton,
StopMessageStreamingButton,
VideoAttachmentUploadPreview,
- VideoRecorderSelectorIcon,
});
const messageListContext = useCreatePaginatedMessageListContext({
@@ -1907,12 +1878,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
const messagesContext = useCreateMessagesContext({
additionalPressableProps,
Attachment,
- AttachmentActions,
AudioAttachment,
- Card,
- CardCover,
- CardFooter,
- CardHeader,
channelId,
customMessageSwipeAction,
DateHeader,
@@ -1930,7 +1896,6 @@ const ChannelWithContext = (props: PropsWithChildren) =
forceAlignMessages,
Gallery,
getMessageGroupStyle,
- getMessagesGroupStyles,
Giphy,
giphyVersion,
handleBan,
@@ -1956,7 +1921,6 @@ const ChannelWithContext = (props: PropsWithChildren) =
InlineUnreadIndicator,
isAttachmentEqual,
isMessageAIGenerated,
- legacyImageViewerSwipeBehaviour,
markdownRules,
Message,
MessageActionList,
@@ -1968,7 +1932,6 @@ const ChannelWithContext = (props: PropsWithChildren) =
MessageContent,
messageContentOrder,
MessageDeleted,
- MessageEditedTimestamp,
MessageError,
MessageFooter,
MessageHeader,
@@ -2079,9 +2042,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
{children}
- {!disableAttachmentPicker && (
-
- )}
+
diff --git a/package/src/components/Channel/__tests__/isAttachmentEqualHandler.test.js b/package/src/components/Channel/__tests__/isAttachmentEqualHandler.test.js
index b7b71edb4f..703a15a6b3 100644
--- a/package/src/components/Channel/__tests__/isAttachmentEqualHandler.test.js
+++ b/package/src/components/Channel/__tests__/isAttachmentEqualHandler.test.js
@@ -50,7 +50,8 @@ describe('isAttachmentEqualHandler', () => {
const getMessageWithCustomFields = () => {
const isAttachmentEqualHandler = (prevProps, nextProps) => {
- const propsEqual = prevProps.customField === nextProps.customField;
+ const propsEqual =
+ prevProps.customField === nextProps.customField && prevProps.type === nextProps.type;
if (!propsEqual) {
return false;
}
@@ -61,7 +62,7 @@ describe('isAttachmentEqualHandler', () => {
{
+ UrlPreview={({ attachment: { customField, type } }) => {
if (type === 'test') {
return {customField};
}
diff --git a/package/src/components/Channel/__tests__/ownCapabilities.test.js b/package/src/components/Channel/__tests__/ownCapabilities.test.js
index 876bb5b483..b12e2f874d 100644
--- a/package/src/components/Channel/__tests__/ownCapabilities.test.js
+++ b/package/src/components/Channel/__tests__/ownCapabilities.test.js
@@ -1,6 +1,8 @@
import React from 'react';
import { FlatList } from 'react-native';
+import { SafeAreaProvider } from 'react-native-safe-area-context';
+
import { act, fireEvent, render, waitFor } from '@testing-library/react-native';
import { OverlayProvider } from '../../../contexts/overlayContext/OverlayProvider';
@@ -47,14 +49,16 @@ describe('Own capabilities', () => {
});
const getComponent = (props = {}) => (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
const generateChannelWithCapabilities = async (capabilities = []) => {
diff --git a/package/src/components/Channel/__tests__/useMessageListPagination.test.js b/package/src/components/Channel/__tests__/useMessageListPagination.test.js
index a41501fa39..d782de22cc 100644
--- a/package/src/components/Channel/__tests__/useMessageListPagination.test.js
+++ b/package/src/components/Channel/__tests__/useMessageListPagination.test.js
@@ -161,8 +161,10 @@ describe('useMessageListPagination', () => {
await waitFor(() => {
expect(queryFn).toHaveBeenCalledWith({
- messages: { id_lt: messages[0].id, limit: 20 },
- watchers: { limit: 20 },
+ messages: { id_lt: messages[0].id, limit: 10 },
+ watchers: {
+ limit: 10,
+ },
});
expect(result.current.state.hasMore).toBe(true);
expect(result.current.state.messages.length).toBe(40);
diff --git a/package/src/components/Channel/hooks/useCreateChannelContext.ts b/package/src/components/Channel/hooks/useCreateChannelContext.ts
index d47c70fc5d..824f30cab5 100644
--- a/package/src/components/Channel/hooks/useCreateChannelContext.ts
+++ b/package/src/components/Channel/hooks/useCreateChannelContext.ts
@@ -4,7 +4,6 @@ import type { ChannelContextValue } from '../../../contexts/channelContext/Chann
export const useCreateChannelContext = ({
channel,
- channelUnreadState,
channelUnreadStateStore,
disabled,
EmptyStateIndicator,
@@ -51,7 +50,6 @@ export const useCreateChannelContext = ({
const channelContext: ChannelContextValue = useMemo(
() => ({
channel,
- channelUnreadState,
channelUnreadStateStore,
disabled,
EmptyStateIndicator,
diff --git a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts
index 5c5d4a0607..3046035e6b 100644
--- a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts
+++ b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts
@@ -10,13 +10,10 @@ export const useCreateInputMessageInputContext = ({
asyncMessagesMultiSendEnabled,
asyncMessagesSlideToCancelDistance,
AttachButton,
- AttachmentPickerBottomSheetHandle,
- attachmentPickerBottomSheetHandleHeight,
attachmentPickerBottomSheetHeight,
AttachmentPickerSelectionBar,
attachmentSelectionBarHeight,
AttachmentUploadPreviewList,
- AttachmentUploadProgressIndicator,
AudioAttachmentUploadPreview,
AudioRecorder,
audioRecordingEnabled,
@@ -28,29 +25,29 @@ export const useCreateInputMessageInputContext = ({
AutoCompleteSuggestionItem,
AutoCompleteSuggestionList,
channelId,
- CameraSelectorIcon,
- CommandInput,
- CommandsButton,
compressImageQuality,
CooldownTimer,
CreatePollContent,
- CreatePollIcon,
+ createPollOptionGap,
doFileUploadRequest,
editMessage,
FileAttachmentUploadPreview,
- FileSelectorIcon,
+ FileUploadInProgressIndicator,
+ FileUploadRetryIndicator,
+ FileUploadNotSupportedIndicator,
+ ImageUploadInProgressIndicator,
+ ImageUploadRetryIndicator,
+ ImageUploadNotSupportedIndicator,
handleAttachButtonPress,
hasCameraPicker,
hasCommands,
hasFilePicker,
hasImagePicker,
ImageAttachmentUploadPreview,
- ImageSelectorIcon,
Input,
InputButtons,
- InputEditingStateHeader,
- InputReplyStateHeader,
- MoreOptionsButton,
+ messageInputFloating,
+ messageInputHeightStore,
openPollCreationDialog,
SendButton,
sendMessage,
@@ -61,7 +58,6 @@ export const useCreateInputMessageInputContext = ({
StartAudioRecordingButton,
StopMessageStreamingButton,
VideoAttachmentUploadPreview,
- VideoRecorderSelectorIcon,
}: InputMessageInputContextValue & {
/**
* To ensure we allow re-render, when channel is changed
@@ -77,13 +73,10 @@ export const useCreateInputMessageInputContext = ({
asyncMessagesMultiSendEnabled,
asyncMessagesSlideToCancelDistance,
AttachButton,
- AttachmentPickerBottomSheetHandle,
- attachmentPickerBottomSheetHandleHeight,
attachmentPickerBottomSheetHeight,
AttachmentPickerSelectionBar,
attachmentSelectionBarHeight,
AttachmentUploadPreviewList,
- AttachmentUploadProgressIndicator,
AudioAttachmentUploadPreview,
AudioRecorder,
audioRecordingEnabled,
@@ -94,29 +87,29 @@ export const useCreateInputMessageInputContext = ({
AutoCompleteSuggestionHeader,
AutoCompleteSuggestionItem,
AutoCompleteSuggestionList,
- CameraSelectorIcon,
- CommandInput,
- CommandsButton,
compressImageQuality,
CooldownTimer,
CreatePollContent,
- CreatePollIcon,
+ createPollOptionGap,
doFileUploadRequest,
editMessage,
FileAttachmentUploadPreview,
- FileSelectorIcon,
+ FileUploadInProgressIndicator,
+ FileUploadRetryIndicator,
+ FileUploadNotSupportedIndicator,
+ ImageUploadInProgressIndicator,
+ ImageUploadRetryIndicator,
+ ImageUploadNotSupportedIndicator,
handleAttachButtonPress,
hasCameraPicker,
hasCommands,
hasFilePicker,
hasImagePicker,
ImageAttachmentUploadPreview,
- ImageSelectorIcon,
Input,
InputButtons,
- InputEditingStateHeader,
- InputReplyStateHeader,
- MoreOptionsButton,
+ messageInputFloating,
+ messageInputHeightStore,
openPollCreationDialog,
SendButton,
sendMessage,
@@ -127,7 +120,6 @@ export const useCreateInputMessageInputContext = ({
StartAudioRecordingButton,
StopMessageStreamingButton,
VideoAttachmentUploadPreview,
- VideoRecorderSelectorIcon,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts
index 690a34a23d..1fba6d9da3 100644
--- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts
+++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts
@@ -5,12 +5,7 @@ import type { MessagesContextValue } from '../../../contexts/messagesContext/Mes
export const useCreateMessagesContext = ({
additionalPressableProps,
Attachment,
- AttachmentActions,
AudioAttachment,
- Card,
- CardCover,
- CardFooter,
- CardHeader,
channelId,
customMessageSwipeAction,
DateHeader,
@@ -28,7 +23,6 @@ export const useCreateMessagesContext = ({
forceAlignMessages,
Gallery,
getMessageGroupStyle,
- getMessagesGroupStyles,
Giphy,
giphyVersion,
handleBan,
@@ -53,7 +47,6 @@ export const useCreateMessagesContext = ({
InlineUnreadIndicator,
isAttachmentEqual,
isMessageAIGenerated,
- legacyImageViewerSwipeBehaviour,
markdownRules,
Message,
MessageActionList,
@@ -65,7 +58,6 @@ export const useCreateMessagesContext = ({
MessageContent,
messageContentOrder,
MessageDeleted,
- MessageEditedTimestamp,
MessageError,
MessageFooter,
MessageHeader,
@@ -126,12 +118,7 @@ export const useCreateMessagesContext = ({
() => ({
additionalPressableProps,
Attachment,
- AttachmentActions,
AudioAttachment,
- Card,
- CardCover,
- CardFooter,
- CardHeader,
customMessageSwipeAction,
DateHeader,
deletedMessagesVisibilityType,
@@ -148,7 +135,6 @@ export const useCreateMessagesContext = ({
forceAlignMessages,
Gallery,
getMessageGroupStyle,
- getMessagesGroupStyles,
Giphy,
giphyVersion,
handleBan,
@@ -173,7 +159,6 @@ export const useCreateMessagesContext = ({
InlineUnreadIndicator,
isAttachmentEqual,
isMessageAIGenerated,
- legacyImageViewerSwipeBehaviour,
markdownRules,
Message,
MessageActionList,
@@ -185,7 +170,6 @@ export const useCreateMessagesContext = ({
MessageContent,
messageContentOrder,
MessageDeleted,
- MessageEditedTimestamp,
MessageError,
MessageFooter,
MessageHeader,
diff --git a/package/src/components/Channel/hooks/useMessageListPagination.tsx b/package/src/components/Channel/hooks/useMessageListPagination.tsx
index 4c6e454e20..df08fbd6b0 100644
--- a/package/src/components/Channel/hooks/useMessageListPagination.tsx
+++ b/package/src/components/Channel/hooks/useMessageListPagination.tsx
@@ -74,7 +74,7 @@ export const useMessageListPagination = ({ channel }: { channel: Channel }) => {
/**
* This function loads more messages before the first message in current channel state.
*/
- const loadMore = useStableCallback(async (limit: number = 20) => {
+ const loadMore = useStableCallback(async (limit: number = 10) => {
if (!channel.state.messagePagination.hasPrev) {
return;
}
diff --git a/package/src/components/ChannelList/Skeleton.tsx b/package/src/components/ChannelList/Skeleton.tsx
index 088a15a9fc..f709931e68 100644
--- a/package/src/components/ChannelList/Skeleton.tsx
+++ b/package/src/components/ChannelList/Skeleton.tsx
@@ -17,6 +17,8 @@ const paddingLarge = 16;
const paddingMedium = 12;
const paddingSmall = 8;
+const AnimatedPath = Animated.createAnimatedComponent(Path);
+
const styles = StyleSheet.create({
background: {
height: 64,
@@ -44,7 +46,8 @@ export const Skeleton = () => {
height = 64,
maskFillColor,
},
- colors: { border, grey_gainsboro, white_snow },
+ colors: { grey_gainsboro, white_snow },
+ semantics,
},
} = useTheme();
@@ -124,7 +127,7 @@ export const Skeleton = () => {
return (
@@ -148,7 +151,7 @@ export const Skeleton = () => {
-
+
);
diff --git a/package/src/components/ChannelList/hooks/usePaginatedChannels.ts b/package/src/components/ChannelList/hooks/usePaginatedChannels.ts
index 4fd5b1ffaf..27af07468f 100644
--- a/package/src/components/ChannelList/hooks/usePaginatedChannels.ts
+++ b/package/src/components/ChannelList/hooks/usePaginatedChannels.ts
@@ -13,8 +13,6 @@ import { useChatContext } from '../../../contexts/chatContext/ChatContext';
import { useStateStore } from '../../../hooks';
import { useIsMountedRef } from '../../../hooks/useIsMountedRef';
-import { MAX_QUERY_CHANNELS_LIMIT } from '../utils';
-
type Parameters = {
channelManager: ChannelManager;
enableOfflineSupport: boolean;
@@ -24,10 +22,6 @@ type Parameters = {
sort: ChannelSort;
};
-const DEFAULT_OPTIONS = {
- message_limit: 10,
-};
-
const RETRY_INTERVAL_IN_MS = 5000;
type QueryType = 'queryLocalDB' | 'reload' | 'refresh' | 'loadChannels';
@@ -46,7 +40,7 @@ export const usePaginatedChannels = ({
channelManager,
enableOfflineSupport,
filters = {},
- options = DEFAULT_OPTIONS,
+ options = {},
sort = {},
}: Parameters) => {
const [staticChannelsActive, setStaticChannelsActive] = useState(false);
@@ -99,7 +93,6 @@ export const usePaginatedChannels = ({
setActiveQueryType(queryType);
const newOptions = {
- limit: options?.limit ?? MAX_QUERY_CHANNELS_LIMIT,
offset: 0,
...options,
};
diff --git a/package/src/components/ChannelPreview/ChannelAvatar.tsx b/package/src/components/ChannelPreview/ChannelAvatar.tsx
deleted file mode 100644
index e38b3e428e..0000000000
--- a/package/src/components/ChannelPreview/ChannelAvatar.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React from 'react';
-
-import type { ChannelPreviewProps } from './ChannelPreview';
-import { useChannelPreviewDisplayAvatar } from './hooks/useChannelPreviewDisplayAvatar';
-import { useChannelPreviewDisplayPresence } from './hooks/useChannelPreviewDisplayPresence';
-
-import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-
-import { Avatar } from '../Avatar/Avatar';
-import { GroupAvatar } from '../Avatar/GroupAvatar';
-
-export type ChannelAvatarProps = Pick & {
- /**
- * The size of the avatar.
- */
- size?: number;
-};
-
-/**
- * This UI component displays an avatar for a particular channel.
- */
-export const ChannelAvatarWithContext = (
- props: ChannelAvatarProps & Pick,
-) => {
- const { channel, ImageComponent, size: propSize } = props;
- const {
- theme: {
- channelPreview: {
- avatar: { size: themeSize },
- },
- },
- } = useTheme();
-
- const size = propSize || themeSize;
-
- const displayAvatar = useChannelPreviewDisplayAvatar(channel);
- const displayPresence = useChannelPreviewDisplayPresence(channel);
-
- if (displayAvatar.images) {
- return (
-
- );
- }
-
- return (
-
- );
-};
-
-export const ChannelAvatar = (props: ChannelAvatarProps) => {
- const { ImageComponent } = useChatContext();
-
- return ;
-};
diff --git a/package/src/components/ChannelPreview/ChannelPreviewMessenger.tsx b/package/src/components/ChannelPreview/ChannelPreviewMessenger.tsx
index 65f87b0b6b..4193be9778 100644
--- a/package/src/components/ChannelPreview/ChannelPreviewMessenger.tsx
+++ b/package/src/components/ChannelPreview/ChannelPreviewMessenger.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
-import { ChannelAvatar } from './ChannelAvatar';
import type { ChannelPreviewProps } from './ChannelPreview';
import { ChannelPreviewMessage } from './ChannelPreviewMessage';
import { ChannelPreviewMutedStatus } from './ChannelPreviewMutedStatus';
@@ -18,6 +17,7 @@ import {
} from '../../contexts/channelsContext/ChannelsContext';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { useViewport } from '../../hooks/useViewport';
+import { ChannelAvatar } from '../ui/Avatar/ChannelAvatar';
const styles = StyleSheet.create({
container: {
@@ -119,7 +119,8 @@ const ChannelPreviewMessengerWithContext = (props: ChannelPreviewMessengerPropsW
const {
theme: {
channelPreview: { container, contentContainer, row, title },
- colors: { border, white_snow },
+ colors: { white_snow },
+ semantics,
},
} = useTheme();
@@ -138,12 +139,12 @@ const ChannelPreviewMessengerWithContext = (props: ChannelPreviewMessengerPropsW
style={[
// { opacity: pressed ? 0.5 : 1 },
styles.container,
- { backgroundColor: white_snow, borderBottomColor: border },
+ { backgroundColor: white_snow, borderBottomColor: semantics.borderCoreDefault },
container,
]}
testID='channel-preview-button'
>
-
+
forceUpdate,
maxUnreadCount,
onSelect,
- PreviewAvatar,
PreviewMessage,
PreviewMutedStatus,
PreviewStatus,
@@ -199,7 +199,6 @@ export const ChannelPreviewMessenger = (props: ChannelPreviewMessengerProps) =>
forceUpdate,
maxUnreadCount,
onSelect,
- PreviewAvatar,
PreviewMessage,
PreviewMutedStatus,
PreviewStatus,
diff --git a/package/src/components/ChannelPreview/ChannelPreviewUnreadCount.tsx b/package/src/components/ChannelPreview/ChannelPreviewUnreadCount.tsx
index cd592ac958..59212ec218 100644
--- a/package/src/components/ChannelPreview/ChannelPreviewUnreadCount.tsx
+++ b/package/src/components/ChannelPreview/ChannelPreviewUnreadCount.tsx
@@ -1,10 +1,9 @@
import React from 'react';
-import { StyleSheet, Text, View } from 'react-native';
import { ChannelPreviewProps } from './ChannelPreview';
import type { ChannelsContextValue } from '../../contexts/channelsContext/ChannelsContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { BadgeNotification } from '../ui/Badge';
export type ChannelPreviewUnreadCountProps = Pick &
Pick & {
@@ -16,38 +15,15 @@ export type ChannelPreviewUnreadCountProps = Pick {
const { maxUnreadCount, unread } = props;
- const {
- theme: {
- channelPreview: { unreadContainer, unreadText },
- colors: { accent_red },
- },
- } = useTheme();
-
if (!unread) {
return null;
}
return (
-
-
- {unread > maxUnreadCount ? `${maxUnreadCount}+` : unread}
-
-
+ maxUnreadCount ? maxUnreadCount : unread}
+ size='md'
+ type='primary'
+ />
);
};
-
-const styles = StyleSheet.create({
- unreadContainer: {
- alignItems: 'center',
- borderRadius: 8,
- flexShrink: 1,
- justifyContent: 'center',
- },
- unreadText: {
- color: '#FFFFFF',
- fontSize: 11,
- fontWeight: '700',
- paddingHorizontal: 5,
- paddingVertical: 1,
- },
-});
diff --git a/package/src/components/ImageGallery/ImageGallery.tsx b/package/src/components/ImageGallery/ImageGallery.tsx
index ccd2193b93..c3816d2593 100644
--- a/package/src/components/ImageGallery/ImageGallery.tsx
+++ b/package/src/components/ImageGallery/ImageGallery.tsx
@@ -1,21 +1,17 @@
-import React, { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Image, ImageStyle, Keyboard, StyleSheet, ViewStyle } from 'react-native';
-
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { Image, ImageStyle, StyleSheet, ViewStyle } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
Easing,
- runOnJS,
- runOnUI,
- SharedValue,
+ useAnimatedReaction,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
-import { BottomSheetModal as BottomSheetModalOriginal } from '@gorhom/bottom-sheet';
-import type { UserResponse } from 'stream-chat';
+import BottomSheet from '@gorhom/bottom-sheet';
import { AnimatedGalleryImage } from './components/AnimatedGalleryImage';
import { AnimatedGalleryVideo } from './components/AnimatedGalleryVideo';
@@ -36,20 +32,20 @@ import {
import { useImageGalleryGestures } from './hooks/useImageGalleryGestures';
-import { useChatConfigContext } from '../../contexts/chatConfigContext/ChatConfigContext';
-import { useImageGalleryContext } from '../../contexts/imageGalleryContext/ImageGalleryContext';
-import { OverlayProviderProps } from '../../contexts/overlayContext/OverlayContext';
+import {
+ ImageGalleryProviderProps,
+ useImageGalleryContext,
+} from '../../contexts/imageGalleryContext/ImageGalleryContext';
+import {
+ OverlayContextValue,
+ useOverlayContext,
+} from '../../contexts/overlayContext/OverlayContext';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { useStateStore } from '../../hooks';
import { useViewport } from '../../hooks/useViewport';
-import { isVideoPlayerAvailable, VideoType } from '../../native';
+import { ImageGalleryState } from '../../state-store/image-gallery-state-store';
import { FileTypes } from '../../types/types';
-import { getResizedImageUrl } from '../../utils/getResizedImageUrl';
-import { getUrlOfImageAttachment } from '../../utils/getUrlOfImageAttachment';
-import { getGiphyMimeType } from '../Attachment/utils/getGiphyMimeType';
-import {
- BottomSheetModal,
- BottomSheetModalProvider,
-} from '../BottomSheetCompatibility/BottomSheetModal';
+import { dismissKeyboard } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView';
const MARGIN = 32;
@@ -104,36 +100,40 @@ export type ImageGalleryCustomComponents = {
};
};
-type Props = ImageGalleryCustomComponents & {
- overlayOpacity: SharedValue;
-} & Pick<
- OverlayProviderProps,
- | 'giphyVersion'
- | 'imageGalleryGridSnapPoints'
- | 'imageGalleryGridHandleHeight'
- | 'numberOfImageGalleryGridColumns'
- | 'autoPlayVideo'
- >;
-
-export const ImageGallery = (props: Props) => {
+const imageGallerySelector = (state: ImageGalleryState) => ({
+ assets: state.assets,
+ currentIndex: state.currentIndex,
+});
+
+type ImageGalleryWithContextProps = Pick<
+ ImageGalleryProviderProps,
+ | 'imageGalleryCustomComponents'
+ | 'imageGalleryGridSnapPoints'
+ | 'imageGalleryGridHandleHeight'
+ | 'numberOfImageGalleryGridColumns'
+> &
+ Pick;
+
+export const ImageGalleryWithContext = (props: ImageGalleryWithContextProps) => {
const {
- autoPlayVideo = false,
- giphyVersion = 'fixed_height',
- imageGalleryCustomComponents,
- imageGalleryGridHandleHeight = 40,
+ imageGalleryGridHandleHeight,
imageGalleryGridSnapPoints,
+ imageGalleryCustomComponents,
numberOfImageGalleryGridColumns,
overlayOpacity,
} = props;
- const { resizableCDNHosts } = useChatConfigContext();
const {
theme: {
colors: { white_snow },
imageGallery: { backgroundColor, pager, slide },
},
} = useTheme();
- const [gridPhotos, setGridPhotos] = useState([]);
- const { messages, selectedMessage, setSelectedMessage } = useImageGalleryContext();
+ const { imageGalleryStateStore } = useImageGalleryContext();
+ const { assets, currentIndex } = useStateStore(
+ imageGalleryStateStore.state,
+ imageGallerySelector,
+ );
+ const { videoPlayerPool } = imageGalleryStateStore;
const { vh, vw } = useViewport();
@@ -144,7 +144,7 @@ export const ImageGallery = (props: Props) => {
const halfScreenHeight = fullWindowHeight / 2;
const quarterScreenHeight = fullWindowHeight / 4;
const snapPoints = React.useMemo(
- () => [(fullWindowHeight * 3) / 4, fullWindowHeight - imageGalleryGridHandleHeight],
+ () => [(fullWindowHeight * 3) / 4, fullWindowHeight - (imageGalleryGridHandleHeight ?? 0)],
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
@@ -152,7 +152,7 @@ export const ImageGallery = (props: Props) => {
/**
* BottomSheetModal ref
*/
- const bottomSheetModalRef = useRef(null);
+ const bottomSheetModalRef = useRef(null);
/**
* BottomSheetModal state
@@ -165,22 +165,21 @@ export const ImageGallery = (props: Props) => {
* set to none for fast opening
*/
const screenTranslateY = useSharedValue(fullWindowHeight);
- const showScreen = () => {
+ const showScreen = useCallback(() => {
'worklet';
screenTranslateY.value = withTiming(0, {
duration: 250,
easing: Easing.out(Easing.ease),
});
- };
+ }, [screenTranslateY]);
/**
* Run the fade animation on visible change
*/
useEffect(() => {
- Keyboard.dismiss();
+ dismissKeyboard();
showScreen();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [showScreen]);
/**
* Image height from URL or default to full screen height
@@ -199,128 +198,25 @@ export const ImageGallery = (props: Props) => {
const translateY = useSharedValue(0);
const offsetScale = useSharedValue(1);
const scale = useSharedValue(1);
- const translationX = useSharedValue(0);
-
- /**
- * Photos array created from all currently available
- * photo attachments
- */
+ const translationX = useSharedValue(-(fullWindowWidth + MARGIN) * currentIndex);
- const photos = useMemo(
- () =>
- messages.reduce((acc: Photo[], cur) => {
- const attachmentImages =
- cur.attachments?.filter(
- (attachment) =>
- (attachment.type === FileTypes.Giphy &&
- (attachment.giphy?.[giphyVersion]?.url ||
- attachment.thumb_url ||
- attachment.image_url)) ||
- (attachment.type === FileTypes.Image &&
- !attachment.title_link &&
- !attachment.og_scrape_url &&
- getUrlOfImageAttachment(attachment)) ||
- (isVideoPlayerAvailable() && attachment.type === FileTypes.Video),
- ) || [];
-
- const attachmentPhotos = attachmentImages.map((a) => {
- const imageUrl = getUrlOfImageAttachment(a) as string;
- const giphyURL = a.giphy?.[giphyVersion]?.url || a.thumb_url || a.image_url;
- const isInitiallyPaused = !autoPlayVideo;
-
- return {
- channelId: cur.cid,
- created_at: cur.created_at,
- duration: 0,
- id: `photoId-${cur.id}-${imageUrl}`,
- messageId: cur.id,
- mime_type: a.type === 'giphy' ? getGiphyMimeType(giphyURL ?? '') : a.mime_type,
- original_height: a.original_height,
- original_width: a.original_width,
- paused: isInitiallyPaused,
- progress: 0,
- thumb_url: a.thumb_url,
- type: a.type,
- uri:
- a.type === 'giphy'
- ? giphyURL
- : getResizedImageUrl({
- height: fullWindowHeight,
- resizableCDNHosts,
- url: imageUrl,
- width: fullWindowWidth,
- }),
- user: cur.user,
- user_id: cur.user_id,
- };
- });
-
- return [...attachmentPhotos, ...acc] as Photo[];
- }, []),
- [autoPlayVideo, fullWindowHeight, fullWindowWidth, giphyVersion, messages, resizableCDNHosts],
+ useAnimatedReaction(
+ () => currentIndex,
+ (index) => {
+ translationX.value = -(fullWindowWidth + MARGIN) * index;
+ },
+ [currentIndex, fullWindowWidth],
);
- /**
- * The URL for the images may differ because of dimensions passed as
- * part of the query.
- */
- const stripQueryFromUrl = (url: string) => url.split('?')[0];
-
- const photoSelectedIndex = useMemo(() => {
- const idx = photos.findIndex(
- (photo) =>
- photo.messageId === selectedMessage?.messageId &&
- stripQueryFromUrl(photo.uri) === stripQueryFromUrl(selectedMessage?.url || ''),
- );
-
- return idx === -1 ? 0 : idx;
- }, [photos, selectedMessage]);
-
- /**
- * JS and UI index values, the JS follows the UI but is needed
- * for rendering the virtualized image list
- */
- const [selectedIndex, setSelectedIndex] = useState(photoSelectedIndex);
- const index = useSharedValue(photoSelectedIndex);
-
- const [imageGalleryAttachments, setImageGalleryAttachments] = useState(photos);
-
- /**
- * Photos length needs to be kept as a const here so if the length
- * changes it causes the pan gesture handler function to refresh. This
- * does not work if the calculation for the length of the array is left
- * inside the gesture handler as it will have an array as a dependency
- */
- const photoLength = photos.length;
-
- /**
- * Set selected photo when changed via pressing in the message list
- */
- useEffect(() => {
- const updatePosition = (newIndex: number) => {
- 'worklet';
-
- if (newIndex > -1) {
- index.value = newIndex;
- translationX.value = -(fullWindowWidth + MARGIN) * newIndex;
- runOnJS(setSelectedIndex)(newIndex);
- }
- };
-
- runOnUI(updatePosition)(photoSelectedIndex);
- }, [fullWindowWidth, index, photoSelectedIndex, translationX]);
-
/**
* Image heights are not provided and therefore need to be calculated.
* We start by allowing the image to be the full height then reduce it
* to the proper scaled height based on the width being restricted to the
* screen width when the dimensions are received.
*/
- const uriForCurrentImage = imageGalleryAttachments[selectedIndex]?.uri;
-
useEffect(() => {
let currentImageHeight = fullWindowHeight;
- const photo = imageGalleryAttachments[index.value];
+ const photo = assets[currentIndex];
const height = photo?.original_height;
const width = photo?.original_width;
@@ -338,7 +234,16 @@ export const ImageGallery = (props: Props) => {
setCurrentImageHeight(currentImageHeight);
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [uriForCurrentImage]);
+ }, [currentIndex]);
+
+ // If you change the current index, pause the active video player.
+ useEffect(() => {
+ const activePlayer = videoPlayerPool.getActivePlayer();
+
+ if (activePlayer) {
+ activePlayer.pause();
+ }
+ }, [currentIndex, videoPlayerPool]);
const { doubleTap, pan, pinch, singleTap } = useImageGalleryGestures({
currentImageHeight,
@@ -347,12 +252,9 @@ export const ImageGallery = (props: Props) => {
headerFooterVisible,
offsetScale,
overlayOpacity,
- photoLength,
scale,
screenHeight: fullWindowHeight,
screenWidth: fullWindowWidth,
- selectedIndex,
- setSelectedIndex,
translateX,
translateY,
translationX,
@@ -422,7 +324,6 @@ export const ImageGallery = (props: Props) => {
const closeGridView = () => {
if (bottomSheetModalRef.current?.close) {
bottomSheetModalRef.current.close();
- setGridPhotos([]);
}
};
@@ -430,79 +331,8 @@ export const ImageGallery = (props: Props) => {
* Function to open BottomSheetModal with image grid
*/
const openGridView = () => {
- if (bottomSheetModalRef.current?.present) {
- bottomSheetModalRef.current.present();
- setGridPhotos(imageGalleryAttachments);
- }
- };
-
- const handleEnd = () => {
- handlePlayPause(imageGalleryAttachments[selectedIndex].id, true);
- handleProgress(imageGalleryAttachments[selectedIndex].id, 1, true);
- };
-
- const videoRef = useRef(null);
-
- const handleLoad = (index: string, duration: number) => {
- setImageGalleryAttachments((prevImageGalleryAttachment) =>
- prevImageGalleryAttachment.map((imageGalleryAttachment) => ({
- ...imageGalleryAttachment,
- duration: imageGalleryAttachment.id === index ? duration : imageGalleryAttachment.duration,
- })),
- );
- };
-
- const handleProgress = (index: string, progress: number, hasEnd?: boolean) => {
- setImageGalleryAttachments((prevImageGalleryAttachments) =>
- prevImageGalleryAttachments.map((imageGalleryAttachment) => ({
- ...imageGalleryAttachment,
- progress:
- imageGalleryAttachment.id === index
- ? hasEnd
- ? 1
- : progress
- : imageGalleryAttachment.progress,
- })),
- );
- };
-
- const handlePlayPause = (index: string, pausedStatus?: boolean) => {
- if (pausedStatus === false) {
- // If the status is false we set the audio with the index as playing and the others as paused.
- setImageGalleryAttachments((prevImageGalleryAttachment) =>
- prevImageGalleryAttachment.map((imageGalleryAttachment) => ({
- ...imageGalleryAttachment,
- paused: imageGalleryAttachment.id === index ? false : true,
- })),
- );
-
- if (videoRef.current?.play) {
- videoRef.current.play();
- }
- } else {
- // If the status is true we simply set all the audio's paused state as true.
- setImageGalleryAttachments((prevImageGalleryAttachment) =>
- prevImageGalleryAttachment.map((imageGalleryAttachment) => ({
- ...imageGalleryAttachment,
- paused: true,
- })),
- );
-
- if (videoRef.current?.pause) {
- videoRef.current.pause();
- }
- }
- };
-
- const onPlayPause = (status?: boolean) => {
- if (status === undefined) {
- if (imageGalleryAttachments[selectedIndex].paused) {
- handlePlayPause(imageGalleryAttachments[selectedIndex].id, false);
- } else {
- handlePlayPause(imageGalleryAttachments[selectedIndex].id, true);
- }
- } else {
- handlePlayPause(imageGalleryAttachments[selectedIndex].id, status);
+ if (bottomSheetModalRef.current?.snapToIndex) {
+ bottomSheetModalRef.current.snapToIndex(0);
}
};
@@ -526,24 +356,16 @@ export const ImageGallery = (props: Props) => {
- {imageGalleryAttachments.map((photo, i) =>
+ {assets.map((photo, i) =>
photo.type === FileTypes.Video ? (
i}
- repeat={true}
+ photo={photo}
scale={scale}
screenHeight={fullWindowHeight}
- selected={selectedIndex === i}
- shouldRender={Math.abs(selectedIndex - i) < 4}
- source={{ uri: photo.uri }}
style={[
{
height: fullWindowHeight * 8,
@@ -554,20 +376,17 @@ export const ImageGallery = (props: Props) => {
]}
translateX={translateX}
translateY={translateY}
- videoRef={videoRef as RefObject}
/>
) : (
i}
scale={scale}
screenHeight={fullWindowHeight}
- selected={selectedIndex === i}
- shouldRender={Math.abs(selectedIndex - i) < 4}
+ screenWidth={fullWindowWidth}
style={[
{
height: fullWindowHeight * 8,
@@ -586,59 +405,66 @@ export const ImageGallery = (props: Props) => {
- {imageGalleryAttachments[selectedIndex] && (
- }
- visible={headerFooterVisible}
- {...imageGalleryCustomComponents?.footer}
- />
- )}
+
-
- setCurrentBottomSheetIndex(index)}
- ref={bottomSheetModalRef}
- snapPoints={imageGalleryGridSnapPoints || snapPoints}
- >
-
-
-
+ setCurrentBottomSheetIndex(index)}
+ ref={bottomSheetModalRef}
+ snapPoints={imageGalleryGridSnapPoints || snapPoints}
+ >
+
+
);
};
+export type ImageGalleryProps = Partial;
+
+export const ImageGallery = (props: ImageGalleryProps) => {
+ const {
+ imageGalleryCustomComponents,
+ imageGalleryGridHandleHeight,
+ imageGalleryGridSnapPoints,
+ numberOfImageGalleryGridColumns,
+ } = useImageGalleryContext();
+ const { overlayOpacity } = useOverlayContext();
+ return (
+
+ );
+};
+
/**
* Clamping worklet to clamp the scaling
*/
@@ -654,22 +480,4 @@ const styles = StyleSheet.create({
},
});
-export type Photo = {
- id: string;
- uri: string;
- channelId?: string;
- created_at?: string | Date;
- duration?: number;
- messageId?: string;
- mime_type?: string;
- original_height?: number;
- original_width?: number;
- paused?: boolean;
- progress?: number;
- thumb_url?: string;
- type?: string;
- user?: UserResponse | null;
- user_id?: string;
-};
-
ImageGallery.displayName = 'ImageGallery{imageGallery}';
diff --git a/package/src/components/ImageGallery/__tests__/AnimatedVideoGallery.test.tsx b/package/src/components/ImageGallery/__tests__/AnimatedVideoGallery.test.tsx
deleted file mode 100644
index 139f90ec76..0000000000
--- a/package/src/components/ImageGallery/__tests__/AnimatedVideoGallery.test.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import React from 'react';
-
-import type { SharedValue } from 'react-native-reanimated';
-
-import { act, fireEvent, render, screen } from '@testing-library/react-native';
-
-import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
-import { defaultTheme } from '../../../contexts/themeContext/utils/theme';
-import { AnimatedGalleryVideo, AnimatedGalleryVideoType } from '../components/AnimatedGalleryVideo';
-
-const getComponent = (props: Partial) => (
-
-
-
-);
-
-describe('ImageGallery', () => {
- it('render image gallery component with video rendered', () => {
- render(
- getComponent({
- offsetScale: { value: 1 } as SharedValue,
- scale: { value: 1 } as SharedValue,
- shouldRender: true,
- source: {
- uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
- },
- translateX: { value: 1 } as SharedValue,
- }),
- );
- expect(screen.queryAllByLabelText('Image Gallery Video')).toHaveLength(1);
- });
-
- it('render empty view when shouldRender is false', () => {
- render(
- getComponent({
- offsetScale: { value: 1 } as SharedValue,
- scale: { value: 1 } as SharedValue,
- shouldRender: false,
- translateX: { value: 1 } as SharedValue,
- }),
- );
-
- expect(screen.getByLabelText('Empty View Image Gallery')).not.toBeUndefined();
- });
-
- it('trigger onEnd and onProgress events handlers of Video component', () => {
- const handleEndMock = jest.fn();
- const handleProgressMock = jest.fn();
-
- render(
- getComponent({
- handleEnd: handleEndMock,
- handleProgress: handleProgressMock,
- offsetScale: { value: 1 } as SharedValue,
- scale: { value: 1 } as SharedValue,
- shouldRender: true,
- source: {
- uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
- },
- translateX: { value: 1 } as SharedValue,
- }),
- );
-
- const videoComponent = screen.getByTestId('video-player');
-
- act(() => {
- fireEvent(videoComponent, 'onEnd');
- fireEvent(videoComponent, 'onProgress', { currentTime: 10, seekableDuration: 60 });
- });
-
- expect(handleEndMock).toHaveBeenCalled();
- expect(handleProgressMock).toHaveBeenCalled();
- });
-
- it('trigger onLoadStart event handler of Video component', () => {
- render(
- getComponent({
- offsetScale: { value: 1 } as SharedValue,
- scale: { value: 1 } as SharedValue,
- shouldRender: true,
- source: {
- uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
- },
- translateX: { value: 1 } as SharedValue,
- }),
- );
-
- const videoComponent = screen.getByTestId('video-player');
- const spinnerComponent = screen.queryByLabelText('Spinner');
-
- act(() => {
- fireEvent(videoComponent, 'onLoadStart');
- });
- expect(spinnerComponent?.props.style[1].opacity).toBe(1);
- });
-
- it('trigger onLoad event handler of Video component', () => {
- const handleLoadMock = jest.fn();
-
- render(
- getComponent({
- handleLoad: handleLoadMock,
- offsetScale: { value: 1 } as SharedValue,
- scale: { value: 1 } as SharedValue,
- shouldRender: true,
- source: {
- uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
- },
- translateX: { value: 1 } as SharedValue,
- }),
- );
-
- const videoComponent = screen.getByTestId('video-player');
- const spinnerComponent = screen.queryByLabelText('Spinner');
-
- act(() => {
- fireEvent(videoComponent, 'onLoad', { duration: 10 });
- });
-
- expect(handleLoadMock).toHaveBeenCalled();
- expect(spinnerComponent?.props.style[1].opacity).toBe(0);
- });
-
- it('trigger onBuffer event handler of Video component', () => {
- render(
- getComponent({
- offsetScale: { value: 1 } as SharedValue,
- scale: { value: 1 } as SharedValue,
- shouldRender: true,
- source: {
- uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
- },
- translateX: { value: 1 } as SharedValue,
- }),
- );
-
- const videoComponent = screen.getByTestId('video-player');
- const spinnerComponent = screen.queryByLabelText('Spinner');
-
- act(() => {
- fireEvent(videoComponent, 'onBuffer', {
- isBuffering: false,
- });
- });
-
- expect(spinnerComponent?.props.style[1].opacity).toBe(0);
-
- act(() => {
- fireEvent(videoComponent, 'onBuffer', {
- isBuffering: true,
- });
- });
-
- expect(spinnerComponent?.props.style[1].opacity).toBe(1);
- });
-});
diff --git a/package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx
index e72cd70e3b..02a477da4a 100644
--- a/package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx
+++ b/package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx
@@ -1,8 +1,8 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import type { SharedValue } from 'react-native-reanimated';
-import { act, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
+import { render, screen, waitFor } from '@testing-library/react-native';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
@@ -13,8 +13,6 @@ import {
ImageGalleryContextValue,
} from '../../../contexts/imageGalleryContext/ImageGalleryContext';
import { OverlayProvider } from '../../../contexts/overlayContext/OverlayProvider';
-import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
-import { defaultTheme } from '../../../contexts/themeContext/utils/theme';
import {
generateGiphyAttachment,
generateImageAttachment,
@@ -22,7 +20,8 @@ import {
} from '../../../mock-builders/generator/attachment';
import { generateMessage } from '../../../mock-builders/generator/message';
-import { ImageGallery } from '../ImageGallery';
+import { ImageGalleryStateStore } from '../../../state-store/image-gallery-state-store';
+import { ImageGallery, ImageGalleryProps } from '../ImageGallery';
dayjs.extend(duration);
@@ -39,30 +38,48 @@ jest.mock('../../../native.ts', () => {
};
});
-const getComponent = (props: Partial) => (
-
-
-
- } />
-
-
-
-);
+const ImageGalleryComponent = (props: ImageGalleryProps & { message: LocalMessage }) => {
+ const [imageGalleryStateStore] = useState(() => new ImageGalleryStateStore());
+
+ useEffect(() => {
+ const unsubscribe = imageGalleryStateStore.registerSubscriptions();
+
+ return () => {
+ unsubscribe();
+ };
+ }, [imageGalleryStateStore]);
+
+ const { attachments } = props.message;
+ imageGalleryStateStore.openImageGallery({
+ messages: [props.message],
+ selectedAttachmentUrl: attachments?.[0]?.asset_url || attachments?.[0]?.image_url || '',
+ });
+
+ return (
+ }}>
+
+
+
+
+ );
+};
describe('ImageGallery', () => {
it('render image gallery component', async () => {
render(
- getComponent({
- messages: [
+ ,
);
await waitFor(() => {
@@ -70,154 +87,4 @@ describe('ImageGallery', () => {
expect(screen.queryAllByLabelText('Image Gallery Video')).toHaveLength(1);
});
});
-
- it('handle handleLoad function when video item present and payload duration is available', async () => {
- const attachment = generateVideoAttachment({ type: 'video' });
- const message = generateMessage({
- attachments: [attachment],
- });
- render(
- getComponent({
- messages: [message] as unknown as LocalMessage[],
- }),
- );
-
- const videoItemComponent = screen.getByLabelText('Image Gallery Video');
-
- act(() => {
- fireEvent(
- videoItemComponent,
- 'handleLoad',
- `photoId-${message.id}-${attachment.asset_url}`,
- 10 * 1000,
- );
- });
-
- const videoDurationComponent = screen.getByLabelText('Video Duration');
-
- await waitFor(() => {
- expect(videoDurationComponent.children[0]).toBe('00:10');
- });
- });
-
- it('handle handleLoad function when video item present and payload duration is undefined', async () => {
- render(
- getComponent({
- messages: [
- generateMessage({
- attachments: [generateVideoAttachment({ type: 'video' })],
- }),
- ] as unknown as LocalMessage[],
- }),
- );
-
- const videoItemComponent = screen.getByLabelText('Image Gallery Video');
-
- act(() => {
- fireEvent(videoItemComponent, 'handleLoad', {
- duration: undefined,
- });
- });
-
- const videoDurationComponent = screen.getByLabelText('Video Duration');
- await waitFor(() => {
- expect(videoDurationComponent.children[0]).toBe('00:00');
- });
- });
-
- it('handle handleProgress function when video item present and payload is well defined', async () => {
- const attachment = generateVideoAttachment({ type: 'video' });
- const message = generateMessage({
- attachments: [attachment],
- });
-
- render(
- getComponent({
- messages: [message] as unknown as LocalMessage[],
- }),
- );
-
- const videoItemComponent = screen.getByLabelText('Image Gallery Video');
-
- act(() => {
- fireEvent(
- videoItemComponent,
- 'handleLoad',
- `photoId-${message.id}-${attachment.asset_url}`,
- 10,
- );
- fireEvent(
- videoItemComponent,
- 'handleProgress',
- `photoId-${message.id}-${attachment.asset_url}`,
- 0.3 * 1000,
- );
- });
-
- const progressDurationComponent = screen.getByLabelText('Progress Duration');
-
- await waitFor(() => {
- expect(progressDurationComponent.children[0]).toBe('00:03');
- });
- });
-
- it('handle handleProgress function when video item present and payload is not defined', async () => {
- render(
- getComponent({
- messages: [
- generateMessage({
- attachments: [generateVideoAttachment({ type: 'video' })],
- }),
- ] as unknown as LocalMessage[],
- }),
- );
-
- const videoItemComponent = screen.getByLabelText('Image Gallery Video');
-
- act(() => {
- fireEvent(videoItemComponent, 'handleLoad', {
- duration: 10 * 1000,
- });
- fireEvent(videoItemComponent, 'handleProgress', {
- currentTime: undefined,
- seekableDuration: undefined,
- });
- });
-
- const progressDurationComponent = screen.getByLabelText('Progress Duration');
-
- await waitFor(() => {
- expect(progressDurationComponent.children[0]).toBe('00:00');
- });
- });
-
- it('handle handleEnd function when video item present', async () => {
- const attachment = generateVideoAttachment({ type: 'video' });
- const message = generateMessage({
- attachments: [attachment],
- });
- render(
- getComponent({
- messages: [message] as unknown as LocalMessage[],
- }),
- );
-
- const videoItemComponent = screen.getByLabelText('Image Gallery Video');
-
- act(() => {
- fireEvent(
- videoItemComponent,
- 'handleLoad',
- `photoId-${message.id}-${attachment.asset_url}`,
- 10 * 1000,
- );
- fireEvent(videoItemComponent, 'handleEnd');
- });
-
- const progressDurationComponent = screen.getByLabelText('Progress Duration');
- await waitFor(() => {
- expect(screen.queryAllByLabelText('Play Icon').length).toBeGreaterThan(0);
- expect(progressDurationComponent.children[0]).toBe('00:10');
- });
- });
});
diff --git a/package/src/components/ImageGallery/__tests__/ImageGalleryFooter.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGalleryFooter.test.tsx
index ea553a0eaf..54131fd622 100644
--- a/package/src/components/ImageGallery/__tests__/ImageGalleryFooter.test.tsx
+++ b/package/src/components/ImageGallery/__tests__/ImageGalleryFooter.test.tsx
@@ -1,13 +1,12 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { Text, View } from 'react-native';
import type { SharedValue } from 'react-native-reanimated';
import { render, screen, userEvent, waitFor } from '@testing-library/react-native';
-import { LocalMessage } from 'stream-chat';
+import { Attachment, LocalMessage } from 'stream-chat';
-import { Chat } from '../../../components/Chat/Chat';
import {
ImageGalleryContext,
ImageGalleryContextValue,
@@ -18,9 +17,9 @@ import {
generateVideoAttachment,
} from '../../../mock-builders/generator/attachment';
import { generateMessage } from '../../../mock-builders/generator/message';
-import { getTestClientWithUser } from '../../../mock-builders/mock';
import { NativeHandlers } from '../../../native';
-import { ImageGallery, ImageGalleryCustomComponents } from '../ImageGallery';
+import { ImageGalleryStateStore } from '../../../state-store/image-gallery-state-store';
+import { ImageGallery, ImageGalleryCustomComponents, ImageGalleryProps } from '../ImageGallery';
jest.mock('../../../native.ts', () => {
const { View } = require('react-native');
@@ -38,10 +37,75 @@ jest.mock('../../../native.ts', () => {
};
});
+const ImageGalleryComponentVideo = (props: ImageGalleryProps) => {
+ const [imageGalleryStateStore] = useState(() => new ImageGalleryStateStore());
+
+ useEffect(() => {
+ const unsubscribe = imageGalleryStateStore.registerSubscriptions();
+
+ return () => {
+ unsubscribe();
+ };
+ }, [imageGalleryStateStore]);
+
+ const attachment = generateVideoAttachment({ type: 'video' });
+ imageGalleryStateStore.openImageGallery({
+ messages: [
+ generateMessage({
+ attachments: [attachment],
+ }) as unknown as LocalMessage,
+ ],
+ selectedAttachmentUrl: attachment.asset_url,
+ });
+
+ return (
+ }}>
+
+
+
+
+ );
+};
+
+const ImageGalleryComponentImage = (
+ props: ImageGalleryProps & {
+ attachment: Attachment;
+ },
+) => {
+ const [imageGalleryStateStore] = useState(() => new ImageGalleryStateStore());
+
+ useEffect(() => {
+ const unsubscribe = imageGalleryStateStore.registerSubscriptions();
+
+ return () => {
+ unsubscribe();
+ };
+ }, [imageGalleryStateStore]);
+
+ imageGalleryStateStore.openImageGallery({
+ messages: [
+ generateMessage({
+ attachments: [props.attachment],
+ }) as unknown as LocalMessage,
+ ],
+ selectedAttachmentUrl: props.attachment.image_url as string,
+ });
+
+ return (
+ }}>
+
+
+
+
+ );
+};
+
describe('ImageGalleryFooter', () => {
it('render image gallery footer component with custom component footer props', async () => {
- const chatClient = await getTestClientWithUser({ id: 'testID' });
-
const CustomFooterLeftElement = () => (
Left element
@@ -67,35 +131,18 @@ describe('ImageGalleryFooter', () => {
);
render(
-
-
-
- }
- />
-
-
- ,
+ ,
);
await waitFor(() => {
@@ -107,8 +154,6 @@ describe('ImageGalleryFooter', () => {
});
it('render image gallery footer component with custom component footer Grid Icon and Share Icon component', async () => {
- const chatClient = await getTestClientWithUser({ id: 'testID' });
-
const CustomShareIconElement = () => (
Share Icon element
@@ -122,33 +167,17 @@ describe('ImageGalleryFooter', () => {
);
render(
-
-
-
- ,
- ShareIcon: ,
- },
- } as ImageGalleryCustomComponents['imageGalleryCustomComponents']
- }
- overlayOpacity={{ value: 1 } as SharedValue}
- />
-
-
- ,
+ ,
+ ShareIcon: ,
+ },
+ } as ImageGalleryCustomComponents['imageGalleryCustomComponents']
+ }
+ overlayOpacity={{ value: 1 } as SharedValue}
+ />,
);
await waitFor(() => {
@@ -159,32 +188,13 @@ describe('ImageGalleryFooter', () => {
it('should trigger the share button onPress Handler with local attachment and no mime_type', async () => {
const user = userEvent.setup();
- const chatClient = await getTestClientWithUser({ id: 'testID' });
const saveFileMock = jest.spyOn(NativeHandlers, 'saveFile');
const shareImageMock = jest.spyOn(NativeHandlers, 'shareImage');
const deleteFileMock = jest.spyOn(NativeHandlers, 'deleteFile');
const attachment = generateImageAttachment();
- render(
-
-
-
- } />
-
-
- ,
- );
+ render();
const { getByLabelText } = screen;
@@ -204,32 +214,13 @@ describe('ImageGalleryFooter', () => {
it('should trigger the share button onPress Handler with local attachment and existing mime_type', async () => {
const user = userEvent.setup();
- const chatClient = await getTestClientWithUser({ id: 'testID' });
const saveFileMock = jest.spyOn(NativeHandlers, 'saveFile');
const shareImageMock = jest.spyOn(NativeHandlers, 'shareImage');
const deleteFileMock = jest.spyOn(NativeHandlers, 'deleteFile');
const attachment = { ...generateImageAttachment(), mime_type: 'image/png' };
- render(
-
-
-
- } />
-
-
- ,
- );
+ render();
const { getByLabelText } = screen;
@@ -249,7 +240,6 @@ describe('ImageGalleryFooter', () => {
it('should trigger the share button onPress Handler with cdn attachment', async () => {
const user = userEvent.setup();
- const chatClient = await getTestClientWithUser({ id: 'testID' });
const saveFileMock = jest
.spyOn(NativeHandlers, 'saveFile')
.mockResolvedValue('file:///local/asset/url');
@@ -262,25 +252,7 @@ describe('ImageGalleryFooter', () => {
mime_type: 'image/png',
};
- render(
-
-
-
- } />
-
-
- ,
- );
+ render();
const { getByLabelText } = screen;
diff --git a/package/src/components/ImageGallery/__tests__/ImageGalleryGrid.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGalleryGrid.test.tsx
index 1ea5816c2a..36e2d8d079 100644
--- a/package/src/components/ImageGallery/__tests__/ImageGalleryGrid.test.tsx
+++ b/package/src/components/ImageGallery/__tests__/ImageGalleryGrid.test.tsx
@@ -1,46 +1,71 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { Text, View } from 'react-native';
+import { SharedValue } from 'react-native-reanimated';
+
import { act, fireEvent, render, screen } from '@testing-library/react-native';
-import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
-import { defaultTheme } from '../../../contexts/themeContext/utils/theme';
+import { LocalMessage } from '../../../../../../stream-chat-js/dist/types/types';
import {
- TranslationContextValue,
- TranslationProvider,
-} from '../../../contexts/translationContext/TranslationContext';
+ ImageGalleryContext,
+ ImageGalleryContextValue,
+} from '../../../contexts/imageGalleryContext/ImageGalleryContext';
+import { OverlayProvider } from '../../../contexts/overlayContext/OverlayProvider';
import {
generateImageAttachment,
generateVideoAttachment,
} from '../../../mock-builders/generator/attachment';
+import { generateMessage } from '../../../mock-builders/generator/message';
+import { ImageGalleryStateStore } from '../../../state-store/image-gallery-state-store';
import { ImageGrid, ImageGridType } from '../components/ImageGrid';
-const getComponent = (props: Partial = {}) => {
- const t = jest.fn((key) => key);
+const ImageGalleryGridComponent = (props: Partial & { message: LocalMessage }) => {
+ const { message } = props;
+ const [imageGalleryStateStore] = useState(() => new ImageGalleryStateStore());
+
+ useEffect(() => {
+ const unsubscribe = imageGalleryStateStore.registerSubscriptions();
+
+ return () => {
+ unsubscribe();
+ };
+ }, [imageGalleryStateStore]);
+
+ imageGalleryStateStore.openImageGallery({
+ messages: [message],
+ selectedAttachmentUrl:
+ message.attachments?.[0]?.asset_url || message.attachments?.[0]?.image_url || '',
+ });
return (
-
-
+ }}>
+
-
-
+
+
);
};
describe('ImageGalleryOverlay', () => {
it('should render ImageGalleryGrid', () => {
- render(getComponent({ photos: [generateImageAttachment(), generateImageAttachment()] }));
+ const message = generateMessage({
+ attachments: [generateImageAttachment(), generateImageAttachment()],
+ }) as unknown as LocalMessage;
+
+ render();
expect(screen.queryAllByLabelText('Image Grid')).toHaveLength(1);
});
it('should render ImageGalleryGrid individual images', () => {
- render(
- getComponent({
- photos: [generateImageAttachment(), generateVideoAttachment({ type: 'video' })],
- }),
- );
+ const message = generateMessage({
+ attachments: [generateImageAttachment(), generateVideoAttachment({ type: 'video' })],
+ }) as unknown as LocalMessage;
+
+ render();
expect(screen.queryAllByLabelText('Grid Image')).toHaveLength(2);
});
@@ -52,27 +77,23 @@ describe('ImageGalleryOverlay', () => {
);
- render(
- getComponent({
- imageComponent: CustomImageComponent,
- photos: [generateImageAttachment(), generateVideoAttachment({ type: 'video' })],
- }),
- );
+ const message = generateMessage({
+ attachments: [generateImageAttachment(), generateVideoAttachment({ type: 'video' })],
+ }) as unknown as LocalMessage;
+
+ render();
expect(screen.queryAllByText('Image Attachment')).toHaveLength(2);
});
it('should trigger the selectAndClose when the Image item is pressed', () => {
const closeGridViewMock = jest.fn();
- const setSelectedMessageMock = jest.fn();
-
- render(
- getComponent({
- closeGridView: closeGridViewMock,
- photos: [generateImageAttachment(), generateVideoAttachment({ type: 'video' })],
- setSelectedMessage: setSelectedMessageMock,
- }),
- );
+
+ const message = generateMessage({
+ attachments: [generateImageAttachment(), generateVideoAttachment({ type: 'video' })],
+ }) as unknown as LocalMessage;
+
+ render();
const component = screen.getAllByLabelText('Grid Image');
@@ -81,6 +102,5 @@ describe('ImageGalleryOverlay', () => {
});
expect(closeGridViewMock).toHaveBeenCalledTimes(1);
- expect(setSelectedMessageMock).toHaveBeenCalledTimes(1);
});
});
diff --git a/package/src/components/ImageGallery/__tests__/ImageGalleryGridHandle.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGalleryGridHandle.test.tsx
index 4ab178ed4e..bdc8bee50f 100644
--- a/package/src/components/ImageGallery/__tests__/ImageGalleryGridHandle.test.tsx
+++ b/package/src/components/ImageGallery/__tests__/ImageGalleryGridHandle.test.tsx
@@ -6,10 +6,6 @@ import { render, screen } from '@testing-library/react-native';
import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
import { defaultTheme } from '../../../contexts/themeContext/utils/theme';
-import {
- TranslationContextValue,
- TranslationProvider,
-} from '../../../contexts/translationContext/TranslationContext';
import {
ImageGalleryGridHandleCustomComponentProps,
ImageGridHandle,
@@ -20,14 +16,10 @@ type ImageGridHandleProps = ImageGalleryGridHandleCustomComponentProps & {
};
const getComponent = (props: Partial = {}) => {
- const t = jest.fn((key) => key);
-
return (
-
-
-
-
-
+
+
+
);
};
diff --git a/package/src/components/ImageGallery/__tests__/ImageGalleryHeader.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGalleryHeader.test.tsx
index f39ffaa28f..a608ea2c00 100644
--- a/package/src/components/ImageGallery/__tests__/ImageGalleryHeader.test.tsx
+++ b/package/src/components/ImageGallery/__tests__/ImageGalleryHeader.test.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { Text, View } from 'react-native';
import type { SharedValue } from 'react-native-reanimated';
@@ -7,24 +7,17 @@ import { act, render, screen, userEvent, waitFor } from '@testing-library/react-
import { LocalMessage } from 'stream-chat';
-import { Chat } from '../../../components/Chat/Chat';
import {
ImageGalleryContext,
ImageGalleryContextValue,
} from '../../../contexts/imageGalleryContext/ImageGalleryContext';
-import {
- OverlayContext,
- OverlayContextValue,
-} from '../../../contexts/overlayContext/OverlayContext';
+import * as overlayContext from '../../../contexts/overlayContext/OverlayContext';
import { OverlayProvider } from '../../../contexts/overlayContext/OverlayProvider';
-import {
- generateImageAttachment,
- generateVideoAttachment,
-} from '../../../mock-builders/generator/attachment';
+import { generateImageAttachment } from '../../../mock-builders/generator/attachment';
import { generateMessage } from '../../../mock-builders/generator/message';
-import { getTestClientWithUser } from '../../../mock-builders/mock';
-import { ImageGallery, ImageGalleryCustomComponents } from '../ImageGallery';
+import { ImageGalleryStateStore } from '../../../state-store/image-gallery-state-store';
+import { ImageGallery, ImageGalleryCustomComponents, ImageGalleryProps } from '../ImageGallery';
jest.mock('../../../native.ts', () => {
const { View } = require('react-native');
@@ -39,10 +32,35 @@ jest.mock('../../../native.ts', () => {
};
});
+const ImageGalleryComponent = (props: ImageGalleryProps) => {
+ const [imageGalleryStateStore] = useState(() => new ImageGalleryStateStore());
+ const attachment = generateImageAttachment();
+ imageGalleryStateStore.openImageGallery({
+ messages: [generateMessage({ attachments: [attachment] }) as unknown as LocalMessage],
+ selectedAttachmentUrl: attachment.url,
+ });
+
+ useEffect(() => {
+ const unsubscribe = imageGalleryStateStore.registerSubscriptions();
+
+ return () => {
+ unsubscribe();
+ };
+ }, [imageGalleryStateStore]);
+
+ return (
+ }}>
+
+
+
+
+ );
+};
+
describe('ImageGalleryHeader', () => {
it('render image gallery header component with custom component header props', async () => {
- const chatClient = await getTestClientWithUser({ id: 'testID' });
-
const CustomHeaderLeftElement = () => (
Left element
@@ -62,34 +80,17 @@ describe('ImageGalleryHeader', () => {
);
render(
-
-
-
- }
- />
-
-
- ,
+ ,
);
await waitFor(() => {
@@ -100,8 +101,6 @@ describe('ImageGalleryHeader', () => {
});
it('render image gallery header component with custom Close Icon component', async () => {
- const chatClient = await getTestClientWithUser({ id: 'testID' });
-
const CustomCloseIconElement = () => (
Close Icon element
@@ -109,32 +108,15 @@ describe('ImageGalleryHeader', () => {
);
render(
-
-
-
- ,
- },
- } as ImageGalleryCustomComponents['imageGalleryCustomComponents']
- }
- overlayOpacity={{ value: 1 } as SharedValue}
- />
-
-
- ,
+ ,
+ },
+ } as ImageGalleryCustomComponents['imageGalleryCustomComponents']
+ }
+ />,
);
await waitFor(() => {
expect(screen.queryAllByText('Close Icon element')).toHaveLength(1);
@@ -142,33 +124,16 @@ describe('ImageGalleryHeader', () => {
});
it('should trigger the hideOverlay function on button onPress', async () => {
- const chatClient = await getTestClientWithUser({ id: 'testID' });
const setOverlayMock = jest.fn();
const user = userEvent.setup();
- render(
-
-
-
- } />
-
-
- ,
- );
+ jest.spyOn(overlayContext, 'useOverlayContext').mockImplementation(() => ({
+ setOverlay: setOverlayMock,
+ }));
+
+ render();
- act(() => {
+ await act(() => {
user.press(screen.getByLabelText('Hide Overlay'));
});
diff --git a/package/src/components/ImageGallery/__tests__/ImageGalleryVideoControl.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGalleryVideoControl.test.tsx
deleted file mode 100644
index 4788260271..0000000000
--- a/package/src/components/ImageGallery/__tests__/ImageGalleryVideoControl.test.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import React from 'react';
-
-import { ReactTestInstance } from 'react-test-renderer';
-
-import { act, render, screen, userEvent, waitFor } from '@testing-library/react-native';
-
-import dayjs from 'dayjs';
-import duration from 'dayjs/plugin/duration';
-
-import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
-import { defaultTheme } from '../../../contexts/themeContext/utils/theme';
-import type { ImageGalleryFooterVideoControlProps } from '../components/ImageGalleryFooter';
-import { ImageGalleryVideoControl } from '../components/ImageGalleryVideoControl';
-
-dayjs.extend(duration);
-
-const getComponent = (props: Partial) => (
-
-
-
-);
-
-describe('ImageGalleryOverlay', () => {
- it('should trigger the onPlayPause when play/pause button is pressed', async () => {
- const onPlayPauseMock = jest.fn();
- const user = userEvent.setup();
-
- render(getComponent({ onPlayPause: onPlayPauseMock }));
-
- const component = screen.queryByLabelText('Play Pause Button') as ReactTestInstance;
-
- act(() => {
- user.press(component);
- });
-
- await waitFor(() => {
- expect(component).not.toBeUndefined();
- expect(onPlayPauseMock).toHaveBeenCalled();
- });
- });
-
- it('should render the play icon when paused prop is true', async () => {
- render(getComponent({ paused: true }));
-
- const components = screen.queryAllByLabelText('Play Icon').length;
-
- await waitFor(() => {
- expect(components).toBeGreaterThan(0);
- });
- });
-
- it('should calculate the videoDuration and progressDuration when they are greater than or equal to 3600', () => {
- jest.spyOn(dayjs, 'duration');
-
- render(
- getComponent({
- duration: 3600 * 1000,
- progress: 1,
- }),
- );
-
- const videoDurationComponent = screen.queryByLabelText('Video Duration') as ReactTestInstance;
- const progressDurationComponent = screen.queryByLabelText(
- 'Progress Duration',
- ) as ReactTestInstance;
-
- expect(videoDurationComponent.children[0]).toBe('01:00:00');
- expect(progressDurationComponent.children[0]).toBe('01:00:00');
- });
-
- it('should calculate the videoDuration and progressDuration when they are less than 3600', () => {
- jest.spyOn(dayjs, 'duration');
-
- render(
- getComponent({
- duration: 60 * 1000,
- progress: 0.5,
- }),
- );
-
- const videoDurationComponent = screen.queryByLabelText('Video Duration') as ReactTestInstance;
- const progressDurationComponent = screen.queryByLabelText(
- 'Progress Duration',
- ) as ReactTestInstance;
-
- expect(videoDurationComponent.children[0]).toBe('01:00');
- expect(progressDurationComponent.children[0]).toBe('00:30');
- });
-});
diff --git a/package/src/components/ImageGallery/components/AnimatedGalleryImage.tsx b/package/src/components/ImageGallery/components/AnimatedGalleryImage.tsx
index f3aab7ea39..b6c87ab893 100644
--- a/package/src/components/ImageGallery/components/AnimatedGalleryImage.tsx
+++ b/package/src/components/ImageGallery/components/AnimatedGalleryImage.tsx
@@ -1,8 +1,16 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { View } from 'react-native';
import type { ImageStyle, StyleProp } from 'react-native';
import Animated, { SharedValue } from 'react-native-reanimated';
+import { useChatConfigContext } from '../../../contexts/chatConfigContext/ChatConfigContext';
+import { useImageGalleryContext } from '../../../contexts/imageGalleryContext/ImageGalleryContext';
+import { useStateStore } from '../../../hooks';
+import {
+ ImageGalleryAsset,
+ ImageGalleryState,
+} from '../../../state-store/image-gallery-state-store';
+import { getResizedImageUrl } from '../../../utils/getResizedImageUrl';
import { useAnimatedGalleryStyle } from '../hooks/useAnimatedGalleryStyle';
const oneEighth = 1 / 8;
@@ -11,17 +19,19 @@ type Props = {
accessibilityLabel: string;
index: number;
offsetScale: SharedValue;
- photo: { uri: string };
- previous: boolean;
+ photo: ImageGalleryAsset;
scale: SharedValue;
screenHeight: number;
- selected: boolean;
- shouldRender: boolean;
+ screenWidth: number;
translateX: SharedValue;
translateY: SharedValue;
style?: StyleProp;
};
+const imageGallerySelector = (state: ImageGalleryState) => ({
+ currentIndex: state.currentIndex,
+});
+
export const AnimatedGalleryImage = React.memo(
(props: Props) => {
const {
@@ -29,15 +39,29 @@ export const AnimatedGalleryImage = React.memo(
index,
offsetScale,
photo,
- previous,
scale,
screenHeight,
- selected,
- shouldRender,
+ screenWidth,
style,
translateX,
translateY,
} = props;
+ const { imageGalleryStateStore } = useImageGalleryContext();
+ const { resizableCDNHosts } = useChatConfigContext();
+ const { currentIndex } = useStateStore(imageGalleryStateStore.state, imageGallerySelector);
+
+ const uri = useMemo(() => {
+ return getResizedImageUrl({
+ height: screenHeight,
+ resizableCDNHosts,
+ url: photo.uri,
+ width: screenWidth,
+ });
+ }, [photo.uri, resizableCDNHosts, screenHeight, screenWidth]);
+
+ const selected = currentIndex === index;
+ const previous = currentIndex > index;
+ const shouldRender = Math.abs(currentIndex - index) < 4;
const animatedStyles = useAnimatedGalleryStyle({
index,
@@ -63,19 +87,17 @@ export const AnimatedGalleryImage = React.memo(
);
},
(prevProps, nextProps) => {
if (
- prevProps.selected === nextProps.selected &&
- prevProps.shouldRender === nextProps.shouldRender &&
prevProps.photo.uri === nextProps.photo.uri &&
- prevProps.previous === nextProps.previous &&
prevProps.index === nextProps.index &&
- prevProps.screenHeight === nextProps.screenHeight
+ prevProps.screenHeight === nextProps.screenHeight &&
+ prevProps.screenWidth === nextProps.screenWidth
) {
return true;
}
diff --git a/package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx b/package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx
index 0fe17ca843..8978ff5fb9 100644
--- a/package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx
+++ b/package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx
@@ -1,8 +1,11 @@
-import React, { useState } from 'react';
+import React, { RefObject, useEffect, useRef, useState } from 'react';
import { StyleSheet, View, ViewStyle } from 'react-native';
import type { StyleProp } from 'react-native';
import Animated, { SharedValue } from 'react-native-reanimated';
+import { useImageGalleryContext } from '../../../contexts/imageGalleryContext/ImageGalleryContext';
+import { useStateStore } from '../../../hooks';
+
import {
isVideoPlayerAvailable,
NativeHandlers,
@@ -12,29 +15,27 @@ import {
VideoType,
} from '../../../native';
+import {
+ ImageGalleryAsset,
+ ImageGalleryState,
+} from '../../../state-store/image-gallery-state-store';
+import { VideoPlayerState } from '../../../state-store/video-player';
+import { ONE_SECOND_IN_MILLISECONDS } from '../../../utils/constants';
import { Spinner } from '../../UIComponents/Spinner';
import { useAnimatedGalleryStyle } from '../hooks/useAnimatedGalleryStyle';
+import { useImageGalleryVideoPlayer } from '../hooks/useImageGalleryVideoPlayer';
const oneEighth = 1 / 8;
export type AnimatedGalleryVideoType = {
attachmentId: string;
- handleEnd: () => void;
- handleLoad: (index: string, duration: number) => void;
- handleProgress: (index: string, progress: number, hasEnd?: boolean) => void;
index: number;
offsetScale: SharedValue;
- paused: boolean;
- previous: boolean;
scale: SharedValue;
screenHeight: number;
- selected: boolean;
- shouldRender: boolean;
- source: { uri: string };
+ photo: ImageGalleryAsset;
translateX: SharedValue;
translateY: SharedValue;
- videoRef: React.RefObject;
- repeat?: boolean;
style?: StyleProp;
};
@@ -48,46 +49,66 @@ const styles = StyleSheet.create({
},
});
+const imageGallerySelector = (state: ImageGalleryState) => ({
+ currentIndex: state.currentIndex,
+});
+
+const videoPlayerSelector = (state: VideoPlayerState) => ({
+ isPlaying: state.isPlaying,
+});
+
export const AnimatedGalleryVideo = React.memo(
(props: AnimatedGalleryVideoType) => {
const [opacity, setOpacity] = useState(1);
+ const { imageGalleryStateStore } = useImageGalleryContext();
+ const { currentIndex } = useStateStore(imageGalleryStateStore.state, imageGallerySelector);
const {
attachmentId,
- handleEnd,
- handleLoad,
- handleProgress,
index,
offsetScale,
- paused,
- previous,
- repeat,
scale,
screenHeight,
- selected,
- shouldRender,
- source,
style,
+ photo,
translateX,
translateY,
- videoRef,
} = props;
+
+ const videoRef = useRef(null);
+
+ const videoPlayer = useImageGalleryVideoPlayer({
+ id: attachmentId,
+ });
+
+ useEffect(() => {
+ if (videoRef.current) {
+ videoPlayer.initPlayer({ playerRef: videoRef.current });
+ }
+
+ return () => {
+ videoPlayer.playerRef = null;
+ };
+ }, [videoPlayer]);
+
+ const { isPlaying } = useStateStore(videoPlayer.state, videoPlayerSelector);
+
const onLoadStart = () => {
setOpacity(1);
};
const onLoad = (payload: VideoPayloadData) => {
setOpacity(0);
- // Duration is in seconds so we convert to milliseconds.
- handleLoad(attachmentId, payload.duration * 1000);
+
+ videoPlayer.duration = payload.duration * ONE_SECOND_IN_MILLISECONDS;
};
const onEnd = () => {
- handleEnd();
+ videoPlayer.stop();
};
const onProgress = (data: VideoProgressData) => {
- handleProgress(attachmentId, data.currentTime / data.seekableDuration);
+ videoPlayer.position = data.currentTime * ONE_SECOND_IN_MILLISECONDS;
};
const onBuffer = ({ isBuffering }: { isBuffering: boolean }) => {
@@ -108,13 +129,10 @@ export const AnimatedGalleryVideo = React.memo(
} else {
// Update your UI for the loaded state
setOpacity(0);
- handleLoad(attachmentId, playbackStatus.durationMillis);
+ videoPlayer.duration = playbackStatus.durationMillis;
if (playbackStatus.isPlaying) {
// Update your UI for the playing state
- handleProgress(
- attachmentId,
- playbackStatus.positionMillis / playbackStatus.durationMillis,
- );
+ videoPlayer.progress = playbackStatus.positionMillis / playbackStatus.durationMillis;
}
if (playbackStatus.isBuffering) {
@@ -124,11 +142,15 @@ export const AnimatedGalleryVideo = React.memo(
if (playbackStatus.didJustFinish && !playbackStatus.isLooping) {
// The player has just finished playing and will stop. Maybe you want to play something else?
- handleEnd();
+ videoPlayer.stop();
}
}
};
+ const selected = currentIndex === index;
+ const previous = currentIndex > index;
+ const shouldRender = Math.abs(currentIndex - index) < 4;
+
const animatedStyles = useAnimatedGalleryStyle({
index,
offsetScale,
@@ -164,13 +186,13 @@ export const AnimatedGalleryVideo = React.memo(
onLoadStart={onLoadStart}
onPlaybackStatusUpdate={onPlayBackStatusUpdate}
onProgress={onProgress}
- paused={paused}
- repeat={repeat}
+ paused={!isPlaying}
+ repeat={true}
resizeMode='contain'
style={style}
testID='video-player'
- uri={source.uri}
- videoRef={videoRef}
+ uri={photo.uri}
+ videoRef={videoRef as RefObject}
/>
) : null}
{
if (
- prevProps.paused === nextProps.paused &&
- prevProps.repeat === nextProps.repeat &&
- prevProps.shouldRender === nextProps.shouldRender &&
- prevProps.source.uri === nextProps.source.uri &&
prevProps.screenHeight === nextProps.screenHeight &&
- prevProps.selected === nextProps.selected &&
- prevProps.previous === nextProps.previous &&
- prevProps.index === nextProps.index
+ prevProps.index === nextProps.index &&
+ prevProps.photo === nextProps.photo
) {
return true;
}
diff --git a/package/src/components/ImageGallery/components/ImageGalleryFooter.tsx b/package/src/components/ImageGallery/components/ImageGalleryFooter.tsx
index a9592771d1..f3ac4e53e8 100644
--- a/package/src/components/ImageGallery/components/ImageGalleryFooter.tsx
+++ b/package/src/components/ImageGallery/components/ImageGalleryFooter.tsx
@@ -16,19 +16,16 @@ import Animated, {
import { ImageGalleryVideoControl } from './ImageGalleryVideoControl';
+import { useImageGalleryContext } from '../../../contexts/imageGalleryContext/ImageGalleryContext';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
+import { useStateStore } from '../../../hooks/useStateStore';
import { Grid as GridIconDefault, Share as ShareIconDefault } from '../../../icons';
-import {
- isFileSystemAvailable,
- isShareImageAvailable,
- NativeHandlers,
- VideoType,
-} from '../../../native';
+import { isFileSystemAvailable, isShareImageAvailable, NativeHandlers } from '../../../native';
+import { ImageGalleryState } from '../../../state-store/image-gallery-state-store';
import { FileTypes } from '../../../types/types';
import { SafeAreaView } from '../../UIComponents/SafeAreaViewWrapper';
-import type { Photo } from '../ImageGallery';
const ReanimatedSafeAreaView = Animated.createAnimatedComponent
? Animated.createAnimatedComponent(SafeAreaView)
@@ -36,29 +33,20 @@ const ReanimatedSafeAreaView = Animated.createAnimatedComponent
export type ImageGalleryFooterCustomComponent = ({
openGridView,
- photo,
share,
shareMenuOpen,
}: {
openGridView: () => void;
share: () => Promise;
shareMenuOpen: boolean;
- photo?: Photo;
}) => React.ReactElement | null;
export type ImageGalleryFooterVideoControlProps = {
- duration: number;
- onPlayPause: (status?: boolean) => void;
- paused: boolean;
- progress: number;
- videoRef: React.RefObject;
+ attachmentId: string;
};
export type ImageGalleryFooterVideoControlComponent = ({
- duration,
- onPlayPause,
- paused,
- progress,
+ attachmentId,
}: ImageGalleryFooterVideoControlProps) => React.ReactElement | null;
export type ImageGalleryFooterCustomComponentProps = {
@@ -72,38 +60,27 @@ export type ImageGalleryFooterCustomComponentProps = {
type ImageGalleryFooterPropsWithContext = ImageGalleryFooterCustomComponentProps & {
accessibilityLabel: string;
- duration: number;
- onPlayPause: () => void;
opacity: SharedValue;
openGridView: () => void;
- paused: boolean;
- photo: Photo;
- photoLength: number;
- progress: number;
- selectedIndex: number;
- videoRef: React.RefObject;
visible: SharedValue;
};
+const imageGallerySelector = (state: ImageGalleryState) => ({
+ asset: state.assets[state.currentIndex],
+ currentIndex: state.currentIndex,
+});
+
export const ImageGalleryFooterWithContext = (props: ImageGalleryFooterPropsWithContext) => {
const {
accessibilityLabel,
centerElement,
- duration,
GridIcon,
leftElement,
- onPlayPause,
opacity,
openGridView,
- paused,
- photo,
- photoLength,
- progress,
rightElement,
- selectedIndex,
ShareIcon,
videoControlElement,
- videoRef,
visible,
} = props;
@@ -119,6 +96,8 @@ export const ImageGalleryFooterWithContext = (props: ImageGalleryFooterPropsWith
},
} = useTheme();
const { t } = useTranslationContext();
+ const { imageGalleryStateStore } = useImageGalleryContext();
+ const { asset, currentIndex } = useStateStore(imageGalleryStateStore.state, imageGallerySelector);
const footerStyle = useAnimatedStyle(
() => ({
@@ -141,26 +120,26 @@ export const ImageGalleryFooterWithContext = (props: ImageGalleryFooterPropsWith
if (!NativeHandlers.shareImage || !NativeHandlers.deleteFile) {
return;
}
- const extension = photo.mime_type?.split('/')[1] || 'jpg';
- const shouldDownload = photo.uri && photo.uri.includes('http');
+ const extension = asset.mime_type?.split('/')[1] || 'jpg';
+ const shouldDownload = asset.uri && asset.uri.includes('http');
let localFile;
// If the file is already uploaded to a CDN, create a local reference to
// it first; otherwise just use the local file
if (shouldDownload) {
setSavingInProgress(true);
localFile = await NativeHandlers.saveFile({
- fileName: `${photo.user?.id || 'ChatPhoto'}-${
- photo.messageId
- }-${selectedIndex}.${extension}`,
- fromUrl: photo.uri,
+ fileName: `${asset.user?.id || 'ChatPhoto'}-${
+ asset.messageId
+ }-${currentIndex}.${extension}`,
+ fromUrl: asset.uri,
});
setSavingInProgress(false);
} else {
- localFile = photo.uri;
+ localFile = asset.uri;
}
// `image/jpeg` is added for the case where the mime_type isn't available for a file/image
- await NativeHandlers.shareImage({ type: photo.mime_type || 'image/jpeg', url: localFile });
+ await NativeHandlers.shareImage({ type: asset.mime_type || 'image/jpeg', url: localFile });
// Only delete the file if a local reference has been created beforehand
if (shouldDownload) {
await NativeHandlers.deleteFile({ uri: localFile });
@@ -172,6 +151,10 @@ export const ImageGalleryFooterWithContext = (props: ImageGalleryFooterPropsWith
shareIsInProgressRef.current = false;
};
+ if (!asset) {
+ return null;
+ }
+
return (
- {photo.type === FileTypes.Video ? (
+ {asset.type === FileTypes.Video ? (
videoControlElement ? (
- videoControlElement({ duration, onPlayPause, paused, progress, videoRef })
+ videoControlElement({ attachmentId: asset.id })
) : (
-
+
)
) : null}
{leftElement ? (
- leftElement({ openGridView, photo, share, shareMenuOpen: savingInProgress })
+ leftElement({ openGridView, share, shareMenuOpen: savingInProgress })
) : (
)}
{centerElement ? (
- centerElement({ openGridView, photo, share, shareMenuOpen: savingInProgress })
+ centerElement({ openGridView, share, shareMenuOpen: savingInProgress })
) : (
{t('{{ index }} of {{ photoLength }}', {
- index: selectedIndex + 1,
- photoLength,
+ index: currentIndex + 1,
+ photoLength: imageGalleryStateStore.assets.length,
})}
)}
{rightElement ? (
- rightElement({ openGridView, photo, share, shareMenuOpen: savingInProgress })
+ rightElement({ openGridView, share, shareMenuOpen: savingInProgress })
) : (
@@ -265,49 +242,8 @@ const ShareButton = ({ share, ShareIcon, savingInProgress }: ShareButtonProps) =
);
};
-const areEqual = (
- prevProps: ImageGalleryFooterPropsWithContext,
- nextProps: ImageGalleryFooterPropsWithContext,
-) => {
- const {
- duration: prevDuration,
- paused: prevPaused,
- progress: prevProgress,
- selectedIndex: prevSelectedIndex,
- } = prevProps;
- const {
- duration: nextDuration,
- paused: nextPaused,
- progress: nextProgress,
- selectedIndex: nextSelectedIndex,
- } = nextProps;
-
- const isDurationEqual = prevDuration === nextDuration;
- if (!isDurationEqual) {
- return false;
- }
-
- const isPausedEqual = prevPaused === nextPaused;
- if (!isPausedEqual) {
- return false;
- }
-
- const isProgressEqual = prevProgress === nextProgress;
- if (!isProgressEqual) {
- return false;
- }
-
- const isSelectedIndexEqual = prevSelectedIndex === nextSelectedIndex;
- if (!isSelectedIndexEqual) {
- return false;
- }
-
- return true;
-};
-
const MemoizedImageGalleryFooter = React.memo(
ImageGalleryFooterWithContext,
- areEqual,
) as typeof ImageGalleryFooterWithContext;
export type ImageGalleryFooterProps = ImageGalleryFooterPropsWithContext;
diff --git a/package/src/components/ImageGallery/components/ImageGalleryHeader.tsx b/package/src/components/ImageGallery/components/ImageGalleryHeader.tsx
index d009e93ef0..f077674d6f 100644
--- a/package/src/components/ImageGallery/components/ImageGalleryHeader.tsx
+++ b/package/src/components/ImageGallery/components/ImageGalleryHeader.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { Pressable, StyleSheet, Text, View, ViewStyle } from 'react-native';
@@ -9,14 +9,16 @@ import Animated, {
useAnimatedStyle,
} from 'react-native-reanimated';
+import { useImageGalleryContext } from '../../../contexts/imageGalleryContext/ImageGalleryContext';
import { useOverlayContext } from '../../../contexts/overlayContext/OverlayContext';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
+import { useStateStore } from '../../../hooks/useStateStore';
import { Close } from '../../../icons';
+import { ImageGalleryState } from '../../../state-store/image-gallery-state-store';
import { getDateString } from '../../../utils/i18n/getDateString';
import { SafeAreaView } from '../../UIComponents/SafeAreaViewWrapper';
-import type { Photo } from '../ImageGallery';
const ReanimatedSafeAreaView = Animated.createAnimatedComponent
? Animated.createAnimatedComponent(SafeAreaView)
@@ -24,10 +26,8 @@ const ReanimatedSafeAreaView = Animated.createAnimatedComponent
export type ImageGalleryHeaderCustomComponent = ({
hideOverlay,
- photo,
}: {
hideOverlay: () => void;
- photo?: Photo;
}) => React.ReactElement | null;
export type ImageGalleryHeaderCustomComponentProps = {
@@ -40,12 +40,14 @@ export type ImageGalleryHeaderCustomComponentProps = {
type Props = ImageGalleryHeaderCustomComponentProps & {
opacity: SharedValue;
visible: SharedValue;
- photo?: Photo;
- /* Lookup key in the language corresponding translations sheet to perform date formatting */
};
+const imageGallerySelector = (state: ImageGalleryState) => ({
+ asset: state.assets[state.currentIndex],
+});
+
export const ImageGalleryHeader = (props: Props) => {
- const { centerElement, CloseIcon, leftElement, opacity, photo, rightElement, visible } = props;
+ const { centerElement, CloseIcon, leftElement, opacity, rightElement, visible } = props;
const [height, setHeight] = useState(200);
const {
theme: {
@@ -64,17 +66,19 @@ export const ImageGalleryHeader = (props: Props) => {
},
} = useTheme();
const { t, tDateTimeParser } = useTranslationContext();
+ const { imageGalleryStateStore } = useImageGalleryContext();
+ const { asset } = useStateStore(imageGalleryStateStore.state, imageGallerySelector);
const { setOverlay } = useOverlayContext();
const date = useMemo(
() =>
getDateString({
- date: photo?.created_at,
+ date: asset?.created_at,
t,
tDateTimeParser,
timestampTranslationKey: 'timestamp/ImageGalleryHeader',
}),
- [photo?.created_at, t, tDateTimeParser],
+ [asset?.created_at, t, tDateTimeParser],
);
const headerStyle = useAnimatedStyle(() => ({
@@ -90,6 +94,12 @@ export const ImageGalleryHeader = (props: Props) => {
setOverlay('none');
};
+ useEffect(() => {
+ return () => {
+ imageGalleryStateStore.clear();
+ };
+ }, [imageGalleryStateStore]);
+
return (
setHeight(event.nativeEvent.layout.height)}
@@ -101,7 +111,7 @@ export const ImageGalleryHeader = (props: Props) => {
>
{leftElement ? (
- leftElement({ hideOverlay, photo })
+ leftElement({ hideOverlay })
) : (
@@ -110,17 +120,17 @@ export const ImageGalleryHeader = (props: Props) => {
)}
{centerElement ? (
- centerElement({ hideOverlay, photo })
+ centerElement({ hideOverlay })
) : (
- {photo?.user?.name || photo?.user?.id || t('Unknown User')}
+ {asset?.user?.name || asset?.user?.id || t('Unknown User')}
{date && {date}}
)}
{rightElement ? (
- rightElement({ hideOverlay, photo })
+ rightElement({ hideOverlay })
) : (
)}
diff --git a/package/src/components/ImageGallery/components/ImageGalleryVideoControl.tsx b/package/src/components/ImageGallery/components/ImageGalleryVideoControl.tsx
index 18ff62f08f..39b4a8b45f 100644
--- a/package/src/components/ImageGallery/components/ImageGalleryVideoControl.tsx
+++ b/package/src/components/ImageGallery/components/ImageGalleryVideoControl.tsx
@@ -5,9 +5,12 @@ import type { ImageGalleryFooterVideoControlProps } from './ImageGalleryFooter';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
+import { useStateStore } from '../../../hooks/useStateStore';
import { Pause, Play } from '../../../icons';
+import { VideoPlayerState } from '../../../state-store/video-player';
import { getDurationLabelFromDuration } from '../../../utils/utils';
import { ProgressControl } from '../../ProgressControl/ProgressControl';
+import { useImageGalleryVideoPlayer } from '../hooks/useImageGalleryVideoPlayer';
const styles = StyleSheet.create({
durationTextStyle: {
@@ -40,88 +43,74 @@ const styles = StyleSheet.create({
},
});
-export const ImageGalleryVideoControl = React.memo(
- (props: ImageGalleryFooterVideoControlProps) => {
- const { duration, onPlayPause, paused, progress, videoRef } = props;
+const videoPlayerSelector = (state: VideoPlayerState) => ({
+ duration: state.duration,
+ isPlaying: state.isPlaying,
+ progress: state.progress,
+});
+
+export const ImageGalleryVideoControl = React.memo((props: ImageGalleryFooterVideoControlProps) => {
+ const { attachmentId } = props;
+
+ const videoPlayer = useImageGalleryVideoPlayer({
+ id: attachmentId,
+ });
- const videoDuration = getDurationLabelFromDuration(duration);
+ const { duration, isPlaying, progress } = useStateStore(videoPlayer.state, videoPlayerSelector);
- const progressValueInSeconds = progress * duration;
+ const videoDuration = getDurationLabelFromDuration(duration);
- const progressDuration = getDurationLabelFromDuration(progressValueInSeconds);
+ const progressValueInSeconds = progress * duration;
- const {
- theme: {
- colors: { accent_blue, black, static_black, static_white },
- imageGallery: {
- videoControl: { durationTextStyle, progressDurationText, roundedView, videoContainer },
- },
+ const progressDuration = getDurationLabelFromDuration(progressValueInSeconds);
+
+ const {
+ theme: {
+ colors: { black, static_black, static_white },
+ imageGallery: {
+ videoControl: { durationTextStyle, progressDurationText, roundedView, videoContainer },
},
- } = useTheme();
-
- const handlePlayPause = async () => {
- // Note: Not particularly sure why this was ever added, but
- // will keep it for now for backwards compatibility.
- if (progress === 1) {
- // For expo CLI, expo-av
- if (videoRef.current?.setPositionAsync) {
- await videoRef.current.setPositionAsync(0);
- }
- // For expo CLI, expo-video
- if (videoRef.current?.replay) {
- await videoRef.current.replay();
- }
- }
- onPlayPause();
- };
-
- return (
-
-
-
- {paused ? (
-
- ) : (
-
- )}
-
-
-
- {progressDuration}
-
-
-
-
+ },
+ } = useTheme();
+
+ const handlePlayPause = () => {
+ videoPlayer.toggle();
+ };
-
- {videoDuration}
-
+ return (
+
+
+
+ {!isPlaying ? (
+
+ ) : (
+
+ )}
+
+
+
+ {progressDuration}
+
+
+
- );
- },
- (prevProps, nextProps) => {
- if (
- prevProps.duration === nextProps.duration &&
- prevProps.paused === nextProps.paused &&
- prevProps.progress === nextProps.progress
- ) {
- return true;
- } else {
- return false;
- }
- },
-);
+
+
+ {videoDuration}
+
+
+ );
+});
ImageGalleryVideoControl.displayName = 'ImageGalleryVideoControl{imageGallery{videoControl}}';
diff --git a/package/src/components/ImageGallery/components/ImageGrid.tsx b/package/src/components/ImageGallery/components/ImageGrid.tsx
index 0cf5a70172..f3982f9715 100644
--- a/package/src/components/ImageGallery/components/ImageGrid.tsx
+++ b/package/src/components/ImageGallery/components/ImageGrid.tsx
@@ -2,14 +2,18 @@ import React from 'react';
import { Image, StyleSheet, View } from 'react-native';
import { VideoThumbnail } from '../../../components/Attachment/VideoThumbnail';
+import { useImageGalleryContext } from '../../../contexts/imageGalleryContext/ImageGalleryContext';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
+import { useStateStore } from '../../../hooks/useStateStore';
import { useViewport } from '../../../hooks/useViewport';
+import type {
+ ImageGalleryAsset,
+ ImageGalleryState,
+} from '../../../state-store/image-gallery-state-store';
import { FileTypes } from '../../../types/types';
import { BottomSheetFlatList } from '../../BottomSheetCompatibility/BottomSheetFlatList';
import { BottomSheetTouchableOpacity } from '../../BottomSheetCompatibility/BottomSheetTouchableOpacity';
-import type { Photo } from '../ImageGallery';
-
const styles = StyleSheet.create({
avatarImage: {
borderRadius: 22,
@@ -34,7 +38,7 @@ const styles = StyleSheet.create({
export type ImageGalleryGridImageComponent = ({
item,
}: {
- item: Photo & {
+ item: ImageGalleryAsset & {
selectAndClose: () => void;
numberOfImageGalleryGridColumns?: number;
};
@@ -45,7 +49,7 @@ export type ImageGalleryGridImageComponents = {
imageComponent?: ImageGalleryGridImageComponent;
};
-export type GridImageItem = Photo &
+export type GridImageItem = ImageGalleryAsset &
ImageGalleryGridImageComponents & {
selectAndClose: () => void;
numberOfImageGalleryGridColumns?: number;
@@ -87,28 +91,17 @@ const renderItem = ({ item }: { item: GridImageItem }) => void;
- photos: Photo[];
- setSelectedMessage: React.Dispatch<
- React.SetStateAction<
- | {
- messageId?: string | undefined;
- url?: string | undefined;
- }
- | undefined
- >
- >;
numberOfImageGalleryGridColumns?: number;
};
+const imageGallerySelector = (state: ImageGalleryState) => ({
+ assets: state.assets,
+});
+
export const ImageGrid = (props: ImageGridType) => {
- const {
- avatarComponent,
- closeGridView,
- imageComponent,
- numberOfImageGalleryGridColumns,
- photos,
- setSelectedMessage,
- } = props;
+ const { avatarComponent, closeGridView, imageComponent, numberOfImageGalleryGridColumns } = props;
+ const { imageGalleryStateStore } = useImageGalleryContext();
+ const { assets } = useStateStore(imageGalleryStateStore.state, imageGallerySelector);
const {
theme: {
@@ -119,13 +112,13 @@ export const ImageGrid = (props: ImageGridType) => {
},
} = useTheme();
- const imageGridItems = photos.map((photo) => ({
+ const imageGridItems = assets.map((photo, index) => ({
...photo,
avatarComponent,
imageComponent,
numberOfImageGalleryGridColumns,
selectAndClose: () => {
- setSelectedMessage({ messageId: photo.messageId, url: photo.uri });
+ imageGalleryStateStore.currentIndex = index;
closeGridView();
},
}));
diff --git a/package/src/components/ImageGallery/components/__tests__/ImageGalleryHeader.test.tsx b/package/src/components/ImageGallery/components/__tests__/ImageGalleryHeader.test.tsx
index 00b484f0a0..41bffa5fb1 100644
--- a/package/src/components/ImageGallery/components/__tests__/ImageGalleryHeader.test.tsx
+++ b/package/src/components/ImageGallery/components/__tests__/ImageGalleryHeader.test.tsx
@@ -1,12 +1,44 @@
-import React from 'react';
+import React, { PropsWithChildren, useState } from 'react';
import { SharedValue, useSharedValue } from 'react-native-reanimated';
import { render, renderHook, waitFor } from '@testing-library/react-native';
-import { ThemeProvider } from '../../../../contexts/themeContext/ThemeContext';
+import { LocalMessage } from 'stream-chat';
+import {
+ ImageGalleryContext,
+ ImageGalleryContextValue,
+} from '../../../../contexts/imageGalleryContext/ImageGalleryContext';
+import { OverlayProvider } from '../../../../contexts/overlayContext/OverlayProvider';
+import { generateImageAttachment } from '../../../../mock-builders/generator/attachment';
+import { generateMessage } from '../../../../mock-builders/generator/message';
+import { ImageGalleryStateStore } from '../../../../state-store/image-gallery-state-store';
import { ImageGalleryHeader } from '../ImageGalleryHeader';
+const ImageGalleryComponentWrapper = ({ children }: PropsWithChildren) => {
+ const initialImageGalleryStateStore = new ImageGalleryStateStore();
+ const attachment = generateImageAttachment();
+ initialImageGalleryStateStore.openImageGallery({
+ message: generateMessage({
+ attachments: [attachment],
+ user: {},
+ }) as unknown as LocalMessage,
+ selectedAttachmentUrl: attachment.url,
+ });
+
+ const [imageGalleryStateStore] = useState(initialImageGalleryStateStore);
+
+ return (
+ }}>
+
+ {children}
+
+
+ );
+};
+
it('doesnt fail if fromNow is not available on first render', async () => {
try {
let sharedValueOpacity: SharedValue;
@@ -16,18 +48,14 @@ it('doesnt fail if fromNow is not available on first render', async () => {
sharedValueVisible = useSharedValue(1);
});
const { getAllByText } = render(
-
+
- ,
+ ,
);
await waitFor(() => {
expect(getAllByText('Unknown User')).toBeTruthy();
diff --git a/package/src/components/ImageGallery/hooks/useImageGalleryGestures.tsx b/package/src/components/ImageGallery/hooks/useImageGalleryGestures.tsx
index c9b8a2e1b3..b7a400bec4 100644
--- a/package/src/components/ImageGallery/hooks/useImageGalleryGestures.tsx
+++ b/package/src/components/ImageGallery/hooks/useImageGalleryGestures.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState } from 'react';
+import { useRef, useState } from 'react';
import { Platform } from 'react-native';
import { Gesture, GestureType } from 'react-native-gesture-handler';
import {
@@ -14,7 +14,9 @@ import {
import { useImageGalleryContext } from '../../../contexts/imageGalleryContext/ImageGalleryContext';
import { useOverlayContext } from '../../../contexts/overlayContext/OverlayContext';
+import { useStateStore } from '../../../hooks';
import { NativeHandlers } from '../../../native';
+import { ImageGalleryState } from '../../../state-store/image-gallery-state-store';
export enum HasPinched {
FALSE = 0,
@@ -29,6 +31,10 @@ export enum IsSwiping {
const MARGIN = 32;
+const imageGallerySelector = (state: ImageGalleryState) => ({
+ currentIndex: state.currentIndex,
+});
+
export const useImageGalleryGestures = ({
currentImageHeight,
halfScreenHeight,
@@ -36,12 +42,9 @@ export const useImageGalleryGestures = ({
headerFooterVisible,
offsetScale,
overlayOpacity,
- photoLength,
scale,
screenHeight,
screenWidth,
- selectedIndex,
- setSelectedIndex,
translateX,
translateY,
translationX,
@@ -52,12 +55,9 @@ export const useImageGalleryGestures = ({
headerFooterVisible: SharedValue;
offsetScale: SharedValue;
overlayOpacity: SharedValue;
- photoLength: number;
scale: SharedValue;
screenHeight: number;
screenWidth: number;
- selectedIndex: number;
- setSelectedIndex: React.Dispatch>;
translateX: SharedValue;
translateY: SharedValue;
translationX: SharedValue;
@@ -72,8 +72,11 @@ export const useImageGalleryGestures = ({
* it was always assumed that one started at index 0 in the
* gallery.
* */
- const [index, setIndex] = useState(selectedIndex);
- const { setMessages, setSelectedMessage } = useImageGalleryContext();
+ const { imageGalleryStateStore } = useImageGalleryContext();
+ const { currentIndex } = useStateStore(imageGalleryStateStore.state, imageGallerySelector);
+
+ const [index, setIndex] = useState(currentIndex);
+
/**
* Gesture handler refs
*/
@@ -100,20 +103,6 @@ export const useImageGalleryGestures = ({
const focalX = useSharedValue(0);
const focalY = useSharedValue(0);
- /**
- * if a specific image index > 0 has been passed in
- * while creating the hook, set the value of the index
- * reference to its value.
- *
- * This makes it possible to seelct an image in the list,
- * and scroll/pan as normal. Prior to this,
- * it was always assumed that one started at index 0 in the
- * gallery.
- * */
- useEffect(() => {
- setIndex(selectedIndex);
- }, [selectedIndex]);
-
/**
* Shared values for movement
*/
@@ -167,6 +156,22 @@ export const useImageGalleryGestures = ({
offsetScale.value = 1;
};
+ const assetsLength = imageGalleryStateStore.assets.length;
+
+ const clearImageGallery = () => {
+ runOnJS(setOverlay)('none');
+ };
+
+ const moveToNextImage = () => {
+ runOnJS(setIndex)(index + 1);
+ imageGalleryStateStore.currentIndex = index + 1;
+ };
+
+ const moveToPreviousImage = () => {
+ runOnJS(setIndex)(index - 1);
+ imageGalleryStateStore.currentIndex = index - 1;
+ };
+
/**
* We use simultaneousHandlers to allow pan and pinch gesture handlers
* depending on the gesture. The touch is fully handled by the pinch
@@ -290,7 +295,7 @@ export const useImageGalleryGestures = ({
* As we move towards the left to move to next image, the translationX value will be negative on X axis.
*/
if (
- index < photoLength - 1 &&
+ index < assetsLength - 1 &&
Math.abs(halfScreenWidth * (scale.value - 1) + offsetX.value) < 3 &&
translateX.value < 0 &&
finalXPosition > halfScreenWidth &&
@@ -305,8 +310,7 @@ export const useImageGalleryGestures = ({
},
() => {
resetMovementValues();
- runOnJS(setIndex)(index + 1);
- runOnJS(setSelectedIndex)(index + 1);
+ runOnJS(moveToNextImage)();
},
);
@@ -333,8 +337,7 @@ export const useImageGalleryGestures = ({
},
() => {
resetMovementValues();
- runOnJS(setIndex)(index - 1);
- runOnJS(setSelectedIndex)(index - 1);
+ runOnJS(moveToPreviousImage)();
},
);
}
@@ -433,9 +436,7 @@ export const useImageGalleryGestures = ({
easing: Easing.out(Easing.ease),
},
() => {
- runOnJS(setSelectedMessage)(undefined);
- runOnJS(setMessages)([]);
- runOnJS(setOverlay)('none');
+ runOnJS(clearImageGallery)();
},
);
scale.value = withTiming(0.6, {
diff --git a/package/src/components/ImageGallery/hooks/useImageGalleryVideoPlayer.ts b/package/src/components/ImageGallery/hooks/useImageGalleryVideoPlayer.ts
new file mode 100644
index 0000000000..1961497070
--- /dev/null
+++ b/package/src/components/ImageGallery/hooks/useImageGalleryVideoPlayer.ts
@@ -0,0 +1,23 @@
+import { useMemo } from 'react';
+
+import { useImageGalleryContext } from '../../../contexts/imageGalleryContext/ImageGalleryContext';
+import { VideoPlayerOptions } from '../../../state-store/video-player';
+
+export type UseImageGalleryVideoPlayerProps = VideoPlayerOptions;
+
+/**
+ * Hook to get the video player instance.
+ * @param options - The options for the video player.
+ * @returns The video player instance.
+ */
+export const useImageGalleryVideoPlayer = (options: UseImageGalleryVideoPlayerProps) => {
+ const { autoPlayVideo, imageGalleryStateStore } = useImageGalleryContext();
+ const videoPlayer = useMemo(() => {
+ return imageGalleryStateStore.videoPlayerPool.getOrAddPlayer({
+ ...options,
+ autoPlay: autoPlayVideo,
+ });
+ }, [autoPlayVideo, imageGalleryStateStore, options]);
+
+ return videoPlayer;
+};
diff --git a/package/src/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.tsx b/package/src/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.tsx
new file mode 100644
index 0000000000..b535c2680f
--- /dev/null
+++ b/package/src/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.tsx
@@ -0,0 +1,75 @@
+import React, { useEffect } from 'react';
+
+import {
+ Keyboard,
+ Platform,
+ KeyboardAvoidingViewProps as ReactNativeKeyboardAvoidingViewProps,
+} from 'react-native';
+
+import { KeyboardCompatibleView as KeyboardCompatibleViewDefault } from './KeyboardCompatibleView';
+
+type ExtraKeyboardControllerProps = {
+ behavior?: 'translate-with-padding';
+};
+
+type KeyboardControllerModule = typeof import('react-native-keyboard-controller');
+
+const optionalRequire = (): T | undefined => {
+ try {
+ return require('react-native-keyboard-controller') as T;
+ } catch {
+ return undefined;
+ }
+};
+
+export type KeyboardCompatibleViewProps = ReactNativeKeyboardAvoidingViewProps &
+ ExtraKeyboardControllerProps;
+
+const KeyboardControllerPackage = optionalRequire();
+
+const { AndroidSoftInputModes, KeyboardController, KeyboardProvider, KeyboardAvoidingView } =
+ KeyboardControllerPackage ?? {};
+
+export const KeyboardCompatibleView = (props: KeyboardCompatibleViewProps) => {
+ const { behavior = 'translate-with-padding', children, ...rest } = props;
+
+ useEffect(() => {
+ if (AndroidSoftInputModes) {
+ KeyboardController?.setInputMode(AndroidSoftInputModes.SOFT_INPUT_ADJUST_RESIZE);
+ }
+
+ return () => KeyboardController?.setDefaultMode();
+ }, []);
+
+ if (KeyboardProvider && KeyboardAvoidingView) {
+ return (
+
+ {/* @ts-expect-error - The reason is that react-native-keyboard-controller's KeyboardAvoidingViewProps is a discriminated union, not a simple behavior union so it complains about the `position` value passed. */}
+
+ {children}
+
+
+ );
+ }
+ const compatibleBehavior =
+ behavior === 'translate-with-padding'
+ ? Platform.OS === 'ios'
+ ? 'padding'
+ : 'position'
+ : behavior;
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const dismissKeyboard = () => {
+ if (KeyboardControllerPackage?.KeyboardController) {
+ KeyboardControllerPackage?.KeyboardController.dismiss();
+ }
+ Keyboard.dismiss();
+};
+
+export { KeyboardControllerPackage };
diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx
index 1949d3d6bb..7e9dcd714a 100644
--- a/package/src/components/Message/Message.tsx
+++ b/package/src/components/Message/Message.tsx
@@ -1,5 +1,14 @@
-import React, { useMemo, useState } from 'react';
-import { GestureResponderEvent, Keyboard, StyleProp, View, ViewStyle } from 'react-native';
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import {
+ GestureResponderEvent,
+ StyleProp,
+ useWindowDimensions,
+ View,
+ ViewStyle,
+} from 'react-native';
+
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { Portal } from 'react-native-teleport';
import type { Attachment, LocalMessage, UserResponse } from 'stream-chat';
@@ -9,6 +18,7 @@ import { useMessageActions } from './hooks/useMessageActions';
import { useMessageDeliveredData } from './hooks/useMessageDeliveryData';
import { useMessageReadData } from './hooks/useMessageReadData';
import { useProcessReactions } from './hooks/useProcessReactions';
+import { measureInWindow } from './utils/measureInWindow';
import { messageActions as defaultMessageActions } from './utils/messageActions';
import {
@@ -25,6 +35,7 @@ import {
useMessageComposerAPIContext,
} from '../../contexts/messageComposerContext/MessageComposerAPIContext';
import { MessageContextValue, MessageProvider } from '../../contexts/messageContext/MessageContext';
+import { useMessageListItemContext } from '../../contexts/messageListItemContext/MessageListItemContext';
import {
MessagesContextValue,
useMessagesContext,
@@ -37,17 +48,27 @@ import {
useTranslationContext,
} from '../../contexts/translationContext/TranslationContext';
+import { useStableCallback } from '../../hooks';
import { isVideoPlayerAvailable, NativeHandlers } from '../../native';
+import {
+ closeOverlay,
+ openOverlay,
+ setOverlayBottomH,
+ setOverlayMessageH,
+ setOverlayTopH,
+ useIsOverlayActive,
+} from '../../state-store';
import { FileTypes } from '../../types/types';
import {
checkMessageEquality,
hasOnlyEmojis,
isBlockedMessage,
isBouncedMessage,
- isEditedMessage,
MessageStatusTypes,
} from '../../utils/utils';
import type { Thumbnail } from '../Attachment/utils/buildGallery/types';
+import { dismissKeyboard } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView';
+import { BottomSheetModal } from '../UIComponents';
export type TouchableEmitter =
| 'fileAttachment'
@@ -192,6 +213,13 @@ export type MessagePropsWithContext = Pick<
| 'supportedReactions'
| 'updateMessage'
| 'PollContent'
+ // TODO: remove this comment later, using it as a pragma mark
+ | 'MessageUserReactions'
+ | 'MessageUserReactionsAvatar'
+ | 'MessageUserReactionsItem'
+ | 'MessageReactionPicker'
+ | 'MessageActionList'
+ | 'MessageActionListItem'
> &
Pick &
Pick & {
@@ -219,12 +247,10 @@ export type MessagePropsWithContext = Pick<
* each individual Message component.
*/
const MessageWithContext = (props: MessagePropsWithContext) => {
- const [messageOverlayVisible, setMessageOverlayVisible] = useState(false);
const [isErrorInMessage, setIsErrorInMessage] = useState(false);
- const [showMessageReactions, setShowMessageReactions] = useState(true);
+ const [showMessageReactions, setShowMessageReactions] = useState(false);
const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false);
- const [isEditedMessageOpen, setIsEditedMessageOpen] = useState(false);
- const [selectedReaction, setSelectedReaction] = useState(undefined);
+ // const [selectedReaction, setSelectedReaction] = useState(undefined);
const {
channel,
@@ -232,7 +258,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
deleteMessage: deleteMessageFromContext,
deleteReaction,
deliveredToCount,
- dismissKeyboard,
dismissKeyboardOnMessageTouch,
enableLongPress = true,
enforceUniqueReaction,
@@ -259,7 +284,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
MessageBlocked,
MessageBounce,
messageContentOrder: messageContentOrderProp,
- MessageMenu,
messagesContext,
MessageSimple,
onLongPressMessage: onLongPressMessageProp,
@@ -283,7 +307,15 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
updateMessage,
readBy,
setQuotedMessage,
+ MessageUserReactions,
+ MessageUserReactionsAvatar,
+ MessageUserReactionsItem,
+ MessageReactionPicker,
+ MessageActionList,
+ MessageActionListItem,
} = props;
+ // TODO: V9: Reconsider using safe area insets in every message.
+ const insets = useSafeAreaInsets();
const isMessageAIGenerated = messagesContext.isMessageAIGenerated;
const isAIGenerated = useMemo(
() => isMessageAIGenerated(message),
@@ -293,21 +325,40 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
const { client } = chatContext;
const {
theme: {
- colors: { targetedMessageBackground, bg_gradient_start },
+ colors: { bg_gradient_start },
messageSimple: { targetedMessageContainer, unreadUnderlayColor = bg_gradient_start, wrapper },
screenPadding,
+ semantics,
},
} = useTheme();
- const showMessageOverlay = async (showMessageReactions = false, selectedReaction?: string) => {
- await dismissKeyboard();
- setShowMessageReactions(showMessageReactions);
- setMessageOverlayVisible(true);
- setSelectedReaction(selectedReaction);
- };
+ const [rect, setRect] = useState<{ w: number; h: number; x: number; y: number } | undefined>(
+ undefined,
+ );
+ const { width: screenW } = useWindowDimensions();
+
+ const showMessageOverlay = useStableCallback(async () => {
+ dismissKeyboard();
+ try {
+ const layout = await measureInWindow(messageWrapperRef, insets);
+ setRect(layout);
+ setOverlayMessageH(layout);
+ openOverlay(message.id);
+ } catch (e) {
+ console.error(e);
+ }
+ });
+
+ const showReactionsOverlay = useStableCallback(() => {
+ if (!showMessageReactions) {
+ setShowMessageReactions(true);
+ }
+ });
+
+ const { setNativeScrollability } = useMessageListItemContext();
const dismissOverlay = () => {
- setMessageOverlayVisible(false);
+ closeOverlay();
};
const actionsEnabled =
@@ -341,10 +392,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
const onPress = (error = errorOrFailed) => {
if (dismissKeyboardOnMessageTouch) {
- Keyboard.dismiss();
- }
- if (isEditedMessage(message)) {
- setIsEditedMessageOpen((prevState) => !prevState);
+ dismissKeyboard();
}
const quotedMessage = message.quoted_message;
if (error) {
@@ -415,6 +463,8 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
acc.other = []; // remove other attachments if an image exists
}
// only add other attachments if there are no files/images
+ } else if (cur.type === FileTypes.Image && (cur.og_scrape_url || cur.title_link)) {
+ acc.files.push(cur);
} else if (!acc.files.length && !acc.images.length && !acc.videos.length) {
acc.other.push(cur);
}
@@ -620,7 +670,10 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
unpinMessage: handleTogglePinMessage,
};
+ const messageWrapperRef = useRef(null);
+
const onLongPress = () => {
+ setNativeScrollability(false);
if (hasAttachmentActions || isBlockedMessage(message) || !enableLongPress) {
return;
}
@@ -633,6 +686,12 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
showMessageOverlay();
};
+ const frozenMessage = useRef(message);
+ const { active: overlayActive } = useIsOverlayActive(message.id);
+
+ const messageHasOnlySingleAttachment =
+ !message.text && !message.quoted_message && message.attachments?.length === 1;
+
const messageContext = useCreateMessageContext({
actionsEnabled,
alignment,
@@ -647,13 +706,13 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
handleToggleReaction,
hasReactions,
images: attachments.images,
- isEditedMessageOpen,
isMessageAIGenerated,
isMyMessage,
lastGroupMessage: groupStyles?.[0] === 'single' || groupStyles?.[0] === 'bottom',
members,
- message,
+ message: overlayActive ? frozenMessage.current : message,
messageContentOrder,
+ messageHasOnlySingleAttachment,
myMessageTheme: messagesContext.myMessageTheme,
onLongPress: (payload) => {
const onLongPressArgs = {
@@ -716,18 +775,33 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
}
: null,
otherAttachments: attachments.other,
- preventPress,
+ preventPress: overlayActive ? true : preventPress,
reactions,
readBy,
- setIsEditedMessageOpen,
setQuotedMessage,
showAvatar,
showMessageOverlay,
+ showReactionsOverlay,
showMessageStatus: typeof showMessageStatus === 'boolean' ? showMessageStatus : isMyMessage,
threadList,
videos: attachments.videos,
});
+ const prevActive = useRef(overlayActive);
+
+ useEffect(() => {
+ if (!overlayActive && prevActive.current && setNativeScrollability) {
+ setNativeScrollability(true);
+ }
+ prevActive.current = overlayActive;
+ }, [setNativeScrollability, overlayActive]);
+
+ useEffect(() => {
+ if (!overlayActive) {
+ frozenMessage.current = message;
+ }
+ }, [overlayActive, message]);
+
if (!(isMessageTypeDeleted || messageContentOrder.length)) {
return null;
}
@@ -752,27 +826,87 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
wrapper,
(isTargetedMessage || message.pinned) && !isMessageTypeDeleted
? {
- backgroundColor: targetedMessageBackground,
+ backgroundColor: semantics.backgroundCoreHighlight,
...targetedMessageContainer,
}
: {},
]}
testID='message-wrapper'
>
-
+ {overlayActive && rect ? (
+
+ ) : null}
+ {/*TODO: V9: Find a way to separate these in a dedicated file*/}
+
+ {overlayActive && rect ? (
+ {
+ const { width: w, height: h } = e.nativeEvent.layout;
+
+ setOverlayTopH({
+ h,
+ w,
+ x: isMyMessage ? screenW - rect.x - w : rect.x,
+ y: rect.y - h,
+ });
+ }}
+ >
+
+
+ ) : null}
+
+
+
+
+ {showMessageReactions ? (
+ setShowMessageReactions(false)}
+ visible={showMessageReactions}
+ height={424}
+ >
+
+
+ ) : null}
+
+ {overlayActive && rect ? (
+ {
+ const { width: w, height: h } = e.nativeEvent.layout;
+ setOverlayBottomH({
+ h,
+ w,
+ x: isMyMessage ? screenW - rect.x - w : rect.x,
+ y: rect.y + rect.h,
+ });
+ }}
+ >
+
+
+ ) : null}
+
{isBounceDialogOpen ? (
) : null}
- {messageOverlayVisible ? (
-
- ) : null}
diff --git a/package/src/components/Message/MessageSimple/MessageAvatar.tsx b/package/src/components/Message/MessageSimple/MessageAvatar.tsx
index 0a4f770c46..032e8fa66d 100644
--- a/package/src/components/Message/MessageSimple/MessageAvatar.tsx
+++ b/package/src/components/Message/MessageSimple/MessageAvatar.tsx
@@ -1,29 +1,26 @@
import React from 'react';
import { View } from 'react-native';
-import { ChatContextValue, useChatContext } from '../../../contexts/chatContext/ChatContext';
import {
MessageContextValue,
useMessageContext,
} from '../../../contexts/messageContext/MessageContext';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-
-import { Avatar, AvatarProps } from '../../Avatar/Avatar';
+import { AvatarProps, UserAvatar } from '../../ui';
+import { avatarSizes } from '../../ui/Avatar/constants';
export type MessageAvatarPropsWithContext = Pick<
MessageContextValue,
- 'alignment' | 'lastGroupMessage' | 'message' | 'showAvatar'
+ 'lastGroupMessage' | 'message' | 'showAvatar'
> &
- Pick &
Partial>;
const MessageAvatarWithContext = (props: MessageAvatarPropsWithContext) => {
- const { alignment, ImageComponent, lastGroupMessage, message, showAvatar, size } = props;
+ const { lastGroupMessage, message, showAvatar, size } = props;
const {
theme: {
- avatar: { BASE_AVATAR_SIZE },
messageSimple: {
- avatarWrapper: { container, leftAlign, rightAlign, spacer },
+ avatarWrapper: { container },
},
},
} = useTheme();
@@ -31,19 +28,11 @@ const MessageAvatarWithContext = (props: MessageAvatarPropsWithContext) => {
const visible = typeof showAvatar === 'boolean' ? showAvatar : lastGroupMessage;
return (
-
- {visible ? (
-
+
+ {visible && message.user ? (
+
) : (
-
+
)}
);
@@ -80,14 +69,11 @@ const MemoizedMessageAvatar = React.memo(
export type MessageAvatarProps = Partial;
export const MessageAvatar = (props: MessageAvatarProps) => {
- const { alignment, lastGroupMessage, message, showAvatar } = useMessageContext();
- const { ImageComponent } = useChatContext();
+ const { lastGroupMessage, message, showAvatar } = useMessageContext();
return (
&
Pick<
MessageContentProps,
@@ -30,6 +31,7 @@ export type MessageBubbleProps = Pick<
| 'messageGroupedSingleOrBottom'
| 'noBorder'
| 'setMessageContentWidth'
+ | 'message'
> &
Pick;
@@ -44,15 +46,19 @@ export const MessageBubble = React.memo(
isVeryLastMessage,
messageGroupedSingleOrBottom,
noBorder,
+ MessageError,
+ message,
}: MessageBubbleProps) => {
const {
theme: {
- messageSimple: { contentWrapper },
+ messageSimple: { contentContainer },
},
} = useTheme();
+ const isMessageErrorType =
+ message?.type === 'error' || message?.status === MessageStatusTypes.FAILED;
return (
-
+
) : null}
+ {isMessageErrorType ? (
+
+
+
+ ) : null}
);
},
@@ -210,10 +221,17 @@ const styles = StyleSheet.create({
contentWrapper: {
alignItems: 'center',
flexDirection: 'row',
+ zIndex: 1, // To hide the stick inside the message content
},
+ contentContainer: {},
swipeContentContainer: {
flexShrink: 0,
overflow: 'hidden',
position: 'relative',
},
+ errorContainer: {
+ position: 'absolute',
+ top: 8,
+ right: -12,
+ },
});
diff --git a/package/src/components/Message/MessageSimple/MessageContent.tsx b/package/src/components/Message/MessageSimple/MessageContent.tsx
index aa7efb1af0..861f549264 100644
--- a/package/src/components/Message/MessageSimple/MessageContent.tsx
+++ b/package/src/components/Message/MessageSimple/MessageContent.tsx
@@ -1,8 +1,7 @@
-import React, { useMemo } from 'react';
+import React, { useMemo, useState } from 'react';
import {
AnimatableNumericValue,
ColorValue,
- DimensionValue,
LayoutChangeEvent,
Pressable,
StyleSheet,
@@ -16,6 +15,7 @@ import {
MessageContextValue,
useMessageContext,
} from '../../../contexts/messageContext/MessageContext';
+import { useMessageListItemContext } from '../../../contexts/messageListItemContext/MessageListItemContext';
import {
MessagesContextValue,
useMessagesContext,
@@ -26,52 +26,40 @@ import {
useTranslationContext,
} from '../../../contexts/translationContext/TranslationContext';
-import { useViewport } from '../../../hooks/useViewport';
-
+import { components, primitives } from '../../../theme';
+import { FileTypes } from '../../../types/types';
import { checkMessageEquality, checkQuotedMessageEquality } from '../../../utils/utils';
import { Poll } from '../../Poll/Poll';
import { useMessageData } from '../hooks/useMessageData';
-const styles = StyleSheet.create({
- container: {
- flexShrink: 1,
- },
- containerInner: {
- borderTopLeftRadius: 16,
- borderTopRightRadius: 16,
- borderWidth: 1,
- overflow: 'hidden',
- },
- leftAlignContent: {
- justifyContent: 'flex-start',
- },
- leftAlignItems: {
- alignItems: 'flex-start',
- },
- replyBorder: {
- borderLeftWidth: 1,
- bottom: 0,
- position: 'absolute',
- },
- replyContainer: {
- flexDirection: 'row',
- paddingHorizontal: 8,
- paddingTop: 8,
- },
- rightAlignContent: {
- justifyContent: 'flex-end',
- },
- rightAlignItems: {
- alignItems: 'flex-end',
- },
-});
+const useReplyStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const { isMyMessage } = useMessageContext();
+
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ minWidth: 256, // TODO: Not sure how to fix this
+ backgroundColor: isMyMessage
+ ? semantics.chatBgAttachmentOutgoing
+ : semantics.chatBgAttachmentIncoming,
+ paddingLeft: primitives.spacingSm,
+ },
+ leftContainer: {
+ borderLeftColor: isMyMessage
+ ? semantics.chatReplyIndicatorOutgoing
+ : semantics.chatReplyIndicatorIncoming,
+ },
+ });
+ }, [semantics, isMyMessage]);
+};
export type MessageContentPropsWithContext = Pick<
MessageContextValue,
- | 'alignment'
| 'goToMessage'
| 'groupStyles'
- | 'isEditedMessageOpen'
| 'isMyMessage'
| 'message'
| 'messageContentOrder'
@@ -91,7 +79,6 @@ export type MessageContentPropsWithContext = Pick<
| 'FileAttachmentGroup'
| 'Gallery'
| 'isAttachmentEqual'
- | 'MessageError'
| 'MessageLocation'
| 'myMessageTheme'
| 'Reply'
@@ -115,15 +102,24 @@ export type MessageContentPropsWithContext = Pick<
* If the message is grouped in a single or bottom container
*/
messageGroupedSingleOrBottom?: boolean;
+
+ /**
+ * If the message has a single file
+ */
+ isSingleFile?: boolean;
+ hidePaddingTop?: boolean;
+ hidePaddingHorizontal?: boolean;
+ hidePaddingBottom?: boolean;
+ isMessageReceivedOrErrorType?: boolean;
};
/**
* Child of MessageSimple that displays a message's content
*/
const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
+ const [longPressFired, setLongPressFired] = useState(false);
const {
additionalPressableProps,
- alignment,
Attachment,
backgroundColor,
enableMessageGroupingByUser,
@@ -132,11 +128,11 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
groupStyles,
isMessageAIGenerated,
isMyMessage,
+ isMessageReceivedOrErrorType,
isVeryLastMessage,
message,
messageContentOrder,
messageGroupedSingleOrBottom = false,
- MessageError,
MessageLocation,
noBorder,
onLongPress,
@@ -147,38 +143,39 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
Reply,
setMessageContentWidth,
StreamingMessageView,
- threadList,
+ hidePaddingTop,
+ hidePaddingHorizontal,
+ hidePaddingBottom,
} = props;
const { client } = useChatContext();
const { PollContent: PollContentOverride } = useMessagesContext();
+ const replyStyles = useReplyStyles();
const {
theme: {
- colors: { grey_whisper, light_gray },
+ colors: { grey_whisper },
messageSimple: {
content: {
container: {
borderBottomLeftRadius,
borderBottomRightRadius,
borderRadius,
- borderRadiusL,
- borderRadiusS,
borderTopLeftRadius,
borderTopRightRadius,
...container
},
containerInner,
+ contentContainer,
lastMessageContainer,
messageGroupedSingleOrBottomContainer,
messageGroupedTopContainer,
- replyBorder,
replyContainer,
+ textWrapper,
wrapper,
},
},
},
} = useTheme();
- const { vw } = useViewport();
const onLayout: (event: LayoutChangeEvent) => void = ({
nativeEvent: {
@@ -193,24 +190,20 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
[message, isMessageAIGenerated],
);
- const { hasThreadReplies, isMessageErrorType, isMessageReceivedOrErrorType } = useMessageData({});
-
- const repliesCurveColor = !isMessageReceivedOrErrorType ? backgroundColor : light_gray;
-
const getBorderRadius = () => {
// enum('top', 'middle', 'bottom', 'single')
const groupPosition = groupStyles?.[0];
const isBottomOrSingle = groupPosition === 'single' || groupPosition === 'bottom';
- let borderBottomLeftRadius = borderRadiusL;
- let borderBottomRightRadius = borderRadiusL;
+ let borderBottomLeftRadius = components.messageBubbleRadiusGroupBottom;
+ let borderBottomRightRadius = components.messageBubbleRadiusGroupBottom;
- if (isBottomOrSingle && (!hasThreadReplies || threadList)) {
+ if (isBottomOrSingle) {
// add relevant sharp corner
if (isMyMessage) {
- borderBottomRightRadius = borderRadiusS;
+ borderBottomRightRadius = components.messageBubbleRadiusTail;
} else {
- borderBottomLeftRadius = borderRadiusS;
+ borderBottomLeftRadius = components.messageBubbleRadiusTail;
}
}
@@ -239,10 +232,13 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
return bordersFromTheme;
};
+ const { setNativeScrollability } = useMessageListItemContext();
+
return (
{
+ setLongPressFired(true);
if (onLongPress) {
onLongPress({
emitter: 'messageContent',
@@ -266,24 +262,18 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
});
}
}}
- style={({ pressed }) => [{ opacity: pressed ? 0.5 : 1 }, container]}
+ style={({ pressed }) => [{ opacity: pressed && !longPressFired ? 0.5 : 1 }, container]}
{...additionalPressableProps}
+ onPressOut={(event) => {
+ setLongPressFired(false);
+ setNativeScrollability(true);
+
+ if (additionalPressableProps?.onPressOut) {
+ additionalPressableProps.onPressOut(event);
+ }
+ }}
>
- {hasThreadReplies && !threadList && !noBorder && (
-
- )}
{
]}
testID='message-content-wrapper'
>
- {messageContentOrder.map((messageContentType, messageContentOrderIndex) => {
- switch (messageContentType) {
- case 'quoted_reply':
- return (
- message.quoted_message && (
+
+ {messageContentOrder.map((messageContentType, messageContentOrderIndex) => {
+ switch (messageContentType) {
+ case 'quoted_reply':
+ return (
+ message.quoted_message && (
+
+
+
+ )
+ );
+ case 'attachments':
+ return otherAttachments.map((attachment, attachmentIndex) => (
+
+ ));
+ case 'files':
+ return (
+
+ );
+ case 'gallery':
+ return (
-
+
- )
- );
- case 'attachments':
- return otherAttachments.map((attachment, attachmentIndex) => (
-
- ));
- case 'files':
- return (
-
- );
- case 'gallery':
- return ;
- case 'poll': {
- const pollId = message.poll_id;
- const poll = pollId && client.polls.fromState(pollId);
- return pollId && poll ? (
-
- ) : null;
+ );
+ case 'poll': {
+ const pollId = message.poll_id;
+ const poll = pollId && client.polls.fromState(pollId);
+ return pollId && poll ? (
+
+ ) : null;
+ }
+ case 'location':
+ return MessageLocation ? (
+
+ ) : null;
+ case 'ai_text':
+ return isAIGenerated ? (
+
+ ) : null;
+ default:
+ return null;
}
- case 'location':
- return MessageLocation ? (
-
- ) : null;
- case 'ai_text':
- return isAIGenerated ? (
-
- ) : null;
- case 'text':
- default:
- return (otherAttachments.length && otherAttachments[0].actions) ||
- isAIGenerated ? null : (
-
- );
- }
- })}
+ })}
+
+
+ {(otherAttachments.length && otherAttachments[0].actions) || isAIGenerated ? null : (
+
+ )}
+
- {isMessageErrorType && }
);
@@ -376,10 +382,10 @@ const areEqual = (
nextProps: MessageContentPropsWithContext,
) => {
const {
+ preventPress: prevPreventPress,
goToMessage: prevGoToMessage,
groupStyles: prevGroupStyles,
isAttachmentEqual,
- isEditedMessageOpen: prevIsEditedMessageOpen,
message: prevMessage,
messageContentOrder: prevMessageContentOrder,
myMessageTheme: prevMyMessageTheme,
@@ -387,9 +393,9 @@ const areEqual = (
t: prevT,
} = prevProps;
const {
+ preventPress: nextPreventPress,
goToMessage: nextGoToMessage,
groupStyles: nextGroupStyles,
- isEditedMessageOpen: nextIsEditedMessageOpen,
message: nextMessage,
messageContentOrder: nextMessageContentOrder,
myMessageTheme: nextMyMessageTheme,
@@ -397,14 +403,13 @@ const areEqual = (
t: nextT,
} = nextProps;
- const goToMessageChangedAndMatters =
- nextMessage.quoted_message_id && prevGoToMessage !== nextGoToMessage;
- if (goToMessageChangedAndMatters) {
+ if (prevPreventPress !== nextPreventPress) {
return false;
}
- const isEditedMessageOpenEqual = prevIsEditedMessageOpen === nextIsEditedMessageOpen;
- if (!isEditedMessageOpenEqual) {
+ const goToMessageChangedAndMatters =
+ nextMessage.quoted_message_id && prevGoToMessage !== nextGoToMessage;
+ if (goToMessageChangedAndMatters) {
return false;
}
@@ -529,10 +534,8 @@ export type MessageContentProps = Partial<
*/
export const MessageContent = (props: MessageContentProps) => {
const {
- alignment,
goToMessage,
groupStyles,
- isEditedMessageOpen,
isMessageAIGenerated,
isMyMessage,
message,
@@ -543,6 +546,8 @@ export const MessageContent = (props: MessageContentProps) => {
otherAttachments,
preventPress,
threadList,
+ files,
+ images,
} = useMessageContext();
const {
additionalPressableProps,
@@ -551,32 +556,63 @@ export const MessageContent = (props: MessageContentProps) => {
FileAttachmentGroup,
Gallery,
isAttachmentEqual,
- MessageError,
MessageLocation,
myMessageTheme,
Reply,
StreamingMessageView,
} = useMessagesContext();
const { t } = useTranslationContext();
+ const isSingleFile = files.length === 1;
+ const messageHasPoll = messageContentOrder.includes('poll');
+ const messageHasSingleImage =
+ messageContentOrder.length === 1 &&
+ messageContentOrder.includes('gallery') &&
+ images.length === 1;
+ const messageHasSingleFile =
+ messageContentOrder.length === 1 && messageContentOrder[0] === 'files' && isSingleFile;
+ const messageHasOnlyText = messageContentOrder.length === 1 && messageContentOrder[0] === 'text';
+ const messageHasGiphyOrImgur =
+ otherAttachments.filter(
+ (file) => file.type === FileTypes.Giphy || file.type === FileTypes.Imgur,
+ ).length > 0;
+
+ const hidePaddingTop =
+ messageHasPoll ||
+ messageHasSingleImage ||
+ messageHasSingleFile ||
+ messageHasOnlyText ||
+ messageHasGiphyOrImgur;
+
+ const hidePaddingHorizontal =
+ messageHasPoll || messageHasSingleImage || messageHasSingleFile || messageHasGiphyOrImgur;
+
+ const hidePaddingBottom =
+ messageHasPoll ||
+ messageHasSingleImage ||
+ messageHasSingleFile ||
+ messageHasOnlyText ||
+ messageHasGiphyOrImgur ||
+ (messageContentOrder.length > 1 &&
+ messageContentOrder[messageContentOrder.length - 1] === 'text');
+
+ const { isMessageReceivedOrErrorType } = useMessageData({});
return (
{
StreamingMessageView,
t,
threadList,
+ hidePaddingTop,
+ hidePaddingHorizontal,
+ hidePaddingBottom,
}}
{...props}
/>
);
};
+
+const styles = StyleSheet.create({
+ container: {
+ flexShrink: 1,
+ },
+ containerInner: {
+ borderTopLeftRadius: components.messageBubbleRadiusGroupBottom,
+ borderTopRightRadius: components.messageBubbleRadiusGroupBottom,
+ borderWidth: 1,
+ overflow: 'hidden',
+ },
+ leftAlignContent: {
+ justifyContent: 'flex-start',
+ },
+ leftAlignItems: {
+ alignItems: 'flex-start',
+ },
+ replyBorder: {
+ borderLeftWidth: 1,
+ bottom: 0,
+ position: 'absolute',
+ },
+ replyContainer: {
+ alignSelf: 'center',
+ },
+ galleryContainer: {},
+ rightAlignContent: {
+ justifyContent: 'flex-end',
+ },
+ rightAlignItems: {
+ alignItems: 'flex-end',
+ },
+ textWrapper: {
+ paddingHorizontal: primitives.spacingSm,
+ },
+});
diff --git a/package/src/components/Message/MessageSimple/MessageDeleted.tsx b/package/src/components/Message/MessageSimple/MessageDeleted.tsx
index e8f99eb3fd..b202c15339 100644
--- a/package/src/components/Message/MessageSimple/MessageDeleted.tsx
+++ b/package/src/components/Message/MessageSimple/MessageDeleted.tsx
@@ -1,9 +1,5 @@
-import React from 'react';
-import { LayoutChangeEvent, StyleSheet, View } from 'react-native';
-
-import merge from 'lodash/merge';
-
-import { MessageTextContainer } from './MessageTextContainer';
+import React, { useMemo } from 'react';
+import { StyleSheet, Text, View } from 'react-native';
import {
MessageContextValue,
@@ -15,26 +11,11 @@ import {
} from '../../../contexts/messagesContext/MessagesContext';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
-
-const styles = StyleSheet.create({
- containerInner: {
- borderTopLeftRadius: 16,
- borderTopRightRadius: 16,
- borderWidth: 1,
- overflow: 'hidden',
- },
- leftAlignItems: {
- alignItems: 'flex-start',
- },
- rightAlignItems: {
- alignItems: 'flex-end',
- },
-});
+import { CircleBan } from '../../../icons/CircleBan';
+import { components, primitives } from '../../../theme';
type MessageDeletedComponentProps = {
groupStyle: string;
- noBorder: boolean;
- onLayout: (event: LayoutChangeEvent) => void;
date?: string | Date;
};
@@ -43,55 +24,42 @@ type MessageDeletedPropsWithContext = Pick {
- const { alignment, date, groupStyle, message, MessageFooter, noBorder, onLayout } = props;
+ const { alignment, date, groupStyle, MessageFooter } = props;
const {
theme: {
- colors: { grey, grey_whisper },
messageSimple: {
- content: {
- container: { borderRadiusL, borderRadiusS },
- deletedContainer,
- deletedContainerInner,
- deletedText,
- },
+ deleted: { containerInner, deletedText, container },
},
+ semantics,
},
} = useTheme();
const { t } = useTranslationContext();
+ const styles = useStyles();
return (
-
+
+ {t('Message deleted')}
@@ -138,8 +106,6 @@ const MemoizedMessageDeleted = React.memo(
export type MessageDeletedProps = Partial & {
groupStyle: string;
- noBorder: boolean;
- onLayout: (event: LayoutChangeEvent) => void;
};
export const MessageDeleted = (props: MessageDeletedProps) => {
@@ -160,3 +126,46 @@ export const MessageDeleted = (props: MessageDeletedProps) => {
};
MessageDeleted.displayName = 'MessageDeleted{messageSimple{content}}';
+
+const useStyles = () => {
+ const {
+ theme: {
+ semantics,
+ messageSimple: {
+ deleted: { containerInner, deletedText, container },
+ },
+ },
+ } = useTheme();
+ const { isMyMessage } = useMessageContext();
+
+ return useMemo(() => {
+ return StyleSheet.create({
+ containerInner: {
+ borderTopLeftRadius: components.messageBubbleRadiusGroupBottom,
+ borderTopRightRadius: components.messageBubbleRadiusGroupBottom,
+ paddingHorizontal: primitives.spacingSm,
+ paddingVertical: primitives.spacingXs,
+ backgroundColor: isMyMessage ? semantics.chatBgOutgoing : semantics.chatBgIncoming,
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: primitives.spacingXxs,
+ ...containerInner,
+ },
+ deletedText: {
+ color: isMyMessage ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
+ fontSize: primitives.typographyFontSizeMd,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightNormal,
+ ...deletedText,
+ },
+ leftAlignItems: {
+ alignItems: 'flex-start',
+ ...container,
+ },
+ rightAlignItems: {
+ alignItems: 'flex-end',
+ ...container,
+ },
+ });
+ }, [isMyMessage, semantics, containerInner, deletedText, container]);
+};
diff --git a/package/src/components/Message/MessageSimple/MessageEditedTimestamp.tsx b/package/src/components/Message/MessageSimple/MessageEditedTimestamp.tsx
deleted file mode 100644
index b46142d389..0000000000
--- a/package/src/components/Message/MessageSimple/MessageEditedTimestamp.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from 'react';
-import { StyleSheet, Text, View } from 'react-native';
-
-import {
- MessageContextValue,
- useMessageContext,
-} from '../../../contexts/messageContext/MessageContext';
-import { MessagesContextValue } from '../../../contexts/messagesContext/MessagesContext';
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
-import { isEditedMessage } from '../../../utils/utils';
-
-export type MessageEditedTimestampProps = Partial> &
- Partial>;
-
-export const MessageEditedTimestamp = (props: MessageEditedTimestampProps) => {
- const { message: propMessage, MessageTimestamp } = props;
- const {
- theme: {
- colors: { grey },
- messageSimple: {
- content: { editedLabel, editedTimestampContainer },
- },
- },
- } = useTheme();
- const { t } = useTranslationContext();
- const { message: contextMessage } = useMessageContext();
- const message = propMessage || contextMessage;
-
- if (!isEditedMessage(message)) {
- return null;
- }
-
- return (
-
- {t('Edited') + ' '}
- {MessageTimestamp && (
-
- )}
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- },
- text: {
- fontSize: 12,
- },
-});
diff --git a/package/src/components/Message/MessageSimple/MessageError.tsx b/package/src/components/Message/MessageSimple/MessageError.tsx
index ccc859e9b7..b7a303fa19 100644
--- a/package/src/components/Message/MessageSimple/MessageError.tsx
+++ b/package/src/components/Message/MessageSimple/MessageError.tsx
@@ -1,28 +1,7 @@
import React from 'react';
-import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { Error } from '../../../icons';
+import { ErrorBadge } from '../../ui/Badge/ErrorBadge';
-export type MessageErrorProps = {
- style?: StyleProp;
-};
-
-export const MessageError = ({ style }: MessageErrorProps) => {
- const {
- theme: {
- colors: { accent_red },
- messageSimple: {
- content: { errorIcon, errorIconContainer },
- },
- },
- } = useTheme();
-
- return (
-
-
-
-
-
- );
+export const MessageError = () => {
+ return ;
};
diff --git a/package/src/components/Message/MessageSimple/MessageFooter.tsx b/package/src/components/Message/MessageSimple/MessageFooter.tsx
index 1c8c39d375..e103337c14 100644
--- a/package/src/components/Message/MessageSimple/MessageFooter.tsx
+++ b/package/src/components/Message/MessageSimple/MessageFooter.tsx
@@ -20,6 +20,7 @@ import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
import { Eye } from '../../../icons';
+import { primitives } from '../../../theme';
import { isEditedMessage, MessageStatusTypes } from '../../../utils/utils';
type MessageFooterComponentProps = {
@@ -31,7 +32,6 @@ type MessageFooterComponentProps = {
type MessageFooterPropsWithContext = Pick<
MessageContextValue,
| 'alignment'
- | 'isEditedMessageOpen'
| 'members'
| 'message'
| 'otherAttachments'
@@ -41,10 +41,7 @@ type MessageFooterPropsWithContext = Pick<
> &
Pick<
MessagesContextValue,
- | 'deletedMessagesVisibilityType'
- | 'MessageEditedTimestamp'
- | 'MessageStatus'
- | 'MessageTimestamp'
+ 'deletedMessagesVisibilityType' | 'MessageStatus' | 'MessageTimestamp'
> &
MessageFooterComponentProps;
@@ -53,7 +50,7 @@ const OnlyVisibleToYouComponent = ({ alignment }: { alignment: Alignment }) => {
theme: {
colors: { grey_dark },
messageSimple: {
- content: { deletedMetaText, eyeIcon, metaText },
+ content: { eyeIcon, metaText },
},
},
} = useTheme();
@@ -69,7 +66,6 @@ const OnlyVisibleToYouComponent = ({ alignment }: { alignment: Alignment }) => {
textAlign: alignment,
},
metaText,
- deletedMetaText,
]}
testID='only-visible-to-you'
>
@@ -86,23 +82,21 @@ const MessageFooterWithContext = (props: MessageFooterPropsWithContext) => {
deletedMessagesVisibilityType,
formattedDate,
isDeleted,
- isEditedMessageOpen,
isMessageAIGenerated,
lastGroupMessage,
members,
message,
- MessageEditedTimestamp,
MessageStatus,
MessageTimestamp,
otherAttachments,
showMessageStatus,
} = props;
+ const styles = useStyles();
const {
theme: {
- colors: { grey },
messageSimple: {
- content: { editedLabel, messageUser, metaContainer, metaText },
+ footer: { container, name, editedText },
},
},
} = useTheme();
@@ -115,7 +109,7 @@ const MessageFooterWithContext = (props: MessageFooterPropsWithContext) => {
if (isDeleted) {
return (
-
+
{deletedMessagesVisibilityType === 'sender' && (
)}
@@ -131,41 +125,16 @@ const MessageFooterWithContext = (props: MessageFooterPropsWithContext) => {
const isEdited = isEditedMessage(message) && !isAIGenerated;
return (
- <>
-
- {otherAttachments.length && otherAttachments[0].actions ? (
-
- ) : null}
- {Object.keys(members).length > 2 && alignment === 'left' && message.user?.name ? (
- {message.user.name}
- ) : null}
- {showMessageStatus && }
-
-
- {isEdited && !isEditedMessageOpen ? (
- <>
-
- ⦁
-
-
- {t('Edited')}
-
- >
- ) : null}
-
- {isEdited && isEditedMessageOpen ? (
-
+
+ {otherAttachments.length && otherAttachments[0].actions ? (
+
) : null}
- >
+ {Object.keys(members).length > 2 && alignment === 'left' && message.user?.name ? (
+ {message.user.name}
+ ) : null}
+ {showMessageStatus && }
+ {isEdited ? {t('Edited')} : null}
+
);
};
@@ -177,7 +146,6 @@ const areEqual = (
alignment: prevAlignment,
date: prevDate,
formattedDate: prevFormattedDate,
- isEditedMessageOpen: prevIsEditedMessageOpen,
lastGroupMessage: prevLastGroupMessage,
members: prevMembers,
message: prevMessage,
@@ -188,7 +156,6 @@ const areEqual = (
alignment: nextAlignment,
date: nextDate,
formattedDate: nextFormattedDate,
- isEditedMessageOpen: nextIsEditedMessageOpen,
lastGroupMessage: nextLastGroupMessage,
members: nextMembers,
message: nextMessage,
@@ -201,11 +168,6 @@ const areEqual = (
return false;
}
- const isEditedMessageOpenEqual = prevIsEditedMessageOpen === nextIsEditedMessageOpen;
- if (!isEditedMessageOpenEqual) {
- return false;
- }
-
const membersEqual = Object.keys(prevMembers).length === Object.keys(nextMembers).length;
if (!membersEqual) {
return false;
@@ -279,7 +241,6 @@ export type MessageFooterProps = Partial> &
export const MessageFooter = (props: MessageFooterProps) => {
const {
alignment,
- isEditedMessageOpen,
isMessageAIGenerated,
lastGroupMessage,
members,
@@ -288,20 +249,17 @@ export const MessageFooter = (props: MessageFooterProps) => {
showMessageStatus,
} = useMessageContext();
- const { deletedMessagesVisibilityType, MessageEditedTimestamp, MessageStatus, MessageTimestamp } =
- useMessagesContext();
+ const { deletedMessagesVisibilityType, MessageStatus, MessageTimestamp } = useMessagesContext();
return (
{
);
};
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- flexDirection: 'row',
- justifyContent: 'center',
- marginTop: 4,
- },
- dotText: {
- paddingHorizontal: 4,
- },
- text: {
- fontSize: 12,
- },
-});
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ paddingVertical: primitives.spacingXxs,
+ gap: primitives.spacingXs,
+ },
+ name: {
+ color: semantics.chatTextUsername,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ editedText: {
+ color: semantics.chatTextTimestamp,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ });
+ }, [semantics]);
+};
diff --git a/package/src/components/Message/MessageSimple/MessageHeader.tsx b/package/src/components/Message/MessageSimple/MessageHeader.tsx
new file mode 100644
index 0000000000..a76f6e2162
--- /dev/null
+++ b/package/src/components/Message/MessageSimple/MessageHeader.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+
+import {
+ MessageContextValue,
+ useMessageContext,
+} from '../../../contexts/messageContext/MessageContext';
+import {
+ MessagesContextValue,
+ useMessagesContext,
+} from '../../../contexts/messagesContext/MessagesContext';
+
+type MessageHeaderPropsWithContext = Pick &
+ Pick;
+
+const MessageHeaderWithContext = (props: MessageHeaderPropsWithContext) => {
+ const { message, MessagePinnedHeader } = props;
+
+ return ;
+};
+
+const areEqual = (
+ prevProps: MessageHeaderPropsWithContext,
+ nextProps: MessageHeaderPropsWithContext,
+) => {
+ const { message: prevMessage } = prevProps;
+ const { message: nextMessage } = nextProps;
+
+ const messageEqual =
+ prevMessage.id === nextMessage.id && prevMessage.pinned === nextMessage.pinned;
+ if (!messageEqual) {
+ return false;
+ }
+
+ return true;
+};
+
+const MemoizedMessageHeader = React.memo(
+ MessageHeaderWithContext,
+ areEqual,
+) as typeof MessageHeaderWithContext;
+
+export type MessageHeaderProps = Partial>;
+
+export const MessageHeader = (props: MessageHeaderProps) => {
+ const { message } = useMessageContext();
+ const { MessagePinnedHeader } = useMessagesContext();
+
+ return (
+
+ );
+};
diff --git a/package/src/components/Message/MessageSimple/MessagePinnedHeader.tsx b/package/src/components/Message/MessageSimple/MessagePinnedHeader.tsx
index 4b7e5d8521..e91ddd78d8 100644
--- a/package/src/components/Message/MessageSimple/MessagePinnedHeader.tsx
+++ b/package/src/components/Message/MessageSimple/MessagePinnedHeader.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
@@ -8,7 +8,8 @@ import {
} from '../../../contexts/messageContext/MessageContext';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
-import { PinHeader } from '../../../icons';
+import { NewPin } from '../../../icons/NewPin';
+import { primitives } from '../../../theme';
export type MessagePinnedHeaderProps = Partial>;
@@ -17,22 +18,20 @@ export const MessagePinnedHeader = (props: MessagePinnedHeaderProps) => {
const { message: contextMessage } = useMessageContext();
const message = propMessage || contextMessage;
const {
- theme: {
- colors: { grey },
- messageSimple: { pinnedHeader },
- },
+ theme: { semantics },
} = useTheme();
- const { container, label } = pinnedHeader;
+ const styles = useStyles();
const { t } = useTranslationContext();
const { client } = useChatContext();
+
+ if (!message?.pinned) {
+ return null;
+ }
+
return (
-
-
-
+
+
+
{t('Pinned by')}{' '}
{message?.pinned_by?.id === client?.user?.id ? t('You') : message?.pinned_by?.name}
@@ -40,14 +39,32 @@ export const MessagePinnedHeader = (props: MessagePinnedHeaderProps) => {
);
};
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- flexDirection: 'row',
- justifyContent: 'center',
- },
- label: {
- fontSize: 12,
- marginLeft: 4,
- },
-});
+const useStyles = () => {
+ const {
+ theme: {
+ semantics,
+ messageSimple: {
+ pinnedHeader: { container, label },
+ },
+ },
+ } = useTheme();
+
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: primitives.spacingXxs,
+ paddingVertical: primitives.spacingXxs,
+ ...container,
+ },
+ label: {
+ color: semantics.textPrimary,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightTight,
+ ...label,
+ },
+ });
+ }, [semantics, container, label]);
+};
diff --git a/package/src/components/Message/MessageSimple/MessageReplies.tsx b/package/src/components/Message/MessageSimple/MessageReplies.tsx
index 12bc59e4e8..c0750fa484 100644
--- a/package/src/components/Message/MessageSimple/MessageReplies.tsx
+++ b/package/src/components/Message/MessageSimple/MessageReplies.tsx
@@ -1,5 +1,5 @@
-import React from 'react';
-import { ColorValue, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+import React, { useMemo } from 'react';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
import {
MessageContextValue,
@@ -14,37 +14,9 @@ import {
TranslationContextValue,
useTranslationContext,
} from '../../../contexts/translationContext/TranslationContext';
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- flexDirection: 'row',
- marginTop: 8,
- },
- curveContainer: {
- flexDirection: 'row',
- },
- leftMessageRepliesCurve: {
- borderBottomLeftRadius: 16,
- borderRightWidth: 0,
- },
- messageRepliesCurve: {
- borderTopWidth: 0,
- borderWidth: 2,
- height: 16,
- width: 16,
- },
- messageRepliesText: {
- fontSize: 12,
- fontWeight: '700',
- paddingBottom: 5,
- paddingHorizontal: 8,
- },
- rightMessageRepliesCurve: {
- borderBottomRightRadius: 16,
- borderLeftWidth: 0,
- },
-});
+import { ReplyConnectorLeft } from '../../../icons/ReplyConnectorLeft';
+import { ReplyConnectorRight } from '../../../icons/ReplyConnectorRight';
+import { primitives } from '../../../theme';
export type MessageRepliesPropsWithContext = Pick<
MessageContextValue,
@@ -58,58 +30,42 @@ export type MessageRepliesPropsWithContext = Pick<
| 'threadList'
> &
Pick &
- Pick & {
- noBorder?: boolean;
- repliesCurveColor?: ColorValue;
- };
+ Pick;
const MessageRepliesWithContext = (props: MessageRepliesPropsWithContext) => {
const {
alignment,
message,
MessageRepliesAvatars,
- noBorder,
onLongPress,
onOpenThread,
onPress,
onPressIn,
preventPress,
- repliesCurveColor,
t,
threadList,
} = props;
const {
theme: {
- colors: { accent_blue },
messageSimple: {
- replies: { container, leftCurve, messageRepliesText, rightCurve },
+ replies: { container, messageRepliesText, content },
},
+ semantics,
},
} = useTheme();
+ const styles = useStyles();
if (threadList || !message.reply_count) {
return null;
}
return (
-
+
{alignment === 'left' && (
-
- {!noBorder && (
-
- )}
-
-
+
)}
- {
if (onLongPress) {
@@ -137,31 +93,28 @@ const MessageRepliesWithContext = (props: MessageRepliesPropsWithContext) => {
});
}
}}
- style={[styles.container, container]}
+ style={[
+ styles.content,
+ { flexDirection: alignment === 'left' ? 'row' : 'row-reverse' },
+ content,
+ ]}
testID='message-replies'
>
-
+
+
{message.reply_count === 1
- ? t('1 Thread Reply')
- : t('{{ replyCount }} Thread Replies', {
+ ? t('1 Reply')
+ : t('{{ replyCount }} Replies', {
replyCount: message.reply_count,
})}
-
+
{alignment === 'right' && (
-
-
- {!noBorder && (
-
- )}
-
+
)}
);
@@ -173,17 +126,13 @@ const areEqual = (
) => {
const {
message: prevMessage,
- noBorder: prevNoBorder,
onOpenThread: prevOnOpenThread,
- repliesCurveColor: prevRepliesCurveColor,
t: prevT,
threadList: prevThreadList,
} = prevProps;
const {
message: nextMessage,
- noBorder: nextNoBorder,
onOpenThread: nextOnOpenThread,
- repliesCurveColor: nextRepliesCurveColor,
t: nextT,
threadList: nextThreadList,
} = nextProps;
@@ -198,16 +147,6 @@ const areEqual = (
return false;
}
- const noBorderEqual = prevNoBorder === nextNoBorder;
- if (!noBorderEqual) {
- return false;
- }
-
- const repliesCurveColorEqual = prevRepliesCurveColor === nextRepliesCurveColor;
- if (!repliesCurveColorEqual) {
- return false;
- }
-
const tEqual = prevT === nextT;
if (!tEqual) {
return false;
@@ -262,3 +201,33 @@ export const MessageReplies = (props: MessageRepliesProps) => {
};
MessageReplies.displayName = 'MessageReplies{messageSimple{replies}}';
+
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: primitives.spacingXs,
+ marginTop: -primitives.spacingMd, // Pulling the replies container up to hide the stick in the message content
+ },
+ content: {
+ alignSelf: 'flex-end',
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: primitives.spacingXs,
+ paddingTop: primitives.spacingXs,
+ paddingBottom: primitives.spacingXxs,
+ },
+ messageRepliesText: {
+ color: semantics.textPrimary,
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ });
+ }, [semantics]);
+};
diff --git a/package/src/components/Message/MessageSimple/MessageRepliesAvatars.tsx b/package/src/components/Message/MessageSimple/MessageRepliesAvatars.tsx
index 715ad80b72..dedb86e502 100644
--- a/package/src/components/Message/MessageSimple/MessageRepliesAvatars.tsx
+++ b/package/src/components/Message/MessageSimple/MessageRepliesAvatars.tsx
@@ -1,90 +1,36 @@
import React from 'react';
-import { StyleSheet, View } from 'react-native';
+import { View } from 'react-native';
-import { ChatContextValue, useChatContext } from '../../../contexts/chatContext/ChatContext';
-import type { MessageContextValue } from '../../../contexts/messageContext/MessageContext';
+import {
+ useMessageContext,
+ type MessageContextValue,
+} from '../../../contexts/messageContext/MessageContext';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
+import { UserAvatarStack } from '../../ui/Avatar/AvatarStack';
-import { Avatar } from '../../Avatar/Avatar';
+export type MessageRepliesAvatarsProps = Partial>;
-const styles = StyleSheet.create({
- avatarContainer: {
- alignItems: 'center',
- flexDirection: 'row',
- paddingTop: 2,
- },
- topAvatar: {
- paddingTop: 2,
- position: 'absolute',
- },
-});
-
-export type MessageRepliesAvatarsProps = Pick;
-
-export const MessageRepliesAvatarsWithContext = (
- props: MessageRepliesAvatarsProps & Pick,
-) => {
- const { alignment, ImageComponent, message } = props;
+export const MessageRepliesAvatarsWithContext = (props: MessageRepliesAvatarsProps) => {
+ const { message } = props;
const {
theme: {
- colors: { white_snow },
messageSimple: {
- replies: {
- avatar,
- avatarContainerMultiple,
- avatarContainerSingle,
- avatarSize,
- leftAvatarsContainer,
- rightAvatarsContainer,
- },
+ replies: { avatarStackContainer },
},
},
} = useTheme();
- const avatars = message.thread_participants?.slice(-2) || [];
- const hasMoreThanOneReply = avatars.length > 1;
+ const avatars = message?.thread_participants || [];
return (
-
- {avatars.map((user, i) => (
-
-
-
- ))}
+
+
);
};
export const MessageRepliesAvatars = (props: MessageRepliesAvatarsProps) => {
- const { ImageComponent } = useChatContext();
-
- return ;
+ const { message } = useMessageContext();
+ return ;
};
diff --git a/package/src/components/Message/MessageSimple/MessageSimple.tsx b/package/src/components/Message/MessageSimple/MessageSimple.tsx
index 472d4c47be..e70ddb6089 100644
--- a/package/src/components/Message/MessageSimple/MessageSimple.tsx
+++ b/package/src/components/Message/MessageSimple/MessageSimple.tsx
@@ -1,5 +1,5 @@
-import React, { useMemo, useState } from 'react';
-import { Dimensions, LayoutChangeEvent, StyleSheet, View } from 'react-native';
+import React, { forwardRef, useMemo, useState } from 'react';
+import { Dimensions, StyleSheet, View } from 'react-native';
import { MessageBubble, SwipableMessageBubble } from './MessageBubble';
@@ -15,6 +15,7 @@ import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { useStableCallback } from '../../../hooks/useStableCallback';
+import { primitives } from '../../../theme';
import { checkMessageEquality, checkQuotedMessageEquality } from '../../../utils/utils';
import { useMessageData } from '../hooks/useMessageData';
@@ -22,17 +23,21 @@ const styles = StyleSheet.create({
container: {
alignItems: 'flex-end',
flexDirection: 'row',
+ gap: primitives.spacingXs,
+ },
+ contentContainer: {
+ gap: primitives.spacingXxs,
+ },
+ repliesContainer: {
+ marginTop: -primitives.spacingXxs, // Reducing the margin to account the gap added in the content container
},
- contentContainer: {},
lastMessageContainer: {
marginBottom: 12,
},
leftAlignItems: {
alignItems: 'flex-start',
},
- messageGroupedSingleOrBottomContainer: {
- marginBottom: 8,
- },
+ messageGroupedSingleOrBottomContainer: {},
messageGroupedTopContainer: {},
rightAlignItems: {
alignItems: 'flex-end',
@@ -46,13 +51,12 @@ export type MessageSimplePropsWithContext = Pick<
| 'groupStyles'
| 'hasReactions'
| 'isMyMessage'
- | 'lastGroupMessage'
- | 'members'
| 'message'
| 'onlyEmojis'
| 'otherAttachments'
- | 'showMessageStatus'
| 'setQuotedMessage'
+ | 'lastGroupMessage'
+ | 'members'
> &
Pick<
MessagesContextValue,
@@ -63,11 +67,10 @@ export type MessageSimplePropsWithContext = Pick<
| 'MessageAvatar'
| 'MessageContent'
| 'MessageDeleted'
+ | 'MessageError'
| 'MessageFooter'
| 'MessageHeader'
- | 'MessagePinnedHeader'
| 'MessageReplies'
- | 'MessageStatus'
| 'MessageSwipeContent'
| 'messageSwipeToReplyHitSlop'
| 'ReactionListBottom'
@@ -84,7 +87,7 @@ export type MessageSimplePropsWithContext = Pick<
shouldRenderSwipeableWrapper: boolean;
};
-const MessageSimpleWithContext = (props: MessageSimplePropsWithContext) => {
+const MessageSimpleWithContext = forwardRef((props, ref) => {
const [messageContentWidth, setMessageContentWidth] = useState(0);
const { width } = Dimensions.get('screen');
const {
@@ -96,17 +99,14 @@ const MessageSimpleWithContext = (props: MessageSimplePropsWithContext) => {
groupStyles,
hasReactions,
isMyMessage,
- lastGroupMessage,
- members,
message,
MessageAvatar,
MessageContent,
MessageDeleted,
+ MessageError,
MessageFooter,
MessageHeader,
- MessagePinnedHeader,
MessageReplies,
- MessageStatus,
MessageSwipeContent,
messageSwipeToReplyHitSlop = { left: width, right: width },
onlyEmojis,
@@ -114,7 +114,6 @@ const MessageSimpleWithContext = (props: MessageSimplePropsWithContext) => {
ReactionListBottom,
reactionListPosition,
ReactionListTop,
- showMessageStatus,
shouldRenderSwipeableWrapper,
setQuotedMessage,
} = props;
@@ -124,6 +123,7 @@ const MessageSimpleWithContext = (props: MessageSimplePropsWithContext) => {
colors: { blue_alice, grey_gainsboro, light_blue, light_gray, transparent },
messageSimple: {
container,
+ repliesContainer,
content: {
container: contentContainer,
errorContainer,
@@ -157,14 +157,6 @@ const MessageSimpleWithContext = (props: MessageSimplePropsWithContext) => {
...messageGroupedTopContainer,
};
- const onLayout: (event: LayoutChangeEvent) => void = ({
- nativeEvent: {
- layout: { width },
- },
- }) => {
- setMessageContentWidth(width);
- };
-
const groupStyle = `${alignment}_${groupStyles?.[0]?.toLowerCase?.()}`;
let noBorder = onlyEmojis && !message.quoted_message;
@@ -189,8 +181,6 @@ const MessageSimpleWithContext = (props: MessageSimplePropsWithContext) => {
backgroundColor = receiverMessageBackgroundColor ?? light_gray;
}
- const repliesCurveColor = isMessageReceivedOrErrorType ? grey_gainsboro : backgroundColor;
-
const onSwipeActionHandler = useStableCallback(() => {
if (customMessageSwipeAction) {
customMessageSwipeAction({ channel, message });
@@ -200,100 +190,99 @@ const MessageSimpleWithContext = (props: MessageSimplePropsWithContext) => {
});
return (
-
- {alignment === 'left' ? : null}
- {isMessageTypeDeleted ? (
-
- ) : (
-
+
+
+ {alignment === 'left' ? : null}
+ {isMessageTypeDeleted ? (
+
+ ) : (
- {MessageHeader && (
-
+
+
+ ) : (
+
+ )}
+ {enableSwipeToReply ? (
+
+ ) : (
+
)}
- {message.pinned ? : null}
+
+
+
+
+
+ {reactionListPosition === 'bottom' && ReactionListBottom ? (
+
+ ) : null}
+
- {enableSwipeToReply ? (
-
- ) : (
-
- )}
- {reactionListPosition === 'bottom' && ReactionListBottom ? : null}
-
-
-
- )}
+ )}
+
);
-};
+});
const areEqual = (
prevProps: MessageSimplePropsWithContext,
@@ -303,23 +292,23 @@ const areEqual = (
channel: prevChannel,
groupStyles: prevGroupStyles,
hasReactions: prevHasReactions,
- lastGroupMessage: prevLastGroupMessage,
- members: prevMembers,
message: prevMessage,
myMessageTheme: prevMyMessageTheme,
onlyEmojis: prevOnlyEmojis,
otherAttachments: prevOtherAttachments,
+ lastGroupMessage: prevLastGroupMessage,
+ members: prevMembers,
} = prevProps;
const {
channel: nextChannel,
groupStyles: nextGroupStyles,
hasReactions: nextHasReactions,
- lastGroupMessage: nextLastGroupMessage,
- members: nextMembers,
message: nextMessage,
myMessageTheme: nextMyMessageTheme,
onlyEmojis: nextOnlyEmojis,
otherAttachments: nextOtherAttachments,
+ lastGroupMessage: nextLastGroupMessage,
+ members: nextMembers,
} = nextProps;
const hasReactionsEqual = prevHasReactions === nextHasReactions;
@@ -431,22 +420,22 @@ export type MessageSimpleProps = Partial;
*
* Message UI component
*/
-export const MessageSimple = (props: MessageSimpleProps) => {
+export const MessageSimple = forwardRef((props, ref) => {
const {
alignment,
channel,
groupStyles,
hasReactions,
isMyMessage,
- lastGroupMessage,
- members,
message,
onlyEmojis,
otherAttachments,
- showMessageStatus,
isMessageAIGenerated,
setQuotedMessage,
+ lastGroupMessage,
+ members,
} = useMessageContext();
+
const {
customMessageSwipeAction,
enableMessageGroupingByUser,
@@ -454,11 +443,10 @@ export const MessageSimple = (props: MessageSimpleProps) => {
MessageAvatar,
MessageContent,
MessageDeleted,
+ MessageError,
MessageFooter,
MessageHeader,
- MessagePinnedHeader,
MessageReplies,
- MessageStatus,
MessageSwipeContent,
messageSwipeToReplyHitSlop,
myMessageTheme,
@@ -483,17 +471,14 @@ export const MessageSimple = (props: MessageSimpleProps) => {
groupStyles,
hasReactions,
isMyMessage,
- lastGroupMessage,
- members,
message,
MessageAvatar,
MessageContent,
MessageDeleted,
+ MessageError,
MessageFooter,
MessageHeader,
- MessagePinnedHeader,
MessageReplies,
- MessageStatus,
MessageSwipeContent,
messageSwipeToReplyHitSlop,
myMessageTheme,
@@ -504,11 +489,13 @@ export const MessageSimple = (props: MessageSimpleProps) => {
ReactionListTop,
setQuotedMessage,
shouldRenderSwipeableWrapper,
- showMessageStatus,
+ lastGroupMessage,
+ members,
}}
+ ref={ref}
{...props}
/>
);
-};
+});
MessageSimple.displayName = 'MessageSimple{messageSimple{container}}';
diff --git a/package/src/components/Message/MessageSimple/MessageStatus.tsx b/package/src/components/Message/MessageSimple/MessageStatus.tsx
index f133e9e510..0fed3728b5 100644
--- a/package/src/components/Message/MessageSimple/MessageStatus.tsx
+++ b/package/src/components/Message/MessageSimple/MessageStatus.tsx
@@ -1,33 +1,50 @@
-import React from 'react';
-import { StyleSheet, Text, View } from 'react-native';
+import React, { useMemo } from 'react';
+import { StyleSheet, View } from 'react-native';
import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
import {
MessageContextValue,
useMessageContext,
} from '../../../contexts/messageContext/MessageContext';
+import {
+ MessagesContextValue,
+ useMessagesContext,
+} from '../../../contexts/messagesContext/MessagesContext';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { Check } from '../../../icons/Check';
import { CheckAll } from '../../../icons/CheckAll';
import { Time } from '../../../icons/Time';
+import { primitives } from '../../../theme';
import { MessageStatusTypes } from '../../../utils/utils';
export type MessageStatusPropsWithContext = Pick<
MessageContextValue,
'deliveredToCount' | 'message' | 'readBy' | 'threadList'
-> & {
- channelMembersCount: number;
-};
+> &
+ Pick & {
+ formattedDate?: string | Date;
+ timestamp?: string | Date;
+ };
const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => {
- const { channelMembersCount, deliveredToCount, message, readBy, threadList } = props;
+ const {
+ deliveredToCount,
+ formattedDate,
+ message,
+ readBy,
+ threadList,
+ timestamp,
+ MessageTimestamp,
+ } = props;
+
+ const styles = useStyles();
const {
theme: {
- colors: { accent_blue, grey_dark },
messageSimple: {
- status: { checkAllIcon, checkIcon, readByCount, statusContainer, timeIcon },
+ status: { checkAllIcon, checkIcon, container, timeIcon },
},
+ semantics,
},
} = useTheme();
@@ -47,30 +64,42 @@ const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => {
!read &&
message.type !== 'ephemeral';
- const isGroupChannel = channelMembersCount > 2;
-
- const shouldDisplayReadByCount = isGroupChannel && hasReadByGreaterThanOne;
- const countOfReadBy = typeof readBy === 'number' && shouldDisplayReadByCount ? readBy - 1 : 0;
-
return (
-
- {shouldDisplayReadByCount ? (
-
- {countOfReadBy}
-
- ) : null}
+
{read ? (
-
+
) : delivered ? (
-
+
) : sending ? (
-
+
) : sent ? (
-
+
) : null}
+
);
};
@@ -84,14 +113,16 @@ const areEqual = (
message: prevMessage,
readBy: prevReadBy,
threadList: prevThreadList,
- channelMembersCount: prevChannelMembersCount,
+ formattedDate: prevFormattedDate,
+ timestamp: prevTimestamp,
} = prevProps;
const {
deliveredToCount: nextDeliveredBy,
message: nextMessage,
readBy: nextReadBy,
threadList: nextThreadList,
- channelMembersCount: nextChannelMembersCount,
+ formattedDate: nextFormattedDate,
+ timestamp: nextTimestamp,
} = nextProps;
const deliveredByEqual = prevDeliveredBy === nextDeliveredBy;
@@ -109,17 +140,22 @@ const areEqual = (
return false;
}
- const channelMembersCountEqual = prevChannelMembersCount === nextChannelMembersCount;
- if (!channelMembersCountEqual) {
- return false;
- }
-
const messageEqual =
prevMessage.status === nextMessage.status && prevMessage.type === nextMessage.type;
if (!messageEqual) {
return false;
}
+ const timestampEqual = prevTimestamp === nextTimestamp;
+ if (!timestampEqual) {
+ return false;
+ }
+
+ const formattedDateEqual = prevFormattedDate === nextFormattedDate;
+ if (!formattedDateEqual) {
+ return false;
+ }
+
return true;
};
@@ -133,12 +169,21 @@ export type MessageStatusProps = Partial;
export const MessageStatus = (props: MessageStatusProps) => {
const { channel } = useChannelContext();
const { deliveredToCount, message, readBy, threadList } = useMessageContext();
+ const { MessageTimestamp } = useMessagesContext();
const channelMembersCount = Object.keys(channel?.state.members).length;
return (
);
@@ -146,16 +191,24 @@ export const MessageStatus = (props: MessageStatusProps) => {
MessageStatus.displayName = 'MessageStatus{messageSimple{status}}';
-const styles = StyleSheet.create({
- readByCount: {
- fontSize: 11,
- fontWeight: '700',
- paddingRight: 3,
- },
- statusContainer: {
- alignItems: 'flex-end',
- flexDirection: 'row',
- justifyContent: 'center',
- paddingRight: 3,
- },
-});
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ return useMemo(() => {
+ return StyleSheet.create({
+ readByCount: {
+ color: semantics.accentPrimary,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightTight,
+ },
+ container: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: primitives.spacingXxs,
+ },
+ });
+ }, [semantics]);
+};
diff --git a/package/src/components/Message/MessageSimple/MessageSwipeContent.tsx b/package/src/components/Message/MessageSimple/MessageSwipeContent.tsx
index 3bad53c50b..e47b2e2b52 100644
--- a/package/src/components/Message/MessageSimple/MessageSwipeContent.tsx
+++ b/package/src/components/Message/MessageSimple/MessageSwipeContent.tsx
@@ -2,12 +2,12 @@ import React from 'react';
import { StyleSheet, View } from 'react-native';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { CurveLineLeftUp } from '../../../icons';
+import { CurveLineLeftUpReply } from '../../../icons';
export const MessageSwipeContent = () => {
const {
theme: {
- colors: { grey },
+ semantics,
messageSimple: {
swipeLeftContent: { container },
},
@@ -15,7 +15,7 @@ export const MessageSwipeContent = () => {
} = useTheme();
return (
-
+
);
};
diff --git a/package/src/components/Message/MessageSimple/MessageTextContainer.tsx b/package/src/components/Message/MessageSimple/MessageTextContainer.tsx
index 6f5c892713..e3af2215f1 100644
--- a/package/src/components/Message/MessageSimple/MessageTextContainer.tsx
+++ b/package/src/components/Message/MessageSimple/MessageTextContainer.tsx
@@ -19,7 +19,7 @@ import type { MarkdownStyle, Theme } from '../../../contexts/themeContext/utils/
import { useTranslatedMessage } from '../../../hooks/useTranslatedMessage';
const styles = StyleSheet.create({
- textContainer: { maxWidth: 250, paddingHorizontal: 16 },
+ textContainer: { maxWidth: 256 },
});
export type MessageTextProps = MessageTextContainerProps & {
diff --git a/package/src/components/Message/MessageSimple/MessageTimestamp.tsx b/package/src/components/Message/MessageSimple/MessageTimestamp.tsx
index 7b831097f8..ac4307abf1 100644
--- a/package/src/components/Message/MessageSimple/MessageTimestamp.tsx
+++ b/package/src/components/Message/MessageSimple/MessageTimestamp.tsx
@@ -6,6 +6,7 @@ import {
TranslationContextValue,
useTranslationContext,
} from '../../../contexts/translationContext/TranslationContext';
+import { primitives } from '../../../theme';
import { getDateString } from '../../../utils/i18n/getDateString';
export type MessageTimestampProps = Partial> & {
@@ -32,15 +33,7 @@ export const MessageTimestamp = (props: MessageTimestampProps) => {
} = props;
const { t, tDateTimeParser: contextTDateTimeParser } = useTranslationContext();
const tDateTimeParser = propsTDateTimeParser || contextTDateTimeParser;
-
- const {
- theme: {
- colors: { grey },
- messageSimple: {
- content: { timestampText },
- },
- },
- } = useTheme();
+ const styles = useStyles();
const dateString = useMemo(
() =>
@@ -54,20 +47,34 @@ export const MessageTimestamp = (props: MessageTimestampProps) => {
);
if (formattedDate) {
- return (
- {formattedDate.toString()}
- );
+ return {formattedDate.toString()};
}
if (!dateString) {
return null;
}
- return {dateString.toString()};
+ return {dateString.toString()};
};
-const styles = StyleSheet.create({
- text: {
- fontSize: 12,
- },
-});
+const useStyles = () => {
+ const {
+ theme: {
+ semantics,
+ messageSimple: {
+ content: { timestampText },
+ },
+ },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ text: {
+ color: semantics.chatTextTimestamp,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightTight,
+ ...timestampText,
+ },
+ });
+ }, [semantics, timestampText]);
+};
diff --git a/package/src/components/Message/MessageSimple/MessageWrapper.tsx b/package/src/components/Message/MessageSimple/MessageWrapper.tsx
index 39a4a36f22..97da35634e 100644
--- a/package/src/components/Message/MessageSimple/MessageWrapper.tsx
+++ b/package/src/components/Message/MessageSimple/MessageWrapper.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React from 'react';
import { View } from 'react-native';
@@ -14,7 +14,6 @@ import { ThemeProvider, useTheme } from '../../../contexts/themeContext/ThemeCon
import { useStateStore } from '../../../hooks/useStateStore';
import { ChannelUnreadStateStoreType } from '../../../state-store/channel-unread-state';
-import { MessagePreviousAndNextMessageStoreType } from '../../../state-store/message-list-prev-next-state';
const channelUnreadStateSelector = (state: ChannelUnreadStateStoreType) => ({
first_unread_message_id: state.channelUnreadState?.first_unread_message_id,
@@ -25,10 +24,12 @@ const channelUnreadStateSelector = (state: ChannelUnreadStateStoreType) => ({
export type MessageWrapperProps = {
message: LocalMessage;
+ previousMessage?: LocalMessage;
+ nextMessage?: LocalMessage;
};
export const MessageWrapper = React.memo((props: MessageWrapperProps) => {
- const { message } = props;
+ const { message, previousMessage, nextMessage } = props;
const { client } = useChatContext();
const {
channelUnreadStateStore,
@@ -47,34 +48,22 @@ export const MessageWrapper = React.memo((props: MessageWrapperProps) => {
myMessageTheme,
shouldShowUnreadUnderlay,
} = useMessagesContext();
- const {
- goToMessage,
- onThreadSelect,
- noGroupByUser,
- modifiedTheme,
- messageListPreviousAndNextMessageStore,
- } = useMessageListItemContext();
+ const { goToMessage, onThreadSelect, noGroupByUser, modifiedTheme } = useMessageListItemContext();
const dateSeparatorDate = useMessageDateSeparator({
hideDateSeparators,
message,
- messageListPreviousAndNextMessageStore,
+ previousMessage,
});
- const selector = useCallback(
- (state: MessagePreviousAndNextMessageStoreType) => ({
- nextMessage: state.messageList[message.id]?.nextMessage,
- }),
- [message.id],
- );
- const { nextMessage } = useStateStore(messageListPreviousAndNextMessageStore.state, selector);
const isNewestMessage = nextMessage === undefined;
const groupStyles = useMessageGroupStyles({
dateSeparatorDate,
getMessageGroupStyle,
maxTimeBetweenGroupedMessages,
message,
- messageListPreviousAndNextMessageStore,
+ previousMessage,
+ nextMessage,
noGroupByUser,
});
diff --git a/package/src/components/Message/MessageSimple/ReactionList/ReactionListBottom.tsx b/package/src/components/Message/MessageSimple/ReactionList/ReactionListBottom.tsx
index afac92be24..0aa031c85c 100644
--- a/package/src/components/Message/MessageSimple/ReactionList/ReactionListBottom.tsx
+++ b/package/src/components/Message/MessageSimple/ReactionList/ReactionListBottom.tsx
@@ -39,7 +39,7 @@ export type ReactionListBottomItemProps = Partial<
| 'onPress'
| 'onPressIn'
| 'preventPress'
- | 'showMessageOverlay'
+ | 'showReactionsOverlay'
>
> &
Partial> & {
@@ -54,7 +54,7 @@ export const ReactionListBottomItem = (props: ReactionListBottomItemProps) => {
onPressIn,
preventPress,
reaction,
- showMessageOverlay,
+ showReactionsOverlay,
supportedReactions,
} = props;
const scaleValue = useRef(new Animated.Value(1)).current;
@@ -101,8 +101,8 @@ export const ReactionListBottomItem = (props: ReactionListBottomItemProps) => {
if (onLongPress) {
onLongPress({
defaultHandler: () => {
- if (showMessageOverlay) {
- showMessageOverlay(true, reaction.type);
+ if (showReactionsOverlay) {
+ showReactionsOverlay(reaction.type);
}
},
emitter: 'reactionList',
@@ -172,7 +172,7 @@ const renderItem = ({ index, item }: { index: number; item: ReactionListBottomIt
onPressIn={item.onPressIn}
preventPress={item.preventPress}
reaction={item.reaction}
- showMessageOverlay={item.showMessageOverlay}
+ showReactionsOverlay={item.showReactionsOverlay}
supportedReactions={item.supportedReactions}
/>
);
@@ -187,7 +187,7 @@ export type ReactionListBottomProps = Partial<
| 'onPressIn'
| 'preventPress'
| 'reactions'
- | 'showMessageOverlay'
+ | 'showReactionsOverlay'
>
> &
Partial>;
@@ -201,7 +201,7 @@ export const ReactionListBottom = (props: ReactionListBottomProps) => {
onPressIn: propOnPressIn,
preventPress: propPreventPress,
reactions: propReactions,
- showMessageOverlay: propShowMessageOverlay,
+ showReactionsOverlay: propShowReactionsOverlay,
supportedReactions: propSupportedReactions,
} = props;
@@ -213,7 +213,7 @@ export const ReactionListBottom = (props: ReactionListBottomProps) => {
onPressIn: contextOnPressIn,
preventPress: contextPreventPress,
reactions: contextReactions,
- showMessageOverlay: contextShowMessageOverlay,
+ showReactionsOverlay: contextShowReactionsOverlay,
} = useMessageContext();
const { supportedReactions: contextSupportedReactions } = useMessagesContext();
@@ -225,7 +225,7 @@ export const ReactionListBottom = (props: ReactionListBottomProps) => {
const onPressIn = propOnPressIn || contextOnPressIn;
const preventPress = propPreventPress || contextPreventPress;
const reactions = propReactions || contextReactions;
- const showMessageOverlay = propShowMessageOverlay || contextShowMessageOverlay;
+ const showMessageOverlay = propShowReactionsOverlay || contextShowReactionsOverlay;
const supportedReactions = propSupportedReactions || contextSupportedReactions;
const {
theme: {
diff --git a/package/src/components/Message/MessageSimple/ReactionList/ReactionListTop.tsx b/package/src/components/Message/MessageSimple/ReactionList/ReactionListTop.tsx
index 51b3b6b795..58f79502c4 100644
--- a/package/src/components/Message/MessageSimple/ReactionList/ReactionListTop.tsx
+++ b/package/src/components/Message/MessageSimple/ReactionList/ReactionListTop.tsx
@@ -88,7 +88,7 @@ export type ReactionListTopProps = Partial<
| 'onPressIn'
| 'preventPress'
| 'reactions'
- | 'showMessageOverlay'
+ | 'showReactionsOverlay'
>
> &
Pick & {
@@ -112,7 +112,7 @@ export const ReactionListTop = (props: ReactionListTopProps) => {
preventPress: propPreventPress,
reactions: propReactions,
reactionSize: propReactionSize,
- showMessageOverlay: propShowMessageOverlay,
+ showReactionsOverlay: propShowReactionsOverlay,
supportedReactions: propSupportedReactions,
} = props;
@@ -124,7 +124,7 @@ export const ReactionListTop = (props: ReactionListTopProps) => {
onPressIn: contextOnPressIn,
preventPress: contextPreventPress,
reactions: contextReactions,
- showMessageOverlay: contextShowMessageOverlay,
+ showReactionsOverlay: contextShowReactionsOverlay,
} = useMessageContext();
const { supportedReactions: contextSupportedReactions } = useMessagesContext();
@@ -136,7 +136,7 @@ export const ReactionListTop = (props: ReactionListTopProps) => {
const onPressIn = propOnPressIn || contextOnPressIn;
const preventPress = propPreventPress || contextPreventPress;
const reactions = propReactions || contextReactions;
- const showMessageOverlay = propShowMessageOverlay || contextShowMessageOverlay;
+ const showReactionsOverlay = propShowReactionsOverlay || contextShowReactionsOverlay;
const supportedReactions = propSupportedReactions || contextSupportedReactions;
const {
@@ -195,7 +195,7 @@ export const ReactionListTop = (props: ReactionListTopProps) => {
onPress={(event) => {
if (onPress) {
onPress({
- defaultHandler: () => showMessageOverlay(true),
+ defaultHandler: showReactionsOverlay,
emitter: 'reactionList',
event,
});
@@ -204,7 +204,7 @@ export const ReactionListTop = (props: ReactionListTopProps) => {
onPressIn={(event) => {
if (onPressIn) {
onPressIn({
- defaultHandler: () => showMessageOverlay(true),
+ defaultHandler: showReactionsOverlay,
emitter: 'reactionList',
event,
});
diff --git a/package/src/components/Message/MessageSimple/__tests__/Message.test.js b/package/src/components/Message/MessageSimple/__tests__/Message.test.js
index 69779d57fa..1ea9467329 100644
--- a/package/src/components/Message/MessageSimple/__tests__/Message.test.js
+++ b/package/src/components/Message/MessageSimple/__tests__/Message.test.js
@@ -1,5 +1,7 @@
import React from 'react';
+import { SafeAreaProvider } from 'react-native-safe-area-context';
+
import { cleanup, fireEvent, render, waitFor } from '@testing-library/react-native';
import { OverlayProvider } from '../../../../contexts/overlayContext/OverlayProvider';
@@ -37,15 +39,16 @@ describe('Message', () => {
renderMessage = (options) =>
render(
-
-
-
-
-
-
-
- ,
- ,
+
+
+
+
+
+
+
+
+
+ ,
);
});
diff --git a/package/src/components/Message/MessageSimple/__tests__/MessageAvatar.test.js b/package/src/components/Message/MessageSimple/__tests__/MessageAvatar.test.js
index 6245559578..62ef3248e6 100644
--- a/package/src/components/Message/MessageSimple/__tests__/MessageAvatar.test.js
+++ b/package/src/components/Message/MessageSimple/__tests__/MessageAvatar.test.js
@@ -2,27 +2,34 @@ import React from 'react';
import { cleanup, render, screen, waitFor } from '@testing-library/react-native';
-import { ThemeProvider } from '../../../../contexts/themeContext/ThemeContext';
import { defaultTheme } from '../../../../contexts/themeContext/utils/theme';
import {
generateMessage,
generateStaticMessage,
} from '../../../../mock-builders/generator/message';
import { generateStaticUser } from '../../../../mock-builders/generator/user';
+import { getTestClientWithUser } from '../../../../mock-builders/mock';
+import { Chat } from '../../../Chat/Chat';
import { MessageAvatar } from '../MessageAvatar';
afterEach(cleanup);
describe('MessageAvatar', () => {
+ let chatClient;
+
+ beforeEach(async () => {
+ chatClient = await getTestClientWithUser({ id: 'me' });
+ });
+
it('should render message avatar', async () => {
const staticUser = generateStaticUser(0);
const message = generateMessage({
user: { ...staticUser, image: undefined },
});
render(
-
+
- ,
+ ,
);
await waitFor(() => {
@@ -30,14 +37,14 @@ describe('MessageAvatar', () => {
});
screen.rerender(
-
+
- ,
+ ,
);
await waitFor(() => {
expect(screen.getByTestId('spacer')).toBeTruthy();
- expect(screen.queryAllByTestId('avatar-image')).toHaveLength(0);
+ expect(screen.queryAllByTestId('user-avatar')).toHaveLength(0);
});
const staticMessage = generateStaticMessage('hi', {
@@ -45,19 +52,19 @@ describe('MessageAvatar', () => {
});
screen.rerender(
-
+
- ,
+ ,
);
await waitFor(() => {
expect(screen.getByTestId('message-avatar')).toBeTruthy();
- expect(screen.getByTestId('avatar-image')).toBeTruthy();
+ expect(screen.getByTestId('user-avatar')).toBeTruthy();
expect(screen.toJSON()).toMatchSnapshot();
});
});
diff --git a/package/src/components/Message/MessageSimple/__tests__/MessagePinnedHeader.test.js b/package/src/components/Message/MessageSimple/__tests__/MessagePinnedHeader.test.js
index 6eaa587355..10636e174a 100644
--- a/package/src/components/Message/MessageSimple/__tests__/MessagePinnedHeader.test.js
+++ b/package/src/components/Message/MessageSimple/__tests__/MessagePinnedHeader.test.js
@@ -18,35 +18,37 @@ describe('MessagePinnedHeader', () => {
const staticUser = generateStaticUser(0);
const message = generateMessage({
user: { ...staticUser, image: undefined },
+ pinned: true,
});
render(
-
+
,
);
await waitFor(() => {
- expect(screen.getByTestId('message-pinned')).toBeTruthy();
+ expect(screen.getByLabelText('Message Pinned Header')).toBeTruthy();
});
screen.rerender(
-
+
,
);
const staticMessage = generateStaticMessage('hi', {
user: staticUser,
+ pinned: false,
});
screen.rerender(
-
+
,
);
await waitFor(() => {
- expect(screen.getByTestId('message-pinned')).toBeTruthy();
+ expect(screen.queryByLabelText('Message Pinned Header')).toBeNull();
expect(screen.toJSON()).toMatchSnapshot();
});
});
diff --git a/package/src/components/Message/MessageSimple/__tests__/MessageReplies.test.js b/package/src/components/Message/MessageSimple/__tests__/MessageReplies.test.js
index b8d7769fb8..41207aa481 100644
--- a/package/src/components/Message/MessageSimple/__tests__/MessageReplies.test.js
+++ b/package/src/components/Message/MessageSimple/__tests__/MessageReplies.test.js
@@ -38,12 +38,10 @@ describe('MessageReplies', () => {
await waitFor(() => {
expect(screen.getByTestId('message-replies')).toBeTruthy();
- expect(screen.getByTestId('message-replies-right')).toBeTruthy();
- expect(screen.queryAllByTestId('message-replies-left')).toHaveLength(0);
- expect(t).toHaveBeenCalledWith('{{ replyCount }} Thread Replies', {
+ expect(t).toHaveBeenCalledWith('{{ replyCount }} Replies', {
replyCount: message.reply_count,
});
- expect(screen.getByText('{{ replyCount }} Thread Replies')).toBeTruthy();
+ expect(screen.getByText('{{ replyCount }} Replies')).toBeTruthy();
});
const message2 = generateMessage({
@@ -70,10 +68,8 @@ describe('MessageReplies', () => {
await waitFor(() => {
expect(onPressMock).toHaveBeenCalled();
expect(screen.getByTestId('message-replies')).toBeTruthy();
- expect(screen.getByTestId('message-replies-left')).toBeTruthy();
- expect(screen.queryAllByTestId('message-replies-right')).toHaveLength(0);
- expect(t).toHaveBeenCalledWith('1 Thread Reply');
- expect(screen.getByText('1 Thread Reply')).toBeTruthy();
+ expect(t).toHaveBeenCalledWith('1 Reply');
+ expect(screen.getByText('1 Reply')).toBeTruthy();
});
});
@@ -98,8 +94,6 @@ describe('MessageReplies', () => {
await waitFor(() => {
expect(screen.queryAllByTestId('message-replies')).toHaveLength(0);
- expect(screen.queryAllByTestId('message-replies-left')).toHaveLength(0);
- expect(screen.queryAllByTestId('message-replies-right')).toHaveLength(0);
});
const message2 = generateMessage({
@@ -123,8 +117,6 @@ describe('MessageReplies', () => {
await waitFor(() => {
expect(screen.queryAllByTestId('message-replies')).toHaveLength(0);
- expect(screen.queryAllByTestId('message-replies-left')).toHaveLength(0);
- expect(screen.queryAllByTestId('message-replies-right')).toHaveLength(0);
});
});
});
diff --git a/package/src/components/Message/MessageSimple/__tests__/MessageSimple.test.js b/package/src/components/Message/MessageSimple/__tests__/MessageSimple.test.js
index c2079bdac8..4009988423 100644
--- a/package/src/components/Message/MessageSimple/__tests__/MessageSimple.test.js
+++ b/package/src/components/Message/MessageSimple/__tests__/MessageSimple.test.js
@@ -171,9 +171,7 @@ describe('MessageSimple', () => {
renderMessage({ message });
await waitFor(() => {
- expect(screen.getByTestId('message-simple-wrapper').props.style[1]).toMatchObject({
- marginBottom: 8,
- });
+ expect(screen.getByTestId('message-simple-wrapper').props.style[1]).toMatchObject({});
});
});
diff --git a/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js b/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js
index e741c75844..dbf94316ad 100644
--- a/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js
+++ b/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js
@@ -58,19 +58,18 @@ describe('MessageStatus', () => {
,
);
- it('should render message status with read by container', async () => {
+ it.each('should render message status with read by container', async () => {
const user = generateUser();
const message = generateMessage({ user });
const readBy = 2;
- const { getByLabelText, getByText, rerender, toJSON } = renderMessageStatus({
+ const { getByText, rerender, toJSON } = renderMessageStatus({
deliveredToCount: 2,
message,
readBy,
});
await waitFor(() => {
- expect(getByLabelText('Read by count')).toBeTruthy();
expect(getByText((readBy - 1).toString())).toBeTruthy();
});
@@ -89,7 +88,6 @@ describe('MessageStatus', () => {
await waitFor(() => {
expect(toJSON()).toMatchSnapshot();
- expect(getByLabelText('Read by count')).toBeTruthy();
expect(getByText((readBy - 1).toString())).toBeTruthy();
});
});
diff --git a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageAvatar.test.js.snap b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageAvatar.test.js.snap
index 4b9ebc8f6f..c611896625 100644
--- a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageAvatar.test.js.snap
+++ b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageAvatar.test.js.snap
@@ -1,38 +1,38 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`MessageAvatar should render message avatar 1`] = `
-
+
diff --git a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessagePinnedHeader.test.js.snap b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessagePinnedHeader.test.js.snap
index 05765328b6..4bebf414da 100644
--- a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessagePinnedHeader.test.js.snap
+++ b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessagePinnedHeader.test.js.snap
@@ -1,93 +1,3 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
-exports[`MessagePinnedHeader should render message pinned 1`] = `
-
-
-
-
-
-
-
- Pinned by
-
- You
-
-
-`;
+exports[`MessagePinnedHeader should render message pinned 1`] = `null`;
diff --git a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageStatus.test.js.snap b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageStatus.test.js.snap
deleted file mode 100644
index f771d43d78..0000000000
--- a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageStatus.test.js.snap
+++ /dev/null
@@ -1,169 +0,0 @@
-// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
-
-exports[`MessageStatus should render message status with read by container 1`] = `
-
-
-
-
- 1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap
index c1627aa968..a3949b04ee 100644
--- a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap
+++ b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap
@@ -1,12 +1,11 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`MessageTextContainer should render message text container 1`] = `
{
const {
- theme: {
- colors: { accent_red, grey },
- },
+ theme: { semantics },
} = useTheme();
const {
handleCopyMessage,
@@ -154,8 +152,9 @@ export const useMessageActions = ({
}
},
actionType: 'banUser',
- icon: ,
+ icon: ,
title: message.user?.banned ? t('Unban User') : t('Ban User'),
+ type: 'destructive',
};
const copyMessage: MessageActionType = {
@@ -167,8 +166,9 @@ export const useMessageActions = ({
handleCopyMessage();
},
actionType: 'copyMessage',
- icon: ,
+ icon: ,
title: t('Copy Message'),
+ type: 'standard',
};
const deleteMessage: MessageActionType = {
@@ -180,9 +180,10 @@ export const useMessageActions = ({
handleDeleteMessage();
},
actionType: 'deleteMessage',
- icon: ,
+ icon: ,
title: t('Delete Message'),
- titleStyle: { color: accent_red },
+ titleStyle: { color: semantics.accentError },
+ type: 'destructive',
};
const deleteForMeMessage: MessageActionType = {
@@ -194,8 +195,10 @@ export const useMessageActions = ({
handleDeleteForMeMessage();
},
actionType: 'deleteForMeMessage',
- icon: ,
+ icon: ,
title: t('Delete for me'),
+ titleStyle: { color: semantics.accentError },
+ type: 'destructive',
};
const editMessage: MessageActionType = {
@@ -207,8 +210,9 @@ export const useMessageActions = ({
handleEditMessage();
},
actionType: 'editMessage',
- icon: ,
+ icon: ,
title: t('Edit Message'),
+ type: 'standard',
};
const flagMessage: MessageActionType = {
@@ -220,8 +224,9 @@ export const useMessageActions = ({
handleFlagMessage();
},
actionType: 'flagMessage',
- icon: ,
+ icon: ,
title: t('Flag Message'),
+ type: 'standard',
};
const markUnread: MessageActionType = {
@@ -233,8 +238,9 @@ export const useMessageActions = ({
handleMarkUnreadMessage();
},
actionType: 'markUnread',
- icon: ,
+ icon: ,
title: t('Mark as Unread'),
+ type: 'standard',
};
const pinMessage: MessageActionType = {
@@ -246,8 +252,9 @@ export const useMessageActions = ({
handleTogglePinMessage();
},
actionType: 'pinMessage',
- icon: ,
+ icon: ,
title: t('Pin to Conversation'),
+ type: 'standard',
};
const unpinMessage: MessageActionType = {
@@ -259,8 +266,10 @@ export const useMessageActions = ({
handleTogglePinMessage();
},
actionType: 'unpinMessage',
- icon: ,
+ // TODO: V9: This icon does not exist yet, replace the old when when we get a new one
+ icon: ,
title: t('Unpin from Conversation'),
+ type: 'standard',
};
const handleReaction = !error
@@ -287,8 +296,9 @@ export const useMessageActions = ({
}
},
actionType: 'muteUser',
- icon: ,
+ icon: ,
title: isMuted ? t('Unmute User') : t('Mute User'),
+ type: 'standard',
};
const quotedReply: MessageActionType = {
@@ -300,8 +310,9 @@ export const useMessageActions = ({
handleQuotedReplyMessage();
},
actionType: 'quotedReply',
- icon: ,
+ icon: ,
title: t('Reply'),
+ type: 'standard',
};
const retry: MessageActionType = {
@@ -315,8 +326,9 @@ export const useMessageActions = ({
await handleResendMessage();
},
actionType: 'retry',
- icon: ,
+ icon: ,
title: t('Resend'),
+ type: 'standard',
};
const threadReply: MessageActionType = {
@@ -328,8 +340,9 @@ export const useMessageActions = ({
onOpenThread();
},
actionType: 'threadReply',
- icon: ,
+ icon: ,
title: t('Thread Reply'),
+ type: 'standard',
};
return {
diff --git a/package/src/components/Message/utils/measureInWindow.ts b/package/src/components/Message/utils/measureInWindow.ts
new file mode 100644
index 0000000000..fb22c07e49
--- /dev/null
+++ b/package/src/components/Message/utils/measureInWindow.ts
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Platform, View } from 'react-native';
+import { EdgeInsets } from 'react-native-safe-area-context';
+
+export const measureInWindow = (
+ node: React.RefObject,
+ insets: EdgeInsets,
+): Promise<{ x: number; y: number; w: number; h: number }> => {
+ return new Promise((resolve, reject) => {
+ const handle = node.current;
+ if (!handle)
+ return reject(
+ new Error('The native handle could not be found while invoking measureInWindow.'),
+ );
+
+ handle.measureInWindow((x, y, w, h) =>
+ resolve({ h, w, x, y: y + (Platform.OS === 'android' ? insets.top : 0) }),
+ );
+ });
+};
diff --git a/package/src/components/Message/utils/messageActions.ts b/package/src/components/Message/utils/messageActions.ts
index d7973cc2db..3ecb01c3b1 100644
--- a/package/src/components/Message/utils/messageActions.ts
+++ b/package/src/components/Message/utils/messageActions.ts
@@ -43,6 +43,7 @@ export const messageActions = ({
isMyMessage,
isThreadMessage,
markUnread,
+ muteUser,
message,
ownCapabilities,
pinMessage,
@@ -108,5 +109,9 @@ export const messageActions = ({
actions.push(deleteMessage);
}
+ if (!isMyMessage) {
+ actions.push(muteUser);
+ }
+
return actions;
};
diff --git a/package/src/components/MessageInput/AttachButton.tsx b/package/src/components/MessageInput/AttachButton.tsx
deleted file mode 100644
index a5a36efc96..0000000000
--- a/package/src/components/MessageInput/AttachButton.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-import React, { useState } from 'react';
-import type { GestureResponderEvent, LayoutChangeEvent, LayoutRectangle } from 'react-native';
-import { Pressable } from 'react-native';
-
-import { NativeAttachmentPicker } from './components/NativeAttachmentPicker';
-
-import {
- AttachmentPickerContextValue,
- useAttachmentPickerContext,
-} from '../../contexts/attachmentPickerContext/AttachmentPickerContext';
-import {
- MessageInputContextValue,
- useMessageInputContext,
-} from '../../contexts/messageInputContext/MessageInputContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { Attach } from '../../icons/Attach';
-
-type AttachButtonPropsWithContext = Pick<
- MessageInputContextValue,
- 'handleAttachButtonPress' | 'toggleAttachmentPicker'
-> &
- Pick & {
- disabled?: boolean;
- /** Function that opens attachment options bottom sheet */
- handleOnPress?: ((event: GestureResponderEvent) => void) & (() => void);
- };
-
-const AttachButtonWithContext = (props: AttachButtonPropsWithContext) => {
- const [showAttachButtonPicker, setShowAttachButtonPicker] = useState(false);
- const [attachButtonLayoutRectangle, setAttachButtonLayoutRectangle] = useState();
- const {
- disableAttachmentPicker,
- disabled = false,
- handleAttachButtonPress,
- handleOnPress,
- selectedPicker,
- toggleAttachmentPicker,
- } = props;
- const {
- theme: {
- colors: { accent_blue, grey },
- messageInput: { attachButton },
- },
- } = useTheme();
-
- const onAttachButtonLayout = (event: LayoutChangeEvent) => {
- const layout = event.nativeEvent.layout;
- setAttachButtonLayoutRectangle((prev) => {
- if (
- prev &&
- prev.width === layout.width &&
- prev.height === layout.height &&
- prev.x === layout.x &&
- prev.y === layout.y
- ) {
- return prev;
- }
- return layout;
- });
- };
-
- const attachButtonHandler = () => {
- setShowAttachButtonPicker((prevShowAttachButtonPicker) => !prevShowAttachButtonPicker);
- };
-
- const onPressHandler = () => {
- if (disabled) {
- return;
- }
- if (handleOnPress) {
- handleOnPress();
- return;
- }
- if (handleAttachButtonPress) {
- handleAttachButtonPress();
- return;
- }
- if (!disableAttachmentPicker) {
- toggleAttachmentPicker();
- } else {
- attachButtonHandler();
- }
- };
-
- return (
- <>
-
-
-
- {showAttachButtonPicker ? (
- setShowAttachButtonPicker(false)}
- />
- ) : null}
- >
- );
-};
-
-const areEqual = (
- prevProps: AttachButtonPropsWithContext,
- nextProps: AttachButtonPropsWithContext,
-) => {
- const { handleOnPress: prevHandleOnPress, selectedPicker: prevSelectedPicker } = prevProps;
- const { handleOnPress: nextHandleOnPress, selectedPicker: nextSelectedPicker } = nextProps;
-
- const handleOnPressEqual = prevHandleOnPress === nextHandleOnPress;
- if (!handleOnPressEqual) {
- return false;
- }
-
- const selectedPickerEqual = prevSelectedPicker === nextSelectedPicker;
- if (!selectedPickerEqual) {
- return false;
- }
-
- return true;
-};
-
-const MemoizedAttachButton = React.memo(
- AttachButtonWithContext,
- areEqual,
-) as typeof AttachButtonWithContext;
-
-export type AttachButtonProps = Partial;
-
-/**
- * UI Component for attach button in MessageInput component.
- */
-export const AttachButton = (props: AttachButtonProps) => {
- const { disableAttachmentPicker, selectedPicker } = useAttachmentPickerContext();
- const { handleAttachButtonPress, toggleAttachmentPicker } = useMessageInputContext();
-
- return (
-
- );
-};
-
-AttachButton.displayName = 'AttachButton{messageInput}';
diff --git a/package/src/components/MessageInput/AttachmentUploadPreviewList.tsx b/package/src/components/MessageInput/AttachmentUploadPreviewList.tsx
deleted file mode 100644
index ed225bbabe..0000000000
--- a/package/src/components/MessageInput/AttachmentUploadPreviewList.tsx
+++ /dev/null
@@ -1,277 +0,0 @@
-import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { FlatList, LayoutChangeEvent, StyleSheet, View } from 'react-native';
-
-import {
- isLocalAudioAttachment,
- isLocalFileAttachment,
- isLocalImageAttachment,
- isLocalVoiceRecordingAttachment,
- isVideoAttachment,
- LocalAttachment,
- LocalImageAttachment,
-} from 'stream-chat';
-
-import { useAudioPreviewManager } from './hooks/useAudioPreviewManager';
-
-import { useMessageComposer } from '../../contexts';
-import { useAttachmentManagerState } from '../../contexts/messageInputContext/hooks/useAttachmentManagerState';
-import {
- MessageInputContextValue,
- useMessageInputContext,
-} from '../../contexts/messageInputContext/MessageInputContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { isSoundPackageAvailable } from '../../native';
-
-const IMAGE_PREVIEW_SIZE = 100;
-const FILE_PREVIEW_HEIGHT = 60;
-
-export type AttachmentUploadPreviewListPropsWithContext = Pick<
- MessageInputContextValue,
- | 'AudioAttachmentUploadPreview'
- | 'FileAttachmentUploadPreview'
- | 'ImageAttachmentUploadPreview'
- | 'VideoAttachmentUploadPreview'
->;
-
-/**
- * AttachmentUploadPreviewList
- * UI Component to preview the files set for upload
- */
-const UnMemoizedAttachmentUploadListPreview = (
- props: AttachmentUploadPreviewListPropsWithContext,
-) => {
- const [flatListWidth, setFlatListWidth] = useState(0);
- const flatListRef = useRef | null>(null);
- const {
- AudioAttachmentUploadPreview,
- FileAttachmentUploadPreview,
- ImageAttachmentUploadPreview,
- VideoAttachmentUploadPreview,
- } = props;
- const { attachmentManager } = useMessageComposer();
- const { attachments } = useAttachmentManagerState();
- const {
- theme: {
- colors: { grey_whisper },
- messageInput: {
- attachmentSeparator,
- attachmentUploadPreviewList: { filesFlatList, imagesFlatList, wrapper },
- },
- },
- } = useTheme();
-
- const imageUploads = attachments.filter((attachment) => isLocalImageAttachment(attachment));
- const fileUploads = useMemo(() => {
- return attachments.filter((attachment) => !isLocalImageAttachment(attachment));
- }, [attachments]);
- const audioUploads = useMemo(() => {
- return fileUploads.filter(
- (attachment) =>
- isLocalAudioAttachment(attachment) || isLocalVoiceRecordingAttachment(attachment),
- );
- }, [fileUploads]);
-
- const { audioAttachmentsStateMap, onLoad, onProgress, onPlayPause } =
- useAudioPreviewManager(audioUploads);
-
- const renderImageItem = useCallback(
- ({ item }: { item: LocalImageAttachment }) => {
- return (
-
- );
- },
- [
- ImageAttachmentUploadPreview,
- attachmentManager.removeAttachments,
- attachmentManager.uploadAttachment,
- ],
- );
-
- const renderFileItem = useCallback(
- ({ item }: { item: LocalAttachment }) => {
- if (isLocalImageAttachment(item)) {
- // This is already handled in the `renderImageItem` above, so we return null here to avoid duplication.
- return null;
- } else if (isLocalVoiceRecordingAttachment(item)) {
- return (
-
- );
- } else if (isLocalAudioAttachment(item)) {
- if (isSoundPackageAvailable()) {
- return (
-
- );
- } else {
- return (
-
- );
- }
- } else if (isVideoAttachment(item)) {
- return (
-
- );
- } else if (isLocalFileAttachment(item)) {
- return (
-
- );
- } else return null;
- },
- [
- AudioAttachmentUploadPreview,
- FileAttachmentUploadPreview,
- VideoAttachmentUploadPreview,
- attachmentManager.removeAttachments,
- attachmentManager.uploadAttachment,
- audioAttachmentsStateMap,
- flatListWidth,
- onLoad,
- onPlayPause,
- onProgress,
- ],
- );
-
- useEffect(() => {
- if (fileUploads.length && flatListRef.current) {
- setTimeout(() => flatListRef.current?.scrollToEnd(), 1);
- }
- }, [fileUploads.length]);
-
- const onLayout = useCallback(
- (event: LayoutChangeEvent) => {
- if (flatListRef.current) {
- setFlatListWidth(event.nativeEvent.layout.width);
- }
- },
- [flatListRef],
- );
-
- if (!attachments.length) {
- return null;
- }
-
- return (
-
- {imageUploads.length ? (
- ({
- index,
- length: IMAGE_PREVIEW_SIZE + 8,
- offset: (IMAGE_PREVIEW_SIZE + 8) * index,
- })}
- horizontal
- keyExtractor={(item) => item.localMetadata.id}
- renderItem={renderImageItem}
- style={[styles.imagesFlatList, imagesFlatList]}
- />
- ) : null}
- {imageUploads.length && fileUploads.length ? (
-
- ) : null}
- {fileUploads.length ? (
- ({
- index,
- length: FILE_PREVIEW_HEIGHT + 8,
- offset: (FILE_PREVIEW_HEIGHT + 8) * index,
- })}
- keyExtractor={(item) => item.localMetadata.id}
- onLayout={onLayout}
- ref={flatListRef}
- renderItem={renderFileItem}
- style={[styles.filesFlatList, filesFlatList]}
- testID={'file-upload-preview'}
- />
- ) : null}
-
- );
-};
-
-export type AttachmentUploadPreviewListProps = Partial;
-
-const MemoizedAttachmentUploadPreviewListWithContext = React.memo(
- UnMemoizedAttachmentUploadListPreview,
-);
-
-/**
- * AttachmentUploadPreviewList
- * UI Component to preview the files set for upload
- */
-export const AttachmentUploadPreviewList = (props: AttachmentUploadPreviewListProps) => {
- const {
- AudioAttachmentUploadPreview,
- FileAttachmentUploadPreview,
- ImageAttachmentUploadPreview,
- VideoAttachmentUploadPreview,
- } = useMessageInputContext();
- return (
-
- );
-};
-
-const styles = StyleSheet.create({
- attachmentSeparator: {
- borderBottomWidth: 1,
- marginVertical: 8,
- },
- filesFlatList: { maxHeight: FILE_PREVIEW_HEIGHT * 2.5 + 16 },
- imagesFlatList: {},
- wrapper: {
- paddingTop: 12,
- },
-});
-
-AttachmentUploadPreviewList.displayName =
- 'AttachmentUploadPreviewList{messageInput{attachmentUploadPreviewList}}';
diff --git a/package/src/components/MessageInput/CommandsButton.tsx b/package/src/components/MessageInput/CommandsButton.tsx
deleted file mode 100644
index 7ca093dece..0000000000
--- a/package/src/components/MessageInput/CommandsButton.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React, { useCallback } from 'react';
-import type { GestureResponderEvent, PressableProps } from 'react-native';
-import { Pressable } from 'react-native';
-
-import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { Lightning } from '../../icons/Lightning';
-
-export type CommandsButtonProps = {
- /** Function that opens commands selector. */
- handleOnPress?: PressableProps['onPress'];
-};
-
-export const CommandsButton = (props: CommandsButtonProps) => {
- const { handleOnPress } = props;
- const messageComposer = useMessageComposer();
- const { textComposer } = messageComposer;
-
- const onPressHandler = useCallback(
- async (event: GestureResponderEvent) => {
- if (handleOnPress) {
- handleOnPress(event);
- return;
- }
-
- await textComposer.handleChange({
- selection: {
- end: 1,
- start: 1,
- },
- text: '/',
- });
- },
- [handleOnPress, textComposer],
- );
-
- const {
- theme: {
- colors: { grey },
- messageInput: { commandsButton },
- },
- } = useTheme();
-
- return (
-
-
-
- );
-};
-
-CommandsButton.displayName = 'CommandsButton{messageInput}';
diff --git a/package/src/components/MessageInput/CooldownTimer.tsx b/package/src/components/MessageInput/CooldownTimer.tsx
deleted file mode 100644
index 36c524d2db..0000000000
--- a/package/src/components/MessageInput/CooldownTimer.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from 'react';
-import { StyleSheet, Text, View } from 'react-native';
-
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-
-export type CooldownTimerProps = {
- seconds: number;
-};
-
-const CONTAINER_SIZE = 24;
-const CONTAINER_HORIZONTAL_PADDING = 6;
-const EXTRA_CHARACTER_PADDING = CONTAINER_SIZE - CONTAINER_HORIZONTAL_PADDING * 2;
-
-/**
- * To avoid the container jumping between sizes when there are more
- * than one character in the width of the container since we aren't
- * using a monospaced font.
- */
-const normalizeWidth = (seconds: number) =>
- CONTAINER_SIZE + EXTRA_CHARACTER_PADDING * (`${seconds}`.length - 1);
-
-/**
- * Renders an amount of seconds left for a cooldown to finish.
- *
- * See `useCountdown` for an example of how to set a countdown
- * to use as the source of `seconds`.
- **/
-export const CooldownTimer = (props: CooldownTimerProps) => {
- const { seconds } = props;
- const {
- theme: {
- colors: { black, grey_gainsboro },
- messageInput: {
- cooldownTimer: { container, text },
- },
- },
- } = useTheme();
-
- return (
-
-
- {seconds}
-
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- borderRadius: CONTAINER_SIZE / 2,
- height: CONTAINER_SIZE,
- justifyContent: 'center',
- minWidth: CONTAINER_SIZE,
- paddingHorizontal: CONTAINER_HORIZONTAL_PADDING,
- },
- text: { fontSize: 16, fontWeight: '600' },
-});
diff --git a/package/src/components/MessageInput/InputButtons.tsx b/package/src/components/MessageInput/InputButtons.tsx
deleted file mode 100644
index 0d8e8af2a6..0000000000
--- a/package/src/components/MessageInput/InputButtons.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import React, { useCallback, useEffect, useState } from 'react';
-import { StyleSheet, View } from 'react-native';
-
-import { TextComposerState } from 'stream-chat';
-
-import {
- AttachmentPickerContextValue,
- OwnCapabilitiesContextValue,
- useAttachmentPickerContext,
-} from '../../contexts';
-import { useAttachmentManagerState } from '../../contexts/messageInputContext/hooks/useAttachmentManagerState';
-import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer';
-import {
- MessageInputContextValue,
- useMessageInputContext,
-} from '../../contexts/messageInputContext/MessageInputContext';
-import { useOwnCapabilitiesContext } from '../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { useStateStore } from '../../hooks/useStateStore';
-
-export type InputButtonsProps = Partial;
-
-export type InputButtonsWithContextProps = Pick<
- MessageInputContextValue,
- | 'AttachButton'
- | 'CommandsButton'
- | 'hasCameraPicker'
- | 'hasCommands'
- | 'hasFilePicker'
- | 'hasImagePicker'
- | 'MoreOptionsButton'
- | 'toggleAttachmentPicker'
-> &
- Pick &
- Pick;
-
-const textComposerStateSelector = (state: TextComposerState) => ({
- command: state.command,
- hasText: !!state.text,
-});
-
-export const InputButtonsWithContext = (props: InputButtonsWithContextProps) => {
- const {
- AttachButton,
- CommandsButton,
- hasCameraPicker,
- hasCommands,
- hasFilePicker,
- hasImagePicker,
- MoreOptionsButton,
- uploadFile: ownCapabilitiesUploadFile,
- } = props;
- const { textComposer } = useMessageComposer();
- const { command, hasText } = useStateStore(textComposer.state, textComposerStateSelector);
-
- const [showMoreOptions, setShowMoreOptions] = useState(true);
- const { attachments } = useAttachmentManagerState();
-
- const shouldShowMoreOptions = hasText || attachments.length;
-
- useEffect(() => {
- setShowMoreOptions(!shouldShowMoreOptions);
- }, [shouldShowMoreOptions]);
-
- const {
- theme: {
- messageInput: { attachButtonContainer },
- },
- } = useTheme();
-
- const handleShowMoreOptions = useCallback(() => {
- setShowMoreOptions(true);
- }, [setShowMoreOptions]);
-
- const hasAttachmentUploadCapabilities =
- (hasCameraPicker || hasFilePicker || hasImagePicker) && ownCapabilitiesUploadFile;
- const showCommandsButton = hasCommands && !hasText;
-
- if (command) {
- return null;
- }
-
- if (!hasAttachmentUploadCapabilities && !hasCommands) {
- return null;
- }
-
- return !showMoreOptions ? (
-
- ) : (
- <>
- {hasAttachmentUploadCapabilities ? (
-
-
-
- ) : null}
- {showCommandsButton ? : null}
- >
- );
-};
-
-const areEqual = (
- prevProps: InputButtonsWithContextProps,
- nextProps: InputButtonsWithContextProps,
-) => {
- const {
- hasCameraPicker: prevHasCameraPicker,
- hasCommands: prevHasCommands,
- hasFilePicker: prevHasFilePicker,
- hasImagePicker: prevHasImagePicker,
- selectedPicker: prevSelectedPicker,
- } = prevProps;
-
- const {
- hasCameraPicker: nextHasCameraPicker,
- hasCommands: nextHasCommands,
- hasFilePicker: nextHasFilePicker,
- hasImagePicker: nextHasImagePicker,
- selectedPicker: nextSelectedPicker,
- } = nextProps;
-
- if (prevHasCameraPicker !== nextHasCameraPicker) {
- return false;
- }
-
- if (prevHasImagePicker !== nextHasImagePicker) {
- return false;
- }
-
- if (prevHasFilePicker !== nextHasFilePicker) {
- return false;
- }
-
- if (prevHasCommands !== nextHasCommands) {
- return false;
- }
-
- if (prevSelectedPicker !== nextSelectedPicker) {
- return false;
- }
-
- return true;
-};
-
-const MemoizedInputButtonsWithContext = React.memo(
- InputButtonsWithContext,
- areEqual,
-) as typeof InputButtonsWithContext;
-
-export const InputButtons = (props: InputButtonsProps) => {
- const {
- AttachButton,
- CommandsButton,
- hasCameraPicker,
- hasCommands,
- hasFilePicker,
- hasImagePicker,
- MoreOptionsButton,
- toggleAttachmentPicker,
- } = useMessageInputContext();
- const { selectedPicker } = useAttachmentPickerContext();
- const { uploadFile } = useOwnCapabilitiesContext();
-
- return (
-
- );
-};
-
-const styles = StyleSheet.create({
- attachButtonContainer: { paddingRight: 5 },
-});
diff --git a/package/src/components/MessageInput/MessageComposerLeadingView.tsx b/package/src/components/MessageInput/MessageComposerLeadingView.tsx
new file mode 100644
index 0000000000..e45e927757
--- /dev/null
+++ b/package/src/components/MessageInput/MessageComposerLeadingView.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+
+import Animated, { LinearTransition } from 'react-native-reanimated';
+
+import { InputButtons } from './components/InputButtons';
+import { idleRecordingStateSelector } from './utils/audioRecorderSelectors';
+
+import { useMessageInputContext } from '../../contexts/messageInputContext/MessageInputContext';
+import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { useStateStore } from '../../hooks/useStateStore';
+
+export const MessageComposerLeadingView = () => {
+ const {
+ theme: {
+ messageInput: { inputButtonsContainer },
+ },
+ } = useTheme();
+ const { audioRecorderManager, messageInputFloating } = useMessageInputContext();
+ const { isRecordingStateIdle } = useStateStore(
+ audioRecorderManager.state,
+ idleRecordingStateSelector,
+ );
+
+ return isRecordingStateIdle ? (
+
+
+
+ ) : null;
+};
+
+const styles = StyleSheet.create({
+ inputButtonsContainer: {
+ alignSelf: 'flex-end',
+ },
+ shadow: {
+ elevation: 6,
+
+ shadowColor: 'hsla(0, 0%, 0%, 0.24)',
+ shadowOffset: { height: 4, width: 0 },
+ shadowOpacity: 0.24,
+ shadowRadius: 12,
+ },
+});
diff --git a/package/src/components/MessageInput/MessageComposerTrailingView.tsx b/package/src/components/MessageInput/MessageComposerTrailingView.tsx
new file mode 100644
index 0000000000..0ff4c0341a
--- /dev/null
+++ b/package/src/components/MessageInput/MessageComposerTrailingView.tsx
@@ -0,0 +1,3 @@
+export const MessageComposerTrailingView = () => {
+ return null;
+};
diff --git a/package/src/components/MessageInput/MessageInput.tsx b/package/src/components/MessageInput/MessageInput.tsx
index 7ced90ea08..bb56b3ad83 100644
--- a/package/src/components/MessageInput/MessageInput.tsx
+++ b/package/src/components/MessageInput/MessageInput.tsx
@@ -1,31 +1,36 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useMemo } from 'react';
import { Modal, StyleSheet, TextInput, TextInputProps, View } from 'react-native';
-import {
- Gesture,
- GestureDetector,
- GestureHandlerRootView,
- PanGestureHandlerEventPayload,
-} from 'react-native-gesture-handler';
+import { GestureHandlerRootView } from 'react-native-gesture-handler';
import Animated, {
Extrapolation,
+ FadeIn,
+ FadeOut,
interpolate,
- runOnJS,
+ LinearTransition,
useAnimatedStyle,
useSharedValue,
- withSpring,
} from 'react-native-reanimated';
-import { type MessageComposerState, type TextComposerState, type UserResponse } from 'stream-chat';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { type UserResponse } from 'stream-chat';
+
+import { MicPositionProvider } from './contexts/MicPositionContext';
+import { MessageComposerLeadingView } from './MessageComposerLeadingView';
+import { MessageComposerTrailingView } from './MessageComposerTrailingView';
+import { MessageInputHeaderView } from './MessageInputHeaderView';
+import { MessageInputLeadingView } from './MessageInputLeadingView';
+import { MessageInputTrailingView } from './MessageInputTrailingView';
-import { useAudioController } from './hooks/useAudioController';
-import { useCountdown } from './hooks/useCountdown';
+import { audioRecorderSelector } from './utils/audioRecorderSelectors';
-import { ChatContextValue, useChatContext, useOwnCapabilitiesContext } from '../../contexts';
import {
- AttachmentPickerContextValue,
+ ChatContextValue,
useAttachmentPickerContext,
-} from '../../contexts/attachmentPickerContext/AttachmentPickerContext';
+ useChatContext,
+ useOwnCapabilitiesContext,
+} from '../../contexts';
import {
ChannelContextValue,
useChannelContext,
@@ -34,9 +39,7 @@ import {
MessageComposerAPIContextValue,
useMessageComposerAPIContext,
} from '../../contexts/messageComposerContext/MessageComposerAPIContext';
-import { useAttachmentManagerState } from '../../contexts/messageInputContext/hooks/useAttachmentManagerState';
import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer';
-import { useMessageComposerHasSendableData } from '../../contexts/messageInputContext/hooks/useMessageComposerHasSendableData';
import {
MessageInputContextValue,
useMessageInputContext,
@@ -52,87 +55,114 @@ import {
useTranslationContext,
} from '../../contexts/translationContext/TranslationContext';
+import { useAttachmentPickerState } from '../../hooks/useAttachmentPickerState';
+import { useKeyboardVisibility } from '../../hooks/useKeyboardVisibility';
import { useStateStore } from '../../hooks/useStateStore';
-import { isAudioRecorderAvailable, NativeHandlers } from '../../native';
-import { AIStates, useAIState } from '../AITypingIndicatorView';
+import { AudioRecorderManagerState } from '../../state-store/audio-recorder-manager';
+import { MessageInputHeightState } from '../../state-store/message-input-height-store';
+import { primitives } from '../../theme';
import { AutoCompleteInput } from '../AutoCompleteInput/AutoCompleteInput';
import { CreatePoll } from '../Poll/CreatePollContent';
import { SafeAreaViewWrapper } from '../UIComponents/SafeAreaViewWrapper';
-const styles = StyleSheet.create({
- attachmentSeparator: {
- borderBottomWidth: 1,
- marginBottom: 10,
- },
- autoCompleteInputContainer: {
- alignItems: 'center',
- flexDirection: 'row',
- },
- composerContainer: {
- alignItems: 'center',
- flexDirection: 'row',
- justifyContent: 'space-between',
- },
- container: {
- borderTopWidth: 1,
- padding: 10,
- },
- inputBoxContainer: {
- borderRadius: 20,
- borderWidth: 1,
- flex: 1,
- marginHorizontal: 10,
- },
- micButtonContainer: {},
- optionsContainer: {
- flexDirection: 'row',
- },
- replyContainer: { paddingBottom: 0, paddingHorizontal: 8, paddingTop: 8 },
- sendButtonContainer: {},
- suggestionsListContainer: {
- position: 'absolute',
- width: '100%',
- },
-});
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: primitives.spacingXs,
+ justifyContent: 'space-between',
+ },
+ contentContainer: {
+ gap: primitives.spacingXxs,
+ overflow: 'hidden',
+ paddingHorizontal: primitives.spacingXs,
+ },
+ floatingWrapper: {
+ left: 0,
+ position: 'absolute',
+ right: 0,
+ },
+ giphyContainer: {
+ padding: primitives.spacingXs,
+ },
+ inputBoxContainer: {
+ flex: 1,
+ },
+ inputBoxWrapper: {
+ borderRadius: 24,
+ borderWidth: 1,
+ flex: 1,
+ flexDirection: 'row',
+ backgroundColor: semantics.composerBg,
+ borderColor: semantics.borderCoreDefault,
+ },
+ inputButtonsContainer: {
+ alignSelf: 'flex-end',
+ },
+ inputContainer: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ micButtonContainer: {},
+ outputButtonsContainer: {
+ alignSelf: 'flex-end',
+ padding: primitives.spacingXs,
+ },
+ shadow: {
+ elevation: 6,
+
+ shadowColor: 'hsla(0, 0%, 0%, 0.24)',
+ shadowOffset: { height: 4, width: 0 },
+ shadowOpacity: 0.24,
+ shadowRadius: 12,
+ },
+ suggestionsListContainer: {
+ position: 'absolute',
+ width: '100%',
+ },
+ wrapper: {
+ paddingHorizontal: primitives.spacingMd,
+ paddingTop: primitives.spacingMd,
+ },
+ audioLockIndicatorWrapper: {
+ position: 'absolute',
+ right: primitives.spacingMd,
+ padding: 4,
+ },
+ });
+ }, [semantics]);
+};
-type MessageInputPropsWithContext = Pick<
- AttachmentPickerContextValue,
- 'bottomInset' | 'disableAttachmentPicker' | 'selectedPicker'
-> &
- Pick &
+type MessageInputPropsWithContext = Pick &
Pick &
Pick<
MessageInputContextValue,
+ | 'audioRecorderManager'
| 'additionalTextInputProps'
| 'audioRecordingEnabled'
| 'asyncMessagesLockDistance'
| 'asyncMessagesMinimumPressDuration'
| 'asyncMessagesSlideToCancelDistance'
| 'asyncMessagesMultiSendEnabled'
- | 'attachmentPickerBottomSheetHeight'
- | 'AttachmentPickerSelectionBar'
- | 'attachmentSelectionBarHeight'
| 'AttachmentUploadPreviewList'
| 'AudioRecorder'
| 'AudioRecordingInProgress'
| 'AudioRecordingLockIndicator'
| 'AudioRecordingPreview'
| 'AutoCompleteSuggestionList'
- | 'cooldownEndsAt'
- | 'CooldownTimer'
| 'closeAttachmentPicker'
| 'compressImageQuality'
| 'Input'
| 'inputBoxRef'
| 'InputButtons'
- | 'InputEditingStateHeader'
- | 'CameraSelectorIcon'
- | 'CreatePollIcon'
- | 'FileSelectorIcon'
- | 'ImageSelectorIcon'
- | 'VideoRecorderSelectorIcon'
- | 'CommandInput'
- | 'InputReplyStateHeader'
+ | 'messageInputFloating'
+ | 'messageInputHeightStore'
| 'SendButton'
| 'ShowThreadMessageInChannelButton'
| 'StartAudioRecordingButton'
@@ -142,108 +172,84 @@ type MessageInputPropsWithContext = Pick<
| 'showPollCreationDialog'
| 'sendMessage'
| 'CreatePollContent'
+ | 'createPollOptionGap'
| 'StopMessageStreamingButton'
> &
Pick &
Pick &
- Pick & {
+ Pick &
+ Pick & {
editing: boolean;
+ isKeyboardVisible: boolean;
TextInputComponent?: React.ComponentType<
TextInputProps & {
ref: React.Ref | undefined;
}
>;
+ isRecordingStateIdle?: boolean;
+ recordingStatus?: string;
};
-const textComposerStateSelector = (state: TextComposerState) => ({
- command: state.command,
- hasText: !!state.text,
- mentionedUsers: state.mentionedUsers,
- suggestions: state.suggestions,
-});
-
-const messageComposerStateStoreSelector = (state: MessageComposerState) => ({
- quotedMessage: state.quotedMessage,
+const messageInputHeightStoreSelector = (state: MessageInputHeightState) => ({
+ height: state.height,
});
const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
const {
- AttachmentPickerSelectionBar,
- attachmentPickerBottomSheetHeight,
- attachmentSelectionBarHeight,
- bottomInset,
- selectedPicker,
-
additionalTextInputProps,
asyncMessagesLockDistance,
- asyncMessagesMinimumPressDuration,
- asyncMessagesMultiSendEnabled,
asyncMessagesSlideToCancelDistance,
- AttachmentUploadPreviewList,
AudioRecorder,
- audioRecordingEnabled,
AudioRecordingInProgress,
AudioRecordingLockIndicator,
AudioRecordingPreview,
AutoCompleteSuggestionList,
- channel,
closeAttachmentPicker,
closePollCreationDialog,
- cooldownEndsAt,
- CooldownTimer,
CreatePollContent,
- disableAttachmentPicker,
+ createPollOptionGap,
editing,
+ messageInputFloating,
+ messageInputHeightStore,
Input,
inputBoxRef,
- InputButtons,
- InputEditingStateHeader,
- CommandInput,
- InputReplyStateHeader,
- isOnline,
+ isKeyboardVisible,
members,
- Reply,
threadList,
- SendButton,
sendMessage,
showPollCreationDialog,
ShowThreadMessageInChannelButton,
- StartAudioRecordingButton,
- StopMessageStreamingButton,
TextInputComponent,
watchers,
+ micLocked,
+ isRecordingStateIdle,
+ recordingStatus,
} = props;
+ const styles = useStyles();
+ const { selectedPicker } = useAttachmentPickerState();
+ const { attachmentPickerBottomSheetHeight, bottomInset } = useAttachmentPickerContext();
const messageComposer = useMessageComposer();
- const { textComposer } = messageComposer;
- const { command, hasText } = useStateStore(textComposer.state, textComposerStateSelector);
- const { quotedMessage } = useStateStore(messageComposer.state, messageComposerStateStoreSelector);
- const { attachments } = useAttachmentManagerState();
- const hasSendableData = useMessageComposerHasSendableData();
- const [height, setHeight] = useState(0);
+ const { height } = useStateStore(messageInputHeightStore.store, messageInputHeightStoreSelector);
const {
theme: {
- colors: { border, grey_whisper, white, white_smoke },
+ semantics,
messageInput: {
- attachmentSelectionBar,
- autoCompleteInputContainer,
- composerContainer,
container,
+ floatingWrapper,
focusedInputBoxContainer,
inputBoxContainer,
- micButtonContainer,
- optionsContainer,
- replyContainer,
- sendButtonContainer,
+ inputBoxWrapper,
+ inputContainer,
+ inputFloatingContainer,
suggestionsListContainer: { container: suggestionListContainer },
+ wrapper,
},
},
} = useTheme();
- const { seconds: cooldownRemainingSeconds } = useCountdown(cooldownEndsAt);
-
// Close the attachment picker state when the component unmounts
useEffect(
() => () => {
@@ -308,90 +314,11 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
const isFocused = inputBoxRef.current?.isFocused();
- const {
- deleteVoiceRecording,
- micLocked,
- onVoicePlayerPlayPause,
- paused,
- permissionsGranted,
- position,
- progress,
- recording,
- recordingDuration,
- recordingStatus,
- setMicLocked,
- startVoiceRecording,
- stopVoiceRecording,
- uploadVoiceRecording,
- waveformData,
- } = useAudioController();
-
- const asyncAudioEnabled = audioRecordingEnabled && isAudioRecorderAvailable();
- const showSendingButton = hasText || attachments.length || command;
-
- const isSendingButtonVisible = useMemo(() => {
- return asyncAudioEnabled ? showSendingButton && !recording : true;
- }, [asyncAudioEnabled, recording, showSendingButton]);
-
const micPositionX = useSharedValue(0);
const micPositionY = useSharedValue(0);
const X_AXIS_POSITION = -asyncMessagesSlideToCancelDistance;
const Y_AXIS_POSITION = -asyncMessagesLockDistance;
- const resetAudioRecording = async () => {
- await deleteVoiceRecording();
- };
-
- const micLockHandler = () => {
- setMicLocked(true);
- NativeHandlers.triggerHaptic('impactMedium');
- };
-
- const panGestureMic = Gesture.Pan()
- .activateAfterLongPress(asyncMessagesMinimumPressDuration + 100)
- .onChange((event: PanGestureHandlerEventPayload) => {
- const newPositionX = event.translationX;
- const newPositionY = event.translationY;
-
- if (newPositionX <= 0 && newPositionX >= X_AXIS_POSITION) {
- micPositionX.value = newPositionX;
- }
- if (newPositionY <= 0 && newPositionY >= Y_AXIS_POSITION) {
- micPositionY.value = newPositionY;
- }
- })
- .onEnd(() => {
- const belowThresholdY = micPositionY.value > Y_AXIS_POSITION / 2;
- const belowThresholdX = micPositionX.value > X_AXIS_POSITION / 2;
-
- if (belowThresholdY && belowThresholdX) {
- micPositionY.value = withSpring(0);
- micPositionX.value = withSpring(0);
- if (recordingStatus === 'recording') {
- runOnJS(uploadVoiceRecording)(asyncMessagesMultiSendEnabled);
- }
- return;
- }
-
- if (!belowThresholdY) {
- micPositionY.value = withSpring(Y_AXIS_POSITION);
- runOnJS(micLockHandler)();
- }
-
- if (!belowThresholdX) {
- micPositionX.value = withSpring(X_AXIS_POSITION);
- runOnJS(resetAudioRecording)();
- }
-
- micPositionX.value = 0;
- micPositionY.value = 0;
- })
- .onStart(() => {
- micPositionX.value = 0;
- micPositionY.value = 0;
- runOnJS(setMicLocked)(false);
- });
-
const lockIndicatorAnimatedStyle = useAnimatedStyle(() => ({
transform: [
{
@@ -404,172 +331,127 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
},
],
}));
- const micButttonAnimatedStyle = useAnimatedStyle(() => ({
- opacity: interpolate(micPositionX.value, [0, X_AXIS_POSITION], [1, 0], Extrapolation.CLAMP),
- transform: [{ translateX: micPositionX.value }, { translateY: micPositionY.value }],
- }));
const slideToCancelAnimatedStyle = useAnimatedStyle(() => ({
opacity: interpolate(micPositionX.value, [0, X_AXIS_POSITION], [1, 0], Extrapolation.CLAMP),
- transform: [
- {
- translateX: interpolate(
- micPositionX.value,
- [0, X_AXIS_POSITION],
- [0, X_AXIS_POSITION / 2],
- Extrapolation.CLAMP,
- ),
- },
- ],
}));
+ const { bottom } = useSafeAreaInsets();
- const { aiState } = useAIState(channel);
+ const BOTTOM_OFFSET = isKeyboardVisible || selectedPicker ? 16 : bottom ? bottom : 16;
- const stopGenerating = useCallback(() => channel?.stopAIResponse(), [channel]);
- const shouldDisplayStopAIGeneration =
- [AIStates.Thinking, AIStates.Generating].includes(aiState) && !!StopMessageStreamingButton;
+ const micPositionContextValue = useMemo(
+ () => ({ micPositionX, micPositionY }),
+ [micPositionX, micPositionY],
+ );
return (
- <>
-
+ {/* TODO V9: Think of a better way to do this without so much re-layouting. */}
+ setHeight(newHeight)}
- style={[styles.container, { backgroundColor: white, borderColor: border }, container]}
+ }) =>
+ messageInputHeightStore.setHeight(
+ messageInputFloating ? newHeight + BOTTOM_OFFSET : newHeight,
+ )
+ } // BOTTOM OFFSET is the position of the input from the bottom of the screen
+ style={
+ messageInputFloating
+ ? [styles.wrapper, styles.floatingWrapper, { bottom: BOTTOM_OFFSET }, floatingWrapper]
+ : [
+ styles.wrapper,
+ {
+ borderTopWidth: 1,
+ backgroundColor: semantics.composerBg,
+ borderColor: semantics.borderCoreDefault,
+ // paddingBottom: BOTTOM_OFFSET,
+ paddingBottom:
+ selectedPicker && !isKeyboardVisible
+ ? attachmentPickerBottomSheetHeight - bottomInset + BOTTOM_OFFSET
+ : BOTTOM_OFFSET,
+ },
+ wrapper,
+ ]
+ }
>
- {editing &&