diff --git a/.github/actions/build-android/action.yml b/.github/actions/build-android/action.yml
index 86c531d52c9..874a0da82e3 100644
--- a/.github/actions/build-android/action.yml
+++ b/.github/actions/build-android/action.yml
@@ -45,17 +45,8 @@ runs:
- name: Set gradle.properties
working-directory: android
run: |
- echo "" > ./gradle.properties
- echo -e "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8" >> ./gradle.properties
- echo -e "android.useAndroidX=true" >> ./gradle.properties
- echo -e "android.enableJetifier=true" >> ./gradle.properties
- echo -e "android.jetifier.ignorelist=bcprov-jdk18on" >> ./gradle.properties
- echo -e "reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64" >> ./gradle.properties
- echo -e "newArchEnabled=true" >> ./gradle.properties
- echo -e "hermesEnabled=true" >> ./gradle.properties
- echo -e "edgeToEdgeEnabled=true" >> ./gradle.properties
+ printf '\n' >> ./gradle.properties
echo -e "VERSIONCODE=${{ inputs.BUILD_VERSION }}" >> ./gradle.properties
- echo -e "APPLICATION_ID=chat.rocket.android" >> ./gradle.properties
echo -e "BugsnagAPIKey=${{ inputs.BUGSNAG_KEY_OFFICIAL }}" >> ./gradle.properties
echo -e "KEYSTORE=${{ inputs.KEYSTORE_OFFICIAL }}" >> ./gradle.properties
echo -e 'KEYSTORE_PASSWORD=${{ inputs.KEYSTORE_OFFICIAL_PASSWORD }}' >> ./gradle.properties
@@ -154,7 +145,7 @@ runs:
- name: Upload sourcemaps/NDK symbols to Bugsnag
run: |
pnpm bugsnag:upload-android --variant release --app-manifest android/app/build/intermediates/merged_manifests/release/processReleaseManifest/AndroidManifest.xml
- pnpm exec bugsnag-cli upload android-aab android/app/build/outputs/bundle/release/app-release.aab
+ pnpm exec bugsnag-cli upload android-aab --exclude "**/libratex_ffi.so*" android/app/build/outputs/bundle/release/app-release.aab
shell: bash
- name: Upload AAB
diff --git a/.github/actions/build-ios/action.yml b/.github/actions/build-ios/action.yml
index ddb2133bd5d..3b718269fb6 100644
--- a/.github/actions/build-ios/action.yml
+++ b/.github/actions/build-ios/action.yml
@@ -44,7 +44,7 @@ runs:
- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
- xcode-version: '26.2.0'
+ xcode-version: '26.6'
- name: Set up Ruby
uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1.302.0
@@ -58,9 +58,9 @@ runs:
path: |
~/.pods
ios/Pods
- key: pods-v1-{{ checksum "ios/Podfile.lock" }}
+ key: pods-v2-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: |
- pods-v1-
+ pods-v2-
- name: pod install
run: |
diff --git a/.github/actions/upload-ios/action.yml b/.github/actions/upload-ios/action.yml
index 781851ac8d3..2a5e75d8eb8 100644
--- a/.github/actions/upload-ios/action.yml
+++ b/.github/actions/upload-ios/action.yml
@@ -57,14 +57,14 @@ runs:
path: |
~/.pods
ios/Pods
- key: pods-v1-{{ checksum "ios/Podfile.lock" }}
+ key: pods-v2-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: |
- pods-v1-
+ pods-v2-
- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
- xcode-version: '26.2.0'
+ xcode-version: '26.6'
- name: Set up Ruby
uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1.302.0
diff --git a/.github/workflows/e2e-build-android.yml b/.github/workflows/e2e-build-android.yml
index 93ddf48c31c..89455a5af67 100644
--- a/.github/workflows/e2e-build-android.yml
+++ b/.github/workflows/e2e-build-android.yml
@@ -103,16 +103,8 @@ jobs:
- name: Set gradle.properties
working-directory: android
run: |
- echo "" > ./gradle.properties
- echo -e "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8" >> ./gradle.properties
- echo -e "android.useAndroidX=true" >> ./gradle.properties
- echo -e "android.enableJetifier=true" >> ./gradle.properties
- echo -e "android.jetifier.ignorelist=bcprov-jdk18on" >> ./gradle.properties
+ printf '\n' >> ./gradle.properties
echo -e "reactNativeArchitectures=arm64-v8a,x86_64" >> ./gradle.properties
- echo -e "APPLICATION_ID=chat.rocket.android" >> ./gradle.properties
- echo -e "newArchEnabled=true" >> ./gradle.properties
- echo -e "hermesEnabled=true" >> ./gradle.properties
- echo -e "edgeToEdgeEnabled=true" >> ./gradle.properties
echo -e "VERSIONCODE=999999" >> ./gradle.properties
echo -e "KEYSTORE=release.keystore" >> ./gradle.properties
echo -e 'KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_OFFICIAL_PASSWORD }}' >> ./gradle.properties
diff --git a/.github/workflows/e2e-build-ios.yml b/.github/workflows/e2e-build-ios.yml
index f290a6d4228..f6f787074f2 100644
--- a/.github/workflows/e2e-build-ios.yml
+++ b/.github/workflows/e2e-build-ios.yml
@@ -68,7 +68,7 @@ jobs:
- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
- xcode-version: '26.2.0'
+ xcode-version: '26.6'
- name: Set up Ruby
uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1.302.0
diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml
index 2372337e384..36a6c5ce89e 100644
--- a/.github/workflows/eslint.yml
+++ b/.github/workflows/eslint.yml
@@ -19,10 +19,13 @@ jobs:
uses: ./.github/actions/setup-node
- name: Run Oxlint
- run: pnpm lint
+ run: pnpm exec oxlint
+
+ - name: Run TypeScript
+ run: pnpm exec tsc
- name: Run Tests
- run: pnpm test --runInBand
+ run: pnpm test
- name: Validate sniffler test-map freshness
run: node .github/scripts/validate-test-map.js
diff --git a/.github/workflows/maestro-android.yml b/.github/workflows/maestro-android.yml
index c6cf9e0c462..203b6684544 100644
--- a/.github/workflows/maestro-android.yml
+++ b/.github/workflows/maestro-android.yml
@@ -204,4 +204,5 @@ jobs:
~/.maestro/tests/**/*
maestro-report.xml
maestro-rerun-round-*.xml
+ *.mp4
retention-days: 7
diff --git a/.github/workflows/maestro-ios.yml b/.github/workflows/maestro-ios.yml
index 3cf48779f52..e26337da799 100644
--- a/.github/workflows/maestro-ios.yml
+++ b/.github/workflows/maestro-ios.yml
@@ -26,7 +26,7 @@ jobs:
# Match the Xcode version e2e-build-ios.yml builds with
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
- xcode-version: '26.2.0'
+ xcode-version: '26.6'
- name: Configure Simulator
run: |
@@ -144,4 +144,5 @@ jobs:
~/.maestro/tests/**/*
maestro-report.xml
maestro-rerun-round-*.xml
+ *.mp4
retention-days: 7
diff --git a/.github/workflows/organize_translations.yml b/.github/workflows/organize_translations.yml
index 4e2dad51c2a..12cb600686b 100644
--- a/.github/workflows/organize_translations.yml
+++ b/.github/workflows/organize_translations.yml
@@ -20,7 +20,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
- node-version: 18
+ node-version: 22
- name: Run script to organize JSON keys
run: node scripts/organize-translations.js
diff --git a/.maestro/helpers/assert-room-title.yaml b/.maestro/helpers/assert-room-title.yaml
new file mode 100644
index 00000000000..183c4e2556a
--- /dev/null
+++ b/.maestro/helpers/assert-room-title.yaml
@@ -0,0 +1,23 @@
+appId: ${APP_ID}
+name: Assert room title
+tags:
+ - 'util'
+
+---
+- runFlow:
+ when:
+ platform: Android
+ commands:
+ - extendedWaitUntil:
+ visible:
+ id: 'room-view-title-${ROOM}'
+ timeout: 60000
+
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ visible:
+ text: '${ROOM}'
+ timeout: 60000
diff --git a/.maestro/helpers/clear-search-field.yaml b/.maestro/helpers/clear-search-field.yaml
new file mode 100644
index 00000000000..3ee551c0df0
--- /dev/null
+++ b/.maestro/helpers/clear-search-field.yaml
@@ -0,0 +1,18 @@
+appId: ${APP_ID}
+name: Clear search field
+tags:
+ - 'util'
+---
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - tapOn:
+ id: 'magnifyingglass'
+ - eraseText
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - tapOn:
+ id: clear-text-input
diff --git a/.maestro/helpers/close-search.yaml b/.maestro/helpers/close-search.yaml
new file mode 100644
index 00000000000..db6560964fd
--- /dev/null
+++ b/.maestro/helpers/close-search.yaml
@@ -0,0 +1,13 @@
+appId: ${APP_ID}
+name: Close search
+tags:
+ - 'util'
+---
+- runFlow:
+ when:
+ platform: iOS
+ visible:
+ text: 'Close'
+ commands:
+ - tapOn:
+ text: 'Close'
diff --git a/.maestro/helpers/go-back.yaml b/.maestro/helpers/go-back.yaml
index 667f678c475..81fd69def37 100644
--- a/.maestro/helpers/go-back.yaml
+++ b/.maestro/helpers/go-back.yaml
@@ -6,16 +6,15 @@ tags:
---
- runFlow:
when:
- visible:
- id: 'custom-header-back'
+ platform: Android
commands:
- tapOn:
- id: 'custom-header-back'
+ id: '(custom-)?header-back'
- runFlow:
when:
- visible:
- id: 'header-back'
+ platform: iOS
commands:
- tapOn:
- id: 'header-back'
+ text: 'Back(, .*)?'
+ retryTapIfNoChange: true
diff --git a/.maestro/helpers/login-with-deeplink.yaml b/.maestro/helpers/login-with-deeplink.yaml
index dba619eeea9..346b4659cd8 100644
--- a/.maestro/helpers/login-with-deeplink.yaml
+++ b/.maestro/helpers/login-with-deeplink.yaml
@@ -36,6 +36,11 @@ tags:
visible: ".*isn't responding.*"
commands:
- tapOn: 'Wait'
+ - runFlow:
+ when:
+ visible: 'Sign in to this workspace?'
+ commands:
+ - tapOn: '(?i)login'
- extendedWaitUntil:
visible:
id: 'rooms-list-view'
diff --git a/.maestro/helpers/logout.yaml b/.maestro/helpers/logout.yaml
index 70ddc19cfb7..b19b9eba4ee 100644
--- a/.maestro/helpers/logout.yaml
+++ b/.maestro/helpers/logout.yaml
@@ -14,12 +14,22 @@ tags:
- runFlow:
when:
+ platform: Android
visible:
id: 'rooms-list-view-sidebar'
commands:
- tapOn:
id: 'rooms-list-view-sidebar'
+- runFlow:
+ when:
+ platform: iOS
+ visible:
+ text: 'Menu'
+ commands:
+ - tapOn:
+ text: 'Menu'
+
- runFlow:
when:
platform: Android
diff --git a/.maestro/helpers/navigate-to-recent-room.yaml b/.maestro/helpers/navigate-to-recent-room.yaml
index fdfc1057707..5da07125d33 100644
--- a/.maestro/helpers/navigate-to-recent-room.yaml
+++ b/.maestro/helpers/navigate-to-recent-room.yaml
@@ -8,19 +8,23 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- tapOn:
- id: 'rooms-list-view-search'
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-search-input'
- timeout: 60000
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - tapOn:
+ id: 'rooms-list-view-search'
+ - extendedWaitUntil:
+ visible:
+ id: 'rooms-list-view-search-input'
+ timeout: 60000
- extendedWaitUntil:
visible:
id: 'rooms-list-view-item-${ROOM}'
timeout: 60000
- tapOn:
id: 'rooms-list-view-item-${ROOM}'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${ROOM}'
- timeout: 60000
+- runFlow:
+ file: 'assert-room-title.yaml'
+ env:
+ ROOM: ${ROOM}
diff --git a/.maestro/helpers/navigate-to-room-action.yaml b/.maestro/helpers/navigate-to-room-action.yaml
index 08a0dfeb718..f9367ee7fd3 100644
--- a/.maestro/helpers/navigate-to-room-action.yaml
+++ b/.maestro/helpers/navigate-to-room-action.yaml
@@ -18,13 +18,5 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- assertVisible:
- id: 'room-header'
- index: 0
-- tapOn:
- id: 'room-header'
- index: 0
-- extendedWaitUntil:
- visible:
- id: 'room-actions-view'
- timeout: 60000
+
+- runFlow: './open-room-actions.yaml'
diff --git a/.maestro/helpers/navigate-to-room.yaml b/.maestro/helpers/navigate-to-room.yaml
index e8eba0728b0..af619b5da61 100644
--- a/.maestro/helpers/navigate-to-room.yaml
+++ b/.maestro/helpers/navigate-to-room.yaml
@@ -4,6 +4,7 @@ tags:
- 'util'
---
+- runFlow: './pop-to-rooms-list.yaml'
- waitForAnimationToEnd
- scrollUntilVisible:
element:
@@ -11,7 +12,7 @@ tags:
- waitForAnimationToEnd
- tapOn:
id: 'rooms-list-view-item-${ROOM}'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${ROOM}'
- timeout: 60000
+- runFlow:
+ file: 'assert-room-title.yaml'
+ env:
+ ROOM: ${ROOM}
diff --git a/.maestro/helpers/open-room-actions.yaml b/.maestro/helpers/open-room-actions.yaml
new file mode 100644
index 00000000000..e351514fbfc
--- /dev/null
+++ b/.maestro/helpers/open-room-actions.yaml
@@ -0,0 +1,18 @@
+appId: ${APP_ID}
+name: Open room actions
+tags:
+ - 'util'
+
+---
+- extendedWaitUntil:
+ visible:
+ id: 'room-header'
+ timeout: 60000
+- tapOn:
+ id: 'room-header'
+ index: 0
+ retryTapIfNoChange: true
+- extendedWaitUntil:
+ visible:
+ id: 'room-actions-view'
+ timeout: 60000
diff --git a/.maestro/helpers/open-servers-list.yaml b/.maestro/helpers/open-servers-list.yaml
new file mode 100644
index 00000000000..49052b5455d
--- /dev/null
+++ b/.maestro/helpers/open-servers-list.yaml
@@ -0,0 +1,32 @@
+appId: ${APP_ID}
+name: Open servers list
+tags:
+ - 'util'
+
+---
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ visible:
+ text: 'Menu'
+ timeout: 60000
+ - tapOn:
+ text: '(?!\d{1,2}:\d{2}).+'
+ rightOf:
+ text: 'Menu'
+ leftOf:
+ text: 'Create new channel.*'
+ index: 0
+
+- runFlow:
+ when:
+ platform: Android
+ commands:
+ - extendedWaitUntil:
+ visible:
+ id: 'rooms-list-header-servers-list-button'
+ timeout: 60000
+ - tapOn:
+ id: 'rooms-list-header-servers-list-button'
diff --git a/.maestro/helpers/pop-to-rooms-list.yaml b/.maestro/helpers/pop-to-rooms-list.yaml
new file mode 100644
index 00000000000..1e8fb0abeb0
--- /dev/null
+++ b/.maestro/helpers/pop-to-rooms-list.yaml
@@ -0,0 +1,30 @@
+appId: ${APP_ID}
+name: Pop to rooms list
+tags:
+ - 'util'
+
+---
+- repeat:
+ times: 4
+ while:
+ notVisible:
+ id: 'rooms-list-view'
+ commands:
+ - runFlow:
+ when:
+ platform: Android
+ visible:
+ id: '(custom-)?header-back'
+ commands:
+ - tapOn:
+ id: '(custom-)?header-back'
+ - runFlow:
+ when:
+ platform: iOS
+ visible:
+ text: 'Back(, .*)?'
+ commands:
+ - tapOn:
+ text: 'Back(, .*)?'
+ - waitForAnimationToEnd:
+ timeout: 5000
diff --git a/.maestro/helpers/search-and-navigate-room.yaml b/.maestro/helpers/search-and-navigate-room.yaml
index ba8479a1c02..da3bdf0b8e1 100644
--- a/.maestro/helpers/search-and-navigate-room.yaml
+++ b/.maestro/helpers/search-and-navigate-room.yaml
@@ -3,7 +3,14 @@ name: Search and navigate room
tags:
- 'util'
---
+- extendedWaitUntil:
+ visible:
+ id: 'rooms-list-view'
+ timeout: 60000
- runFlow:
+ when:
+ notVisible:
+ id: 'rooms-list-view-item-${ROOM}'
file: '../helpers/search-room.yaml'
env:
ROOM: ${ROOM}
diff --git a/.maestro/helpers/search-room.yaml b/.maestro/helpers/search-room.yaml
index 6314ba6f9af..17dfdc0668f 100644
--- a/.maestro/helpers/search-room.yaml
+++ b/.maestro/helpers/search-room.yaml
@@ -3,18 +3,31 @@ name: Search room
tags:
- 'util'
---
+- runFlow: './pop-to-rooms-list.yaml'
- extendedWaitUntil:
visible:
id: 'rooms-list-view'
timeout: 60000
- waitForAnimationToEnd:
timeout: 5000
-- tapOn:
- id: 'rooms-list-view-search'
-- tapOn:
- id: 'rooms-list-view-search-input'
-- inputText:
- text: ${ROOM}
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - tapOn:
+ id: 'magnifyingglass'
+ - inputText:
+ text: ${ROOM}
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - tapOn:
+ id: 'rooms-list-view-search'
+ - tapOn:
+ id: 'rooms-list-view-search-input'
+ - inputText:
+ text: ${ROOM}
- extendedWaitUntil:
visible:
id: 'rooms-list-view-item-${ROOM}'
diff --git a/.maestro/helpers/select-header-filter.yaml b/.maestro/helpers/select-header-filter.yaml
new file mode 100644
index 00000000000..7a577c1cd3d
--- /dev/null
+++ b/.maestro/helpers/select-header-filter.yaml
@@ -0,0 +1,37 @@
+appId: ${APP_ID}
+name: Select header filter
+tags:
+ - 'util'
+---
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ visible:
+ text: 'Filter'
+ timeout: 60000
+ - tapOn:
+ text: 'Filter'
+ - extendedWaitUntil:
+ visible:
+ text: ${IOS_OPTION}
+ timeout: 60000
+ - tapOn:
+ text: ${IOS_OPTION}
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - extendedWaitUntil:
+ visible:
+ id: ${ANDROID_FILTER_ID}
+ timeout: 60000
+ - tapOn:
+ id: ${ANDROID_FILTER_ID}
+ - extendedWaitUntil:
+ visible:
+ id: ${ANDROID_OPTION_ID}
+ timeout: 60000
+ - tapOn:
+ id: ${ANDROID_OPTION_ID}
diff --git a/.maestro/helpers/tap-header-item.yaml b/.maestro/helpers/tap-header-item.yaml
new file mode 100644
index 00000000000..963ba9f797c
--- /dev/null
+++ b/.maestro/helpers/tap-header-item.yaml
@@ -0,0 +1,21 @@
+appId: ${APP_ID}
+name: Tap header item
+tags:
+ - 'util'
+
+---
+# iOS native header items (unstable_headerLeftItems/RightItems) carry no accessibilityIdentifier,
+# only a label. Android keeps the JS-rendered header, matched by testID as before.
+- runFlow:
+ when:
+ platform: Android
+ commands:
+ - tapOn:
+ id: ${ANDROID_ID}
+
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - tapOn:
+ text: ${IOS_LABEL}
diff --git a/.maestro/helpers/tap-search-field.yaml b/.maestro/helpers/tap-search-field.yaml
new file mode 100644
index 00000000000..a2f14c3f691
--- /dev/null
+++ b/.maestro/helpers/tap-search-field.yaml
@@ -0,0 +1,33 @@
+appId: ${APP_ID}
+name: Tap search field
+tags:
+ - 'util'
+---
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - runFlow:
+ when:
+ notVisible:
+ id: 'magnifyingglass'
+ commands:
+ - swipe:
+ start: 50%, 30%
+ end: 50%, 60%
+ - extendedWaitUntil:
+ visible:
+ id: 'magnifyingglass'
+ timeout: 60000
+ - tapOn:
+ id: 'magnifyingglass'
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - extendedWaitUntil:
+ visible:
+ id: ${SEARCH_ID}
+ timeout: 60000
+ - tapOn:
+ id: ${SEARCH_ID}
diff --git a/.maestro/tests/accessibilityAndAppearance/ToastsAndDialogs.yml b/.maestro/tests/accessibilityAndAppearance/ToastsAndDialogs.yml
index a2bf50b79a6..17c2280bfdc 100644
--- a/.maestro/tests/accessibilityAndAppearance/ToastsAndDialogs.yml
+++ b/.maestro/tests/accessibilityAndAppearance/ToastsAndDialogs.yml
@@ -18,11 +18,27 @@ tags:
# Show alerts as Toasts
- tapOn: 'Menu'
- tapOn: 'Accessibility & appearance'
-- tapOn: '.*Show alerts as.*'
-- assertVisible: 'Toasts. Dismissed automatically. Checked'
-- assertVisible: 'Dialogs. Require manual dismissal. Unchecked'
-- tapOn:
- id: 'action-sheet-handle'
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - tapOn: 'Toasts'
+ - assertVisible:
+ text: 'Toasts'
+ selected: true
+ - assertVisible:
+ text: 'Dialogs'
+ selected: false
+ - tapOn: 'Toasts'
+- runFlow:
+ when:
+ platform: Android
+ commands:
+ - tapOn: '.*Show alerts as.*'
+ - assertVisible: 'Toasts. Dismissed automatically. Checked'
+ - assertVisible: 'Dialogs. Require manual dismissal. Unchecked'
+ - tapOn:
+ id: 'action-sheet-handle'
- waitForAnimationToEnd:
timeout: 5000
- tapOn: 'Menu'
@@ -37,9 +53,23 @@ tags:
# Show alerts as Dialogs
- tapOn: 'Accessibility & appearance'
-- tapOn: '.*Show alerts as.*'
-- assertVisible: 'Toasts. Dismissed automatically. Checked'
-- tapOn: 'Dialogs. Require manual dismissal. Unchecked'
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - tapOn: 'Toasts'
+ - assertVisible:
+ text: 'Toasts'
+ selected: true
+ - tapOn: 'Dialogs'
+ - assertVisible: 'Dialogs'
+- runFlow:
+ when:
+ platform: Android
+ commands:
+ - tapOn: '.*Show alerts as.*'
+ - assertVisible: 'Toasts. Dismissed automatically. Checked'
+ - tapOn: 'Dialogs. Require manual dismissal. Unchecked'
- tapOn: 'Menu'
- tapOn: 'Edit status'
- tapOn:
diff --git a/.maestro/tests/assorted/accessibility-and-appearance.yaml b/.maestro/tests/assorted/accessibility-and-appearance.yaml
index 04a79a29fcd..dec998f84f7 100644
--- a/.maestro/tests/assorted/accessibility-and-appearance.yaml
+++ b/.maestro/tests/assorted/accessibility-and-appearance.yaml
@@ -95,8 +95,7 @@ tags:
timeout: 60000
# should disable mentions with @ symbol
-- tapOn:
- id: header-back
+- runFlow: '../../helpers/go-back.yaml'
- runFlow:
file: './utils/go-to-accessibility-appearance.yaml'
@@ -134,8 +133,7 @@ tags:
timeout: 60000
# should enable rooms with # symbol
-- tapOn:
- id: header-back
+- runFlow: '../../helpers/go-back.yaml'
- runFlow:
file: './utils/go-to-accessibility-appearance.yaml'
@@ -169,8 +167,7 @@ tags:
timeout: 60000
# should disable mentions with # symbol
-- tapOn:
- id: header-back
+- runFlow: '../../helpers/go-back.yaml'
- runFlow:
file: './utils/go-to-accessibility-appearance.yaml'
diff --git a/.maestro/tests/assorted/admin-panel.yaml b/.maestro/tests/assorted/admin-panel.yaml
index e783063497f..7380dfca90a 100644
--- a/.maestro/tests/assorted/admin-panel.yaml
+++ b/.maestro/tests/assorted/admin-panel.yaml
@@ -19,8 +19,11 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-view'
diff --git a/.maestro/tests/assorted/broadcast.yaml b/.maestro/tests/assorted/broadcast.yaml
index 254c6b78b1b..633472aa649 100644
--- a/.maestro/tests/assorted/broadcast.yaml
+++ b/.maestro/tests/assorted/broadcast.yaml
@@ -19,12 +19,11 @@ tags:
PASSWORD: ${output.user.password}
# should create broadcast room
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-create-channel'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-create-channel'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-create-channel'
+ IOS_LABEL: 'Create new channel, team, direct message or discussion'
- extendedWaitUntil:
visible:
id: 'new-message-view'
@@ -39,8 +38,10 @@ tags:
visible:
id: 'select-users-view'
timeout: 60000
-- tapOn:
- id: 'select-users-view-search'
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'select-users-view-search'
- inputText: ${output.otherUser.username}
- extendedWaitUntil:
visible:
@@ -52,6 +53,7 @@ tags:
visible:
id: 'selected-user-${output.otherUser.username}'
timeout: 60000
+- runFlow: '../../helpers/close-search.yaml'
- assertVisible:
id: 'selected-users-view-submit'
- tapOn:
@@ -76,15 +78,11 @@ tags:
timeout: 60000
- tapOn:
id: 'create-channel-submit'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room}'
-- tapOn:
- id: 'room-header'
-- extendedWaitUntil:
- visible:
- id: 'room-actions-view'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room}
+- runFlow: '../../helpers/open-room-actions.yaml'
- tapOn:
id: 'room-actions-info'
- extendedWaitUntil:
@@ -117,10 +115,10 @@ tags:
file: '../../helpers/search-and-navigate-room.yaml'
env:
ROOM: ${output.room}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room}
# should not have message composer
- assertNotVisible:
@@ -139,10 +137,10 @@ tags:
id: 'message-broadcast-reply'
- tapOn:
id: 'message-broadcast-reply'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.user.username}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.user.username}
# should reply broadcasted message
- assertVisible:
@@ -165,6 +163,7 @@ tags:
timeout: 60000
- tapOn:
text: 'Jump to message'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room}'
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room}
diff --git a/.maestro/tests/assorted/change-avatar.yaml b/.maestro/tests/assorted/change-avatar.yaml
index 72285dfc884..087c693591e 100644
--- a/.maestro/tests/assorted/change-avatar.yaml
+++ b/.maestro/tests/assorted/change-avatar.yaml
@@ -16,12 +16,11 @@ tags:
env:
USERNAME: ${output.user.username}
PASSWORD: ${output.user.password}
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-sidebar'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-view'
@@ -56,8 +55,7 @@ tags:
id: 'reset-avatar-suggestion'
# should appear the discard alert when click the back icon
-- tapOn:
- id: 'header-back'
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
text: '.*Discard changes?.*'
@@ -68,8 +66,7 @@ tags:
timeout: 60000
- tapOn:
text: '.*Cancel.*'
-- tapOn:
- id: 'header-back'
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
text: '.*Discard changes?.*'
diff --git a/.maestro/tests/assorted/changeserver.yaml b/.maestro/tests/assorted/changeserver.yaml
index 897eabe427d..6ae06a0747b 100644
--- a/.maestro/tests/assorted/changeserver.yaml
+++ b/.maestro/tests/assorted/changeserver.yaml
@@ -18,12 +18,7 @@ tags:
PASSWORD: ${output.user.password}
# should open the servers list, have the server add button and create workspace button
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-header-servers-list-button'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-header-servers-list-button'
+- runFlow: '../../helpers/open-servers-list.yaml'
- extendedWaitUntil:
visible:
id: 'rooms-list-header-servers-list'
@@ -57,12 +52,7 @@ tags:
timeout: 60000
# should add server and create new user
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-header-servers-list-button'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-header-servers-list-button'
+- runFlow: '../../helpers/open-servers-list.yaml'
- extendedWaitUntil:
visible:
id: 'rooms-list-header-servers-list'
@@ -107,14 +97,9 @@ tags:
server: ${output.data.alternateServer}
# should change back to main server
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-header-servers-list-button'
- timeout: 60000
- waitForAnimationToEnd:
timeout: 5000
-- tapOn:
- id: 'rooms-list-header-servers-list-button'
+- runFlow: '../../helpers/open-servers-list.yaml'
- extendedWaitUntil:
visible:
id: 'rooms-list-header-servers-list'
@@ -154,12 +139,7 @@ tags:
server: ${output.data.server}
# should test swipe to delete on alternate server
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-header-servers-list-button'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-header-servers-list-button'
+- runFlow: '../../helpers/open-servers-list.yaml'
- extendedWaitUntil:
visible:
id: 'server-item-${output.data.alternateServer}'
@@ -198,12 +178,7 @@ tags:
# verify alternate server is deleted
- waitForAnimationToEnd:
timeout: 500
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-header-servers-list-button'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-header-servers-list-button'
+- runFlow: '../../helpers/open-servers-list.yaml'
- extendedWaitUntil:
notVisible:
id: 'server-item-${output.data.alternateServer}'
diff --git a/.maestro/tests/assorted/channel-deeplink.yaml b/.maestro/tests/assorted/channel-deeplink.yaml
index 6edcc747621..5f1d4ea0110 100644
--- a/.maestro/tests/assorted/channel-deeplink.yaml
+++ b/.maestro/tests/assorted/channel-deeplink.yaml
@@ -24,10 +24,10 @@ tags:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'path=group/', output.group.name)}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.group.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.group.name}
# open group using id
- killApp
@@ -35,10 +35,10 @@ tags:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'path=group/', output.group._id)}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.group.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.group.name}
# open channel using name
- killApp
@@ -46,10 +46,10 @@ tags:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'path=channel/', output.channel.name)}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.channel.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.channel.name}
# open channel using id
- killApp
@@ -57,7 +57,7 @@ tags:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'path=channel/', output.channel._id)}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.channel.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.channel.name}
diff --git a/.maestro/tests/assorted/deeplink.yaml b/.maestro/tests/assorted/deeplink.yaml
index 50cd775b78e..eef191bd36e 100644
--- a/.maestro/tests/assorted/deeplink.yaml
+++ b/.maestro/tests/assorted/deeplink.yaml
@@ -37,10 +37,10 @@ tags:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('auth', output.data.server, 'userId=', output.login.userId, '&token=', output.login.authToken, '&path=group/', output.room.name)}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
@@ -70,10 +70,10 @@ tags:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('auth', output.data.server, 'userId=', output.login.userId, '&token=', output.login.authToken, '&path=group/', output.room.name)}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
@@ -94,10 +94,10 @@ tags:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'path=group/', output.room.name)}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
# should navigate to the thread using path
- stopApp: ${APP_ID}
@@ -105,10 +105,10 @@ tags:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'path=group/', output.room.name, '/thread/', output.result.message._id)}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.threadMessage}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.threadMessage}
# should navigate to the room using rid
- stopApp: ${APP_ID}
@@ -116,10 +116,10 @@ tags:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'rid=', output.room._id)}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
@@ -132,10 +132,10 @@ tags:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'path=group/', output.room.name)}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
# should simulate a tap on a push notification and navigate to the room
# we are skipping this because we can't send push on iOS simulator and for android we have similar test
@@ -146,12 +146,7 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-header-servers-list-button'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-header-servers-list-button'
+- runFlow: '../../helpers/open-servers-list.yaml'
- extendedWaitUntil:
visible:
id: 'server-item-${output.data.alternateServer}'
@@ -167,10 +162,10 @@ tags:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'path=group/', output.room.name)}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
# should add a not existing server and fallback to the previous one
- stopApp: ${APP_ID}
@@ -227,12 +222,7 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-header-servers-list-button'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-header-servers-list-button'
+- runFlow: '../../helpers/open-servers-list.yaml'
- extendedWaitUntil:
visible:
id: 'server-item-${output.data.alternateServer}'
@@ -343,8 +333,18 @@ tags:
visible:
id: 'new-server-view'
timeout: 60000
-- assertNotVisible:
- id: 'room-view-title-${output.room.name}'
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - assertNotVisible:
+ id: 'room-view-title-${output.room.name}'
+- runFlow:
+ when:
+ platform: ios
+ commands:
+ - assertNotVisible:
+ text: '${output.room.name}'
# should show a security confirmation prompt on deep link login and sign in when confirmed
- runFlow:
@@ -376,10 +376,10 @@ tags:
commands:
- tapOn:
text: 'Login'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
diff --git a/.maestro/tests/assorted/delete-server.yaml b/.maestro/tests/assorted/delete-server.yaml
index 7e63bee1ea9..f48a289776d 100644
--- a/.maestro/tests/assorted/delete-server.yaml
+++ b/.maestro/tests/assorted/delete-server.yaml
@@ -22,12 +22,7 @@ tags:
server: ${output.data.server}
# should add server
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-header-servers-list-button'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-header-servers-list-button'
+- runFlow: '../../helpers/open-servers-list.yaml'
- extendedWaitUntil:
visible:
id: 'rooms-list-header-server-add'
@@ -67,12 +62,7 @@ tags:
server: ${output.data.alternateServer}
# should delete server
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-header-servers-list-button'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-header-servers-list-button'
+- runFlow: '../../helpers/open-servers-list.yaml'
- extendedWaitUntil:
visible:
id: 'server-item-${output.data.server}'
@@ -109,12 +99,7 @@ tags:
retryTapIfNoChange: true
- waitForAnimationToEnd:
timeout: 300
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-header-servers-list-button'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-header-servers-list-button'
+- runFlow: '../../helpers/open-servers-list.yaml'
- extendedWaitUntil:
visible:
id: 'rooms-list-header-servers-list'
diff --git a/.maestro/tests/assorted/display-perf.yaml b/.maestro/tests/assorted/display-perf.yaml
index 54927e65a37..bbdaf0b54d7 100644
--- a/.maestro/tests/assorted/display-perf.yaml
+++ b/.maestro/tests/assorted/display-perf.yaml
@@ -30,18 +30,44 @@ tags:
label: 'Should have room item'
# should have create channel button
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-create-channel'
- timeout: 60000
- label: 'Should have create channel button'
+- runFlow:
+ when:
+ platform: Android
+ commands:
+ - extendedWaitUntil:
+ visible:
+ id: 'rooms-list-view-create-channel'
+ timeout: 60000
+ label: 'Should have create channel button'
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ visible:
+ text: 'Create new channel, team, direct message or discussion'
+ timeout: 60000
+ label: 'Should have create channel button'
# should have sidebar button
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-sidebar'
- timeout: 60000
- label: 'Should have sidebar button'
+- runFlow:
+ when:
+ platform: Android
+ commands:
+ - extendedWaitUntil:
+ visible:
+ id: 'rooms-list-view-sidebar'
+ timeout: 60000
+ label: 'Should have sidebar button'
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ visible:
+ text: 'Menu'
+ timeout: 60000
+ label: 'Should have sidebar button'
# should go to Display Preferences
- runFlow:
diff --git a/.maestro/tests/assorted/i18n.yaml b/.maestro/tests/assorted/i18n.yaml
index b4be402f7b2..928eed1f68a 100644
--- a/.maestro/tests/assorted/i18n.yaml
+++ b/.maestro/tests/assorted/i18n.yaml
@@ -25,8 +25,11 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-view'
@@ -54,8 +57,11 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-view'
diff --git a/.maestro/tests/assorted/in-app-notification.yaml b/.maestro/tests/assorted/in-app-notification.yaml
index 7b60d11506f..aa0a6cfe914 100644
--- a/.maestro/tests/assorted/in-app-notification.yaml
+++ b/.maestro/tests/assorted/in-app-notification.yaml
@@ -28,10 +28,10 @@ tags:
timeout: 60000
- tapOn:
id: 'in-app-notification-Message in DM'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.sender.username}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.sender.username}
- runFlow: '../../helpers/go-back.yaml'
# receive in another room
@@ -46,10 +46,10 @@ tags:
timeout: 60000
- tapOn:
id: 'in-app-notification-Another msg'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.sender.username}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.sender.username}
# should tap back and go back to RoomsListView
- runFlow: '../../helpers/go-back.yaml'
diff --git a/.maestro/tests/assorted/join-from-directory.yaml b/.maestro/tests/assorted/join-from-directory.yaml
index 748345c4f5d..c9673976a5b 100644
--- a/.maestro/tests/assorted/join-from-directory.yaml
+++ b/.maestro/tests/assorted/join-from-directory.yaml
@@ -22,26 +22,23 @@ tags:
PASSWORD: ${output.user.password}
# should tap directory
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-directory'
- timeout: 60000
- waitForAnimationToEnd:
timeout: 10000
-- tapOn:
- id: 'rooms-list-view-directory'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-directory'
+ IOS_LABEL: 'Directory'
- extendedWaitUntil:
visible:
id: 'directory-view'
timeout: 60000
# should search public channel and navigate
-- extendedWaitUntil:
- visible:
- id: 'directory-view-search'
- timeout: 60000
-- tapOn:
- id: 'directory-view-search'
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'directory-view-search'
- inputText: join-from-directory
- extendedWaitUntil:
visible:
@@ -53,56 +50,60 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-join-from-directory'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'join-from-directory'
# should navigate to thread messages view and load messages
-- extendedWaitUntil:
- visible:
- id: 'room-view-header-threads'
- timeout: 60000
-- tapOn:
- id: 'room-view-header-threads'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'room-view-header-threads'
+ IOS_LABEL: 'Threads'
- extendedWaitUntil:
visible:
id: 'thread-messages-view-${output.thread}'
timeout: 60000
-- tapOn:
- id: 'custom-header-back'
-- extendedWaitUntil:
- visible:
- id: 'room-view-header-threads'
- timeout: 60000
+- runFlow: '../../helpers/go-back.yaml'
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - extendedWaitUntil:
+ visible:
+ id: 'room-view-header-threads'
+ timeout: 60000
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ visible:
+ text: 'Threads'
+ timeout: 60000
# should search user and navigate
-- tapOn:
- id: 'header-back'
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-directory'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-directory'
+- runFlow: '../../helpers/go-back.yaml'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-directory'
+ IOS_LABEL: 'Directory'
- extendedWaitUntil:
visible:
id: 'directory-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'directory-view-filter'
- timeout: 60000
-- tapOn:
- id: 'directory-view-filter'
-- extendedWaitUntil:
- visible:
- id: 'directory-switch-users'
- timeout: 60000
-- tapOn:
- id: 'directory-switch-users'
-- tapOn:
- id: 'directory-view-search'
+- runFlow:
+ file: '../../helpers/select-header-filter.yaml'
+ env:
+ ANDROID_FILTER_ID: 'directory-view-filter'
+ ANDROID_OPTION_ID: 'directory-switch-users'
+ IOS_OPTION: 'Users'
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'directory-view-search'
- inputText: ${output.otherUser.username}
- runFlow:
when:
@@ -119,38 +120,32 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.otherUser.username}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.otherUser.username}
# should search team and navigate
-- tapOn:
- id: 'header-back'
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-directory'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-directory'
+- runFlow: '../../helpers/go-back.yaml'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-directory'
+ IOS_LABEL: 'Directory'
- extendedWaitUntil:
visible:
id: 'directory-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'directory-view-filter'
- timeout: 60000
-- tapOn:
- id: 'directory-view-filter'
-- extendedWaitUntil:
- visible:
- id: 'directory-switch-teams'
- timeout: 60000
-- tapOn:
- id: 'directory-switch-teams'
-- tapOn:
- id: 'directory-view-search'
+- runFlow:
+ file: '../../helpers/select-header-filter.yaml'
+ env:
+ ANDROID_FILTER_ID: 'directory-view-filter'
+ ANDROID_OPTION_ID: 'directory-switch-teams'
+ IOS_OPTION: 'Teams'
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'directory-view-search'
- inputText: ${output.team}
- extendedWaitUntil:
visible:
@@ -162,7 +157,7 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.team}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.team}
diff --git a/.maestro/tests/assorted/join-public-room.yaml b/.maestro/tests/assorted/join-public-room.yaml
index c7f82ad1bd9..c6210a77381 100644
--- a/.maestro/tests/assorted/join-public-room.yaml
+++ b/.maestro/tests/assorted/join-public-room.yaml
@@ -25,8 +25,18 @@ tags:
id: room-view
# should have actions button
-- assertVisible:
- id: room-header
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - assertVisible:
+ id: room-header
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - assertVisible:
+ text: 'More'
# should have join
- assertVisible:
diff --git a/.maestro/tests/assorted/profile.yaml b/.maestro/tests/assorted/profile.yaml
index 257cc297509..e973fcfe20a 100644
--- a/.maestro/tests/assorted/profile.yaml
+++ b/.maestro/tests/assorted/profile.yaml
@@ -14,12 +14,11 @@ tags:
env:
USERNAME: ${output.user.username}
PASSWORD: ${output.user.password}
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-sidebar'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-view'
diff --git a/.maestro/tests/assorted/screen-lock.yaml b/.maestro/tests/assorted/screen-lock.yaml
index 4ea169bfcb4..7fb93da90df 100644
--- a/.maestro/tests/assorted/screen-lock.yaml
+++ b/.maestro/tests/assorted/screen-lock.yaml
@@ -20,8 +20,11 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-settings'
@@ -122,8 +125,11 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-settings'
diff --git a/.maestro/tests/assorted/setting.yaml b/.maestro/tests/assorted/setting.yaml
index 9186da79f1e..3d4a08d2143 100644
--- a/.maestro/tests/assorted/setting.yaml
+++ b/.maestro/tests/assorted/setting.yaml
@@ -20,8 +20,11 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-view'
@@ -177,10 +180,11 @@ tags:
timeout: 60000
# should navigate to legalview
-- assertVisible:
- id: 'rooms-list-view-sidebar'
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-view'
diff --git a/.maestro/tests/assorted/status.yaml b/.maestro/tests/assorted/status.yaml
index 88e5f418369..63ef6bcc4fc 100644
--- a/.maestro/tests/assorted/status.yaml
+++ b/.maestro/tests/assorted/status.yaml
@@ -14,12 +14,11 @@ tags:
env:
USERNAME: ${output.user.username}
PASSWORD: ${output.user.password}
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-sidebar'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-view'
diff --git a/.maestro/tests/assorted/user-preferences.yaml b/.maestro/tests/assorted/user-preferences.yaml
index 80e026160c5..5e6baa77684 100644
--- a/.maestro/tests/assorted/user-preferences.yaml
+++ b/.maestro/tests/assorted/user-preferences.yaml
@@ -89,8 +89,7 @@ tags:
id: push-troubleshoot-view-push-gateway-connection
- assertVisible:
id: push-troubleshoot-view-notification-delay
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
# should have alert in Email
- assertVisible:
@@ -105,8 +104,7 @@ tags:
id: notification-preferences-emailNotificationMode-nothing
- tapOn:
id: action-sheet-handle
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
# ASCII should not be converted to emoji (Android only)
- runFlow:
@@ -114,8 +112,7 @@ tags:
when:
platform: Android
commands:
- - tapOn:
- id: 'custom-header-back'
+ - runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'profile-view'
@@ -162,8 +159,7 @@ tags:
when:
platform: Android
commands:
- - tapOn:
- id: 'header-back'
+ - runFlow: '../../helpers/go-back.yaml'
- runFlow:
file: './utils/go-to-user-preference.yaml'
- extendedWaitUntil:
@@ -172,8 +168,7 @@ tags:
timeout: 60000
- tapOn:
id: 'preferences-view-convert-ascii-to-emoji'
- - tapOn:
- id: 'custom-header-back'
+ - runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'profile-view'
diff --git a/.maestro/tests/assorted/utils/check-server.yaml b/.maestro/tests/assorted/utils/check-server.yaml
index 7e180616e21..486c3e14d43 100644
--- a/.maestro/tests/assorted/utils/check-server.yaml
+++ b/.maestro/tests/assorted/utils/check-server.yaml
@@ -4,12 +4,11 @@ tags:
- 'util'
---
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-sidebar'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-view'
diff --git a/.maestro/tests/assorted/utils/go-to-accessibility-appearance.yaml b/.maestro/tests/assorted/utils/go-to-accessibility-appearance.yaml
index f54a4946e4c..42b2309f656 100644
--- a/.maestro/tests/assorted/utils/go-to-accessibility-appearance.yaml
+++ b/.maestro/tests/assorted/utils/go-to-accessibility-appearance.yaml
@@ -4,12 +4,11 @@ tags:
- 'util'
---
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-sidebar'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-accessibility'
diff --git a/.maestro/tests/assorted/utils/go-to-display-pref.yaml b/.maestro/tests/assorted/utils/go-to-display-pref.yaml
index 4acea68390c..110ae664ff4 100644
--- a/.maestro/tests/assorted/utils/go-to-display-pref.yaml
+++ b/.maestro/tests/assorted/utils/go-to-display-pref.yaml
@@ -4,12 +4,11 @@ tags:
- 'util'
---
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-sidebar'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-accessibility'
diff --git a/.maestro/tests/assorted/utils/go-to-room-list.yaml b/.maestro/tests/assorted/utils/go-to-room-list.yaml
index 6734380e054..5dc8737c39e 100644
--- a/.maestro/tests/assorted/utils/go-to-room-list.yaml
+++ b/.maestro/tests/assorted/utils/go-to-room-list.yaml
@@ -4,12 +4,9 @@ tags:
- 'util'
---
-- extendedWaitUntil:
- visible:
- id: 'custom-header-back'
- timeout: 60000
-- tapOn:
- id: 'custom-header-back'
+- waitForAnimationToEnd:
+ timeout: 10000
+- runFlow: '../../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'accessibility-view-drawer'
diff --git a/.maestro/tests/assorted/utils/go-to-user-preference.yaml b/.maestro/tests/assorted/utils/go-to-user-preference.yaml
index 3a735db6b03..18ac81e4df5 100644
--- a/.maestro/tests/assorted/utils/go-to-user-preference.yaml
+++ b/.maestro/tests/assorted/utils/go-to-user-preference.yaml
@@ -4,12 +4,11 @@ tags:
- 'util'
---
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-sidebar'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-profile'
diff --git a/.maestro/tests/assorted/utils/nav-to-language.yaml b/.maestro/tests/assorted/utils/nav-to-language.yaml
index e92b341fad1..1cfd88f8d61 100644
--- a/.maestro/tests/assorted/utils/nav-to-language.yaml
+++ b/.maestro/tests/assorted/utils/nav-to-language.yaml
@@ -8,12 +8,11 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-sidebar'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: 'sidebar-view'
diff --git a/.maestro/tests/e2ee/flows/create-e2ee-room.yaml b/.maestro/tests/e2ee/flows/create-e2ee-room.yaml
index 02219c31d1d..4c88fb8f384 100644
--- a/.maestro/tests/e2ee/flows/create-e2ee-room.yaml
+++ b/.maestro/tests/e2ee/flows/create-e2ee-room.yaml
@@ -4,12 +4,11 @@ tags:
- 'flow'
---
- launchApp
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-create-channel'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-create-channel'
+- runFlow:
+ file: '../../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-create-channel'
+ IOS_LABEL: 'Create new channel, team, direct message or discussion'
- extendedWaitUntil:
visible:
id: 'new-message-view'
@@ -23,8 +22,10 @@ tags:
visible:
id: 'select-users-view'
timeout: 60000
-- tapOn:
- id: 'select-users-view-search'
+- runFlow:
+ file: '../../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'select-users-view-search'
- inputText: ${USER_TO_ADD}
- extendedWaitUntil:
visible:
@@ -53,7 +54,7 @@ tags:
id: 'create-channel-encrypted'
- tapOn:
id: 'create-channel-submit'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${ROOM}'
- timeout: 60000
+- runFlow:
+ file: '../../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${ROOM}
diff --git a/.maestro/tests/e2ee/flows/reset-keys-and-send.yaml b/.maestro/tests/e2ee/flows/reset-keys-and-send.yaml
index f607b8d3957..9a2ab1da641 100644
--- a/.maestro/tests/e2ee/flows/reset-keys-and-send.yaml
+++ b/.maestro/tests/e2ee/flows/reset-keys-and-send.yaml
@@ -39,12 +39,33 @@ env:
visible:
text: '.*The encryption keys for the room need to be updated, another room member needs to be online for this to happen.*'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-view-header-encryption'
- timeout: 60000
-- tapOn:
- id: 'room-view-header-encryption'
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - extendedWaitUntil:
+ visible:
+ id: 'room-view-header-encryption'
+ timeout: 60000
+ - tapOn:
+ id: 'room-view-header-encryption'
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - runFlow:
+ when:
+ notVisible:
+ text: 'Encrypted'
+ commands:
+ - tapOn:
+ text: 'More'
+ - extendedWaitUntil:
+ visible:
+ text: 'Encrypted'
+ timeout: 60000
+ - tapOn:
+ text: 'Encrypted'
- extendedWaitUntil:
visible:
id: 'e2ee-toggle-room-view'
@@ -69,10 +90,10 @@ env:
file: '../../../helpers/navigate-to-room.yaml'
env:
ROOM: ${ROOM}
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${ROOM}'
- timeout: 60000
+- runFlow:
+ file: '../../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${ROOM}
- runFlow:
file: '../../../helpers/send-message.yaml'
env:
diff --git a/.maestro/tests/e2ee/utils/navigate-to-e2ee-security.yaml b/.maestro/tests/e2ee/utils/navigate-to-e2ee-security.yaml
index 00c21e89233..e37a6e5bd0e 100644
--- a/.maestro/tests/e2ee/utils/navigate-to-e2ee-security.yaml
+++ b/.maestro/tests/e2ee/utils/navigate-to-e2ee-security.yaml
@@ -18,13 +18,11 @@ tags:
visible: 'Enter E2EE password'
commands:
- assertVisible: Enter E2EE password
-- extendedWaitUntil:
- visible:
- id: rooms-list-view-sidebar
- enabled: true
- timeout: 60000
-- tapOn:
- id: rooms-list-view-sidebar
+- runFlow:
+ file: '../../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- extendedWaitUntil:
visible:
id: sidebar-view
diff --git a/.maestro/tests/keyboardNavigation/keyboard-navigation-components.yaml b/.maestro/tests/keyboardNavigation/keyboard-navigation-components.yaml
index da7b8376cad..341680e0429 100644
--- a/.maestro/tests/keyboardNavigation/keyboard-navigation-components.yaml
+++ b/.maestro/tests/keyboardNavigation/keyboard-navigation-components.yaml
@@ -72,8 +72,7 @@ tags:
- pressKey: enter
# open Toasts and Dialogs action sheet and navigate through options
-- tapOn:
- id: 'custom-header-back'
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'accessibility-view-list'
diff --git a/.maestro/tests/keyboardNavigation/keyboard-navigation-onboarding.yaml b/.maestro/tests/keyboardNavigation/keyboard-navigation-onboarding.yaml
index 8d48340f4ef..c40ca6d8223 100644
--- a/.maestro/tests/keyboardNavigation/keyboard-navigation-onboarding.yaml
+++ b/.maestro/tests/keyboardNavigation/keyboard-navigation-onboarding.yaml
@@ -31,21 +31,49 @@ tags:
visible:
id: 'login-view'
timeout: 60000
-- pressKey: Remote Dpad Down
-- waitForAnimationToEnd:
- timeout: 1000
-- pressKey: Remote Dpad Down
-- waitForAnimationToEnd:
- timeout: 1000
-- pressKey: Remote Dpad Down
-- waitForAnimationToEnd:
- timeout: 1000
-- pressKey: Remote Dpad Down
+- repeat:
+ times: 10
+ while:
+ notVisible:
+ id: 'login-view-email'
+ focused: true
+ commands:
+ - pressKey: Remote Dpad Down
+ - waitForAnimationToEnd:
+ timeout: 1000
+- assertVisible:
+ id: 'login-view-email'
+ focused: true
- inputText: ${output.user.email}
- pressKey: enter
-- waitForAnimationToEnd:
- timeout: 1000
+- repeat:
+ times: 10
+ while:
+ notVisible:
+ id: 'login-view-password'
+ focused: true
+ commands:
+ - pressKey: Remote Dpad Down
+ - waitForAnimationToEnd:
+ timeout: 1000
+- assertVisible:
+ id: 'login-view-password'
+ focused: true
- inputText: ${output.user.password}
+- hideKeyboard
+- repeat:
+ times: 10
+ while:
+ notVisible:
+ id: 'login-view-submit'
+ focused: true
+ commands:
+ - pressKey: Remote Dpad Down
+ - waitForAnimationToEnd:
+ timeout: 1000
+- assertVisible:
+ id: 'login-view-submit'
+ focused: true
- pressKey: enter
- extendedWaitUntil:
visible:
diff --git a/.maestro/tests/onboarding/roomslist.yaml b/.maestro/tests/onboarding/roomslist.yaml
index ca236a66b74..2421a01478b 100644
--- a/.maestro/tests/onboarding/roomslist.yaml
+++ b/.maestro/tests/onboarding/roomslist.yaml
@@ -19,10 +19,22 @@ tags:
id: 'rooms-list-view'
- assertVisible:
id: 'rooms-list-view-item-general'
-- assertVisible:
- id: 'rooms-list-view-create-channel'
-- assertVisible:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - assertVisible:
+ id: 'rooms-list-view-create-channel'
+ - assertVisible:
+ id: 'rooms-list-view-sidebar'
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - assertVisible:
+ text: 'Create new channel, team, direct message or discussion'
+ - assertVisible:
+ text: 'Menu'
- runFlow:
file: ../../helpers/search-room.yaml
env:
diff --git a/.maestro/tests/performance/scroll-room-to-top.yaml b/.maestro/tests/performance/scroll-room-to-top.yaml
new file mode 100644
index 00000000000..cbd8dc26ffe
--- /dev/null
+++ b/.maestro/tests/performance/scroll-room-to-top.yaml
@@ -0,0 +1,35 @@
+appId: ${APP_ID}
+name: Scroll room to top
+env:
+ ROOM: message-variations-tester
+onFlowStart:
+ - runFlow: '../../helpers/setup.yaml'
+tags:
+ - 'performance'
+ - 'util'
+---
+- runFlow:
+ file: '../../helpers/login-with-deeplink.yaml'
+ env:
+ USERNAME: ${output.account.adminUser}
+ PASSWORD: ${output.account.adminPassword}
+ CLEAR_STATE: true
+- startRecording: scroll-room-to-top
+- runFlow:
+ file: '../../helpers/search-and-navigate-room.yaml'
+ env:
+ ROOM: ${ROOM}
+- extendedWaitUntil:
+ visible:
+ id: 'room-view'
+ timeout: 60000
+- repeat:
+ while:
+ notVisible:
+ text: '.*SYSTEM MESSAGES.*'
+ commands:
+ - swipe:
+ start: 50%, 20%
+ end: 50%, 95%
+ duration: 40
+- stopRecording
diff --git a/.maestro/tests/room/create-dm-group.yaml b/.maestro/tests/room/create-dm-group.yaml
index 12750ea7d0d..340078ba699 100644
--- a/.maestro/tests/room/create-dm-group.yaml
+++ b/.maestro/tests/room/create-dm-group.yaml
@@ -18,12 +18,11 @@ tags:
env:
USERNAME: ${output.user.username}
PASSWORD: ${output.user.password}
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-create-channel'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-create-channel'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-create-channel'
+ IOS_LABEL: 'Create new channel, team, direct message or discussion'
# should have new message screen
- extendedWaitUntil:
@@ -46,12 +45,10 @@ tags:
id: 'new-message-view-create-direct-message'
# should add users
-- extendedWaitUntil:
- visible:
- id: 'select-users-view-search'
- timeout: 60000
-- tapOn:
- id: 'select-users-view-search'
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'select-users-view-search'
- inputText: 'rocket.cat'
- extendedWaitUntil:
visible:
@@ -59,8 +56,10 @@ tags:
timeout: 60000
- tapOn:
id: 'select-users-view-item-rocket.cat'
-- tapOn:
- id: 'select-users-view-search'
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'select-users-view-search'
- eraseText
- inputText: ${output.otherUser.username}
- extendedWaitUntil:
@@ -77,7 +76,7 @@ tags:
id: 'selected-users-view-submit'
# check Group DM exist
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-rocket.cat, ${output.otherUser.username}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'rocket.cat, ${output.otherUser.username}'
diff --git a/.maestro/tests/room/create-room.yaml b/.maestro/tests/room/create-room.yaml
index d7dc2f2df39..fc8219920eb 100644
--- a/.maestro/tests/room/create-room.yaml
+++ b/.maestro/tests/room/create-room.yaml
@@ -15,12 +15,11 @@ tags:
USERNAME: ${output.user.username}
PASSWORD: ${output.user.password}
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-create-channel'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-create-channel'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-create-channel'
+ IOS_LABEL: 'Create new channel, team, direct message or discussion'
# should have new message screen
- extendedWaitUntil:
@@ -45,8 +44,11 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- tapOn:
- id: 'rooms-list-view-create-channel'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-create-channel'
+ IOS_LABEL: 'Create new channel, team, direct message or discussion'
- extendedWaitUntil:
visible:
id: 'new-message-view'
@@ -70,10 +72,10 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-rocket.cat'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'rocket.cat'
- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
@@ -81,12 +83,11 @@ tags:
timeout: 60000
# should navigate to select users
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-create-channel'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-create-channel'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-create-channel'
+ IOS_LABEL: 'Create new channel, team, direct message or discussion'
- extendedWaitUntil:
visible:
id: 'new-message-view'
@@ -103,12 +104,10 @@ tags:
timeout: 60000
# should search users
-- extendedWaitUntil:
- visible:
- id: 'select-users-view-search'
- timeout: 60000
-- tapOn:
- id: 'select-users-view-search'
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'select-users-view-search'
- inputText: 'rocket.cat'
- extendedWaitUntil:
visible:
@@ -203,10 +202,10 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: ${'room-view-title-' + output.publicRoomName}
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.publicRoomName}
- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
@@ -223,12 +222,11 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-create-channel'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-create-channel'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-create-channel'
+ IOS_LABEL: 'Create new channel, team, direct message or discussion'
- extendedWaitUntil:
visible:
id: 'new-message-view'
@@ -269,10 +267,10 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: ${'room-view-title-' + output.privateRoomName}
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.privateRoomName}
- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
@@ -289,12 +287,11 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-view-create-channel'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-view-create-channel'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-create-channel'
+ IOS_LABEL: 'Create new channel, team, direct message or discussion'
- extendedWaitUntil:
visible:
id: 'new-message-view'
@@ -333,10 +330,10 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: ${'room-view-title-' + output.emptyRoomName}
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.emptyRoomName}
- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
diff --git a/.maestro/tests/room/discussion.yaml b/.maestro/tests/room/discussion.yaml
index 3fc7f536218..380306dec08 100644
--- a/.maestro/tests/room/discussion.yaml
+++ b/.maestro/tests/room/discussion.yaml
@@ -25,10 +25,11 @@ tags:
visible:
id: 'rooms-list-view'
timeout: 60000
-- assertVisible:
- id: 'rooms-list-view-create-channel'
-- tapOn:
- id: 'rooms-list-view-create-channel'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-create-channel'
+ IOS_LABEL: 'Create new channel, team, direct message or discussion'
- extendedWaitUntil:
visible:
id: 'new-message-view'
@@ -44,12 +45,10 @@ tags:
id: 'select-users-view'
timeout: 60000
# Select users in SelectedUsersView
-- extendedWaitUntil:
- visible:
- id: 'select-users-view-search'
- timeout: 60000
-- tapOn:
- id: 'select-users-view-search'
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'select-users-view-search'
- inputText: ${output.selectUser}
- extendedWaitUntil:
visible:
@@ -89,18 +88,17 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.discussionFromNewMessage}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.discussionFromNewMessage}
- runFlow: '../../helpers/hide-keyboard.yaml'
- runFlow:
when:
visible:
id: 'room-view'
commands:
- - tapOn:
- id: 'header-back'
+ - runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'rooms-list-view-item-${output.discussionFromNewMessage}'
@@ -142,10 +140,10 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.discussionFromMessageComposer}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.discussionFromMessageComposer}
# Create Discussion from action sheet
- extendedWaitUntil:
@@ -183,21 +181,13 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-message'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'message'
# Check RoomActionsView render
-- assertVisible:
- id: room-header
-- tapOn:
- id: room-header
- retryTapIfNoChange: true
-- extendedWaitUntil:
- visible:
- id: 'room-actions-view'
- timeout: 60000
+- runFlow: '../../helpers/open-room-actions.yaml'
# should have room actions screen
- assertVisible:
@@ -259,20 +249,17 @@ tags:
id: room-info-view-edit-button
# should go back to main room
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'room-actions-view'
timeout: 60000
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'room-view-messages'
timeout: 60000
-- tapOn:
- id: header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'rooms-list-view'
@@ -283,12 +270,7 @@ tags:
ROOM: ${output.room.name}
# should navigate to DiscussionsView
-- extendedWaitUntil:
- visible:
- id: 'room-header'
- timeout: 60000
-- tapOn:
- id: 'room-header'
+- runFlow: '../../helpers/open-room-actions.yaml'
- extendedWaitUntil:
visible:
id: 'room-actions-discussions'
@@ -307,7 +289,7 @@ tags:
timeout: 60000
- tapOn:
id: 'discussions-view-${output.discussionFromNewMessage}'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.discussionFromNewMessage}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.discussionFromNewMessage}
diff --git a/.maestro/tests/room/ignoreuser.yaml b/.maestro/tests/room/ignoreuser.yaml
index f2ea05b2a37..90fe1d4b380 100644
--- a/.maestro/tests/room/ignoreuser.yaml
+++ b/.maestro/tests/room/ignoreuser.yaml
@@ -224,10 +224,10 @@ tags:
timeout: 60000
- tapOn:
id: 'report-user-view-submit'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.otherUser.username}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.otherUser.username}
# should go to user info view from a channel
- runFlow: '../../helpers/go-back.yaml'
@@ -281,7 +281,7 @@ tags:
timeout: 60000
- tapOn:
id: 'report-user-view-submit'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
diff --git a/.maestro/tests/room/jump-to-message.yaml b/.maestro/tests/room/jump-to-message.yaml
index e6cfb9dcd03..4c966f24015 100644
--- a/.maestro/tests/room/jump-to-message.yaml
+++ b/.maestro/tests/room/jump-to-message.yaml
@@ -85,12 +85,24 @@ tags:
file: '../../helpers/search-and-navigate-room.yaml'
env:
ROOM: 'jumping'
-- extendedWaitUntil:
- visible:
- id: 'room-view-search'
- timeout: 60000
-- tapOn:
- id: 'room-view-search'
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - tapOn:
+ text: 'More'
+ - tapOn:
+ text: 'Search messages'
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - extendedWaitUntil:
+ visible:
+ id: 'room-view-search'
+ timeout: 60000
+ - tapOn:
+ id: 'room-view-search'
- extendedWaitUntil:
visible:
id: 'search-messages-view'
@@ -183,8 +195,7 @@ tags:
visible:
id: 'message-content-202'
timeout: 60000
-- tapOn:
- id: header-back
+- runFlow: '../../helpers/go-back.yaml'
# should navigate to a thread from another room
- runFlow:
@@ -208,14 +219,13 @@ tags:
file: './utils/expect-thread-messages.yaml'
env:
message: Go to jumping-thread's thread
-- tapOn:
- id: header-back
+- runFlow: '../../helpers/go-back.yaml'
# should tap on thread message from main room
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-jumping-thread'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'jumping-thread'
- extendedWaitUntil:
visible:
text: '.*thread message sent to main room.*'
@@ -227,14 +237,13 @@ tags:
file: './utils/expect-thread-messages.yaml'
env:
message: 'thread message sent to main room'
-- tapOn:
- id: header-back
+- runFlow: '../../helpers/go-back.yaml'
# should tap on quote
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-jumping-thread'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'jumping-thread'
- extendedWaitUntil:
visible:
id: '.*reply-.*-quoted.*'
@@ -252,16 +261,27 @@ tags:
file: './utils/expect-thread-messages.yaml'
env:
message: 'quoted'
-- tapOn:
- id: 'header-back'
+- runFlow: '../../helpers/go-back.yaml'
# should jump from search message
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-jumping-thread'
- timeout: 60000
-- tapOn:
- id: 'room-view-search'
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'jumping-thread'
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - tapOn:
+ text: 'More'
+ - tapOn:
+ text: 'Search messages'
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - tapOn:
+ id: 'room-view-search'
- extendedWaitUntil:
visible:
id: 'search-messages-view'
diff --git a/.maestro/tests/room/message-markdown-click.yaml b/.maestro/tests/room/message-markdown-click.yaml
index f0a746f2648..c6ea511cd20 100644
--- a/.maestro/tests/room/message-markdown-click.yaml
+++ b/.maestro/tests/room/message-markdown-click.yaml
@@ -57,14 +57,13 @@ tags:
text: '.*detox-public.*'
# Go back to room
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
# Wait for room view to be visible again
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-maestro-message-clickable-test'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: maestro-message-clickable-test
# Tap on the URL link to open alert
- runFlow:
@@ -137,61 +136,58 @@ tags:
text: '.*e2e_admin.*'
# Go back to room
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
# Wait for room view to be visible again
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-maestro-message-clickable-test'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: maestro-message-clickable-test
# Tap on message with thread to open thread room
- tapOn:
text: '.*Message with thread*.'
# Verify thread room view is opened with the correct id
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-message with thread'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: message with thread
# Go back to main room
-- tapOn:
- id: header-back
+- runFlow: '../../helpers/go-back.yaml'
# Wait for room view to be visible again
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-maestro-message-clickable-test'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: maestro-message-clickable-test
# Tap on "View thread" button
- tapOn:
id: 'message-thread-button-message with thread'
# Verify thread room view is opened
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-message with thread'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: message with thread
# Go back to main room
-- tapOn:
- id: header-back
+- runFlow: '../../helpers/go-back.yaml'
# Wait for room view to be visible again
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-maestro-message-clickable-test'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: maestro-message-clickable-test
# Now tap on a message within the thread
- tapOn:
text: '.*a message in thread*.'
# Verify thread room view remains (clicking message in thread stays in thread)
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-message with thread'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: message with thread
diff --git a/.maestro/tests/room/room-actions.yaml b/.maestro/tests/room/room-actions.yaml
index 52a1d7332a9..da3f531a434 100644
--- a/.maestro/tests/room/room-actions.yaml
+++ b/.maestro/tests/room/room-actions.yaml
@@ -93,14 +93,12 @@ tags:
visible:
id: 'mentioned-messages-view'
timeout: 60000
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'room-actions-view'
timeout: 60000
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
# should star and show in starred message
- runFlow:
@@ -131,12 +129,7 @@ tags:
notVisible:
text: '.*Message Starred.*'
timeout: 60000
-- tapOn:
- id: 'room-header'
-- extendedWaitUntil:
- visible:
- id: 'room-actions-view'
- timeout: 60000
+- runFlow: '../../helpers/open-room-actions.yaml'
- tapOn:
id: 'room-actions-starred'
retryTapIfNoChange: true
@@ -149,14 +142,12 @@ tags:
id: 'message-content-messageToStar'
timeout: 60000
# should unstar
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'room-actions-view'
timeout: 60000
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: message-content-messageToStar
@@ -181,12 +172,7 @@ tags:
notVisible:
text: '.*Message Unstarred.*'
timeout: 60000
-- tapOn:
- id: 'room-header'
-- extendedWaitUntil:
- visible:
- id: 'room-actions-view'
- timeout: 60000
+- runFlow: '../../helpers/open-room-actions.yaml'
- tapOn:
id: 'room-actions-starred'
retryTapIfNoChange: true
@@ -198,14 +184,12 @@ tags:
notVisible:
id: 'message-content-messageToStar'
timeout: 60000
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'room-actions-view'
timeout: 60000
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
# should pin a message and show in pinned
- runFlow:
@@ -236,12 +220,7 @@ tags:
visible:
text: '.*Pinned a message:.*'
timeout: 60000
-- tapOn:
- id: 'room-header'
-- extendedWaitUntil:
- visible:
- id: 'room-actions-view'
- timeout: 60000
+- runFlow: '../../helpers/open-room-actions.yaml'
- tapOn: Pinned
- extendedWaitUntil:
visible:
@@ -261,8 +240,7 @@ tags:
notVisible:
id: 'message-content-messageToPin'
timeout: 60000
-- tapOn:
- id: 'custom-header-back'
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'room-actions-view'
@@ -324,8 +302,7 @@ tags:
id: 'notification-preference-view-email-alert'
timeout: 60000
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'room-actions-view'
@@ -387,12 +364,10 @@ tags:
visible:
id: 'selected-user-rocket.cat'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'select-users-view-search'
- timeout: 60000
-- tapOn:
- id: select-users-view-search
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'select-users-view-search'
- inputText: ${output.otherUser.username}
- extendedWaitUntil:
visible:
@@ -406,8 +381,7 @@ tags:
timeout: 60000
- tapOn:
id: selected-users-view-submit
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
text: '.*3 members.*'
@@ -420,18 +394,12 @@ tags:
visible:
id: 'room-members-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-members-view-filter'
- timeout: 60000
-- tapOn:
- id: 'room-members-view-filter'
-- extendedWaitUntil:
- visible:
- id: 'room-members-view-toggle-status-all'
- timeout: 60000
-- tapOn:
- id: 'room-members-view-toggle-status-all'
+- runFlow:
+ file: '../../helpers/select-header-filter.yaml'
+ env:
+ ANDROID_FILTER_ID: 'room-members-view-filter'
+ ANDROID_OPTION_ID: 'room-members-view-toggle-status-all'
+ IOS_OPTION: 'All'
- extendedWaitUntil:
notVisible:
id: 'room-members-view-toggle-status-all'
@@ -440,8 +408,7 @@ tags:
visible:
id: 'room-members-view-item-${output.otherUser.username}'
timeout: 60000
-- tapOn:
- id: 'custom-header-back'
+- runFlow: '../../helpers/go-back.yaml'
# should filter user
- tapOn:
@@ -450,12 +417,10 @@ tags:
visible:
id: 'room-members-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-members-view-search'
- timeout: 60000
-- tapOn:
- id: room-members-view-search
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'room-members-view-search'
- inputText: 'rocket'
- tapOn:
id: room-members-view
@@ -467,8 +432,7 @@ tags:
notVisible:
id: 'room-members-view-item-${output.otherUser.username}'
timeout: 60000
-- tapOn:
- id: clear-text-input
+- runFlow: '../../helpers/clear-search-field.yaml'
- extendedWaitUntil:
visible:
id: 'room-members-view-item-rocket.cat'
@@ -697,14 +661,12 @@ tags:
visible:
text: 'User has been ignored'
timeout: 60000
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'room-actions-view'
timeout: 60000
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
text: '.*Message ignored. Tap to display it.*'
@@ -717,12 +679,7 @@ tags:
timeout: 60000
# should navigate to direct message
-- tapOn:
- id: 'room-header'
-- extendedWaitUntil:
- visible:
- id: 'room-actions-view'
- timeout: 60000
+- runFlow: '../../helpers/open-room-actions.yaml'
- tapOn:
id: 'room-actions-members'
- extendedWaitUntil:
@@ -745,10 +702,10 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.otherUser.username}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.otherUser.username}
- tapOn: Back
- extendedWaitUntil:
visible:
diff --git a/.maestro/tests/room/room-info.yaml b/.maestro/tests/room/room-info.yaml
index 7859a2a7fa1..0078cc1f46f 100644
--- a/.maestro/tests/room/room-info.yaml
+++ b/.maestro/tests/room/room-info.yaml
@@ -141,8 +141,7 @@ tags:
timeout: 60000
# should reset form
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- tapOn:
id: room-info-view-edit-button
- extendedWaitUntil:
@@ -278,8 +277,7 @@ tags:
id: 'room-info-edit-view-react-when-ro'
# should change room name
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- tapOn:
id: room-info-view-edit-button
- evalScript: ${output.newroomname = output.room.name + 'new'}
@@ -305,8 +303,7 @@ tags:
visible:
text: '.*Settings succesfully changed.*'
timeout: 60000
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'room-info-view'
@@ -367,8 +364,7 @@ tags:
visible:
text: '.*Settings succesfully changed.*'
timeout: 60000
-- tapOn:
- id: custom-header-back
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'room-info-view'
diff --git a/.maestro/tests/room/room.yaml b/.maestro/tests/room/room.yaml
index a719d7f004c..092470c6269 100644
--- a/.maestro/tests/room/room.yaml
+++ b/.maestro/tests/room/room.yaml
@@ -483,10 +483,10 @@ tags:
timeout: 60000
- tapOn:
id: message-actions-reply-in-dm
-- extendedWaitUntil:
- visible:
- id: room-view-title-${output.replyUser.username}
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.replyUser.username}
- extendedWaitUntil:
visible:
id: message-composer-input
diff --git a/.maestro/tests/room/search-member.yaml b/.maestro/tests/room/search-member.yaml
index 40c7e07a35f..9fe42658adc 100644
--- a/.maestro/tests/room/search-member.yaml
+++ b/.maestro/tests/room/search-member.yaml
@@ -20,22 +20,15 @@ tags:
file: '../../helpers/navigate-to-room.yaml'
env:
ROOM: 'general'
-- tapOn:
- id: room-header
-- extendedWaitUntil:
- visible:
- id: 'room-actions-view'
- timeout: 60000
+- runFlow: '../../helpers/open-room-actions.yaml'
- tapOn:
id: 'room-actions-members'
-- extendedWaitUntil:
- visible:
- id: 'room-members-view-search'
- timeout: 60000
# should search in all users
-- tapOn:
- id: room-members-view-search
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'room-members-view-search'
- inputText: rohit.bansal
- extendedWaitUntil:
visible:
@@ -43,40 +36,37 @@ tags:
timeout: 60000
# use online status and it should use the text filter
-- tapOn:
- id: room-members-view-filter
-- extendedWaitUntil:
- visible:
- id: 'room-members-view-toggle-status-online'
- timeout: 60000
-- tapOn:
- id: room-members-view-toggle-status-online
+- runFlow:
+ file: '../../helpers/select-header-filter.yaml'
+ env:
+ ANDROID_FILTER_ID: 'room-members-view-filter'
+ ANDROID_OPTION_ID: 'room-members-view-toggle-status-online'
+ IOS_OPTION: 'Online'
- extendedWaitUntil:
visible:
text: 'No members found'
timeout: 60000
# use all status again and it should use text filter
-- tapOn:
- id: room-members-view-filter
-- extendedWaitUntil:
- visible:
- id: 'room-members-view-toggle-status-all'
- timeout: 60000
-- tapOn:
- id: room-members-view-toggle-status-all
+- runFlow:
+ file: '../../helpers/select-header-filter.yaml'
+ env:
+ ANDROID_FILTER_ID: 'room-members-view-filter'
+ ANDROID_OPTION_ID: 'room-members-view-toggle-status-all'
+ IOS_OPTION: 'All'
- extendedWaitUntil:
visible:
id: 'room-members-view-item-rohit.bansal'
timeout: 60000
-- tapOn:
- id: clear-text-input
+- runFlow: '../../helpers/clear-search-field.yaml'
- evalScript: ${output.secondUser = output.utils.createUser()}
# should search for new user in all list
-- tapOn:
- id: room-members-view-search
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'room-members-view-search'
- inputText: ${output.secondUser.username}
- extendedWaitUntil:
visible:
@@ -84,8 +74,10 @@ tags:
timeout: 60000
# Verify "No members found" message appears correctly when search returns no results
-- tapOn:
- id: room-members-view-search
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'room-members-view-search'
- inputText: nonexistentuser12345
- extendedWaitUntil:
visible:
diff --git a/.maestro/tests/room/servers-list-small-screen.yaml b/.maestro/tests/room/servers-list-small-screen.yaml
index 685822b104b..ed2c7216818 100644
--- a/.maestro/tests/room/servers-list-small-screen.yaml
+++ b/.maestro/tests/room/servers-list-small-screen.yaml
@@ -20,12 +20,7 @@ tags:
# should fully render the Workspaces action sheet on small screens
# (regression for small-screen ServersList — the Add Server button was cut off on iPhone SE)
-- extendedWaitUntil:
- visible:
- id: 'rooms-list-header-servers-list-button'
- timeout: 60000
-- tapOn:
- id: 'rooms-list-header-servers-list-button'
+- runFlow: '../../helpers/open-servers-list.yaml'
- waitForAnimationToEnd
- extendedWaitUntil:
visible:
diff --git a/.maestro/tests/room/share-message.yaml b/.maestro/tests/room/share-message.yaml
index 461818f2f37..0448192fe26 100644
--- a/.maestro/tests/room/share-message.yaml
+++ b/.maestro/tests/room/share-message.yaml
@@ -89,10 +89,10 @@ tags:
timeout: 60000
- tapOn:
id: 'forward-message-view-send'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
# should go to otherUser DM and verify if exist both messages
- runFlow: '../../helpers/go-back.yaml'
diff --git a/.maestro/tests/room/threads.yaml b/.maestro/tests/room/threads.yaml
index f45f17151b4..181e690beda 100644
--- a/.maestro/tests/room/threads.yaml
+++ b/.maestro/tests/room/threads.yaml
@@ -23,22 +23,46 @@ tags:
ROOM: ${output.room.name}
# should have room screen
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
# should have actions button
-- extendedWaitUntil:
- visible:
- id: 'room-header'
- timeout: 60000
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - extendedWaitUntil:
+ visible:
+ id: 'room-header'
+ timeout: 60000
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ visible:
+ text: 'More'
+ timeout: 60000
# should have threads button
-- extendedWaitUntil:
- visible:
- id: 'room-view-header-threads'
- timeout: 60000
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - extendedWaitUntil:
+ visible:
+ id: 'room-view-header-threads'
+ timeout: 60000
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ visible:
+ text: 'Threads'
+ timeout: 60000
# should create thread
- runFlow:
@@ -61,10 +85,10 @@ tags:
timeout: 60000
- tapOn:
text: 'Reply in thread'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-thread'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'thread'
- extendedWaitUntil:
visible:
id: 'message-composer-input-thread'
@@ -78,41 +102,50 @@ tags:
visible:
id: 'message-content-replied'
timeout: 60000
-- tapOn:
- id: 'header-back'
+- runFlow: '../../helpers/go-back.yaml'
# should navigate to thread from button
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
- extendedWaitUntil:
visible:
id: 'message-thread-button-thread'
timeout: 60000
- tapOn:
id: 'message-thread-button-thread'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-thread'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'thread'
# should toggle follow thread
-- extendedWaitUntil:
- visible:
- id: 'room-view-header-unfollow'
- timeout: 60000
-- tapOn:
- id: 'room-view-header-unfollow'
-- extendedWaitUntil:
- visible:
- id: 'room-view-header-follow'
- timeout: 60000
-- tapOn:
- id: 'room-view-header-follow'
-- extendedWaitUntil:
- visible:
- id: 'room-view-header-unfollow'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'room-view-header-unfollow'
+ IOS_LABEL: 'Unfollow thread'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'room-view-header-follow'
+ IOS_LABEL: 'Follow thread'
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - extendedWaitUntil:
+ visible:
+ id: 'room-view-header-unfollow'
+ timeout: 60000
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ visible:
+ text: 'Unfollow thread'
+ timeout: 60000
# should send message in thread only
- runFlow:
@@ -120,16 +153,27 @@ tags:
env:
message: threadonly
isThread: 'true'
-- tapOn:
- id: 'header-back'
-- extendedWaitUntil:
- notVisible:
- id: 'room-view-title-thread'
- timeout: 5000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 5000
+- runFlow: '../../helpers/go-back.yaml'
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - extendedWaitUntil:
+ notVisible:
+ id: 'room-view-title-thread'
+ timeout: 5000
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ notVisible:
+ text: 'thread'
+ timeout: 5000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
- extendedWaitUntil:
notVisible:
id: 'message-content-threadonly'
@@ -142,10 +186,10 @@ tags:
timeout: 60000
- tapOn:
id: 'message-thread-button-thread'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-thread'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'thread'
- tapOn:
id: 'message-composer-input-thread'
- inputText: 'sendToChannel'
@@ -153,16 +197,27 @@ tags:
id: send-to-channel-unchecked
- tapOn:
id: message-composer-send
-- tapOn:
- id: header-back
-- extendedWaitUntil:
- notVisible:
- id: 'room-view-title-thread'
- timeout: 5000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 5000
+- runFlow: '../../helpers/go-back.yaml'
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - extendedWaitUntil:
+ notVisible:
+ id: 'room-view-title-thread'
+ timeout: 5000
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ notVisible:
+ text: 'thread'
+ timeout: 5000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
- extendedWaitUntil:
visible:
text: '.*sendToChannel.*'
@@ -186,36 +241,45 @@ tags:
id: 'message-composer-send-to-channel'
- tapOn:
id: 'message-composer-send'
-- tapOn:
- id: 'header-back'
-- extendedWaitUntil:
- notVisible:
- id: 'room-view-title-thread'
- timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow: '../../helpers/go-back.yaml'
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - extendedWaitUntil:
+ notVisible:
+ id: 'room-view-title-thread'
+ timeout: 60000
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ notVisible:
+ text: 'thread'
+ timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
- extendedWaitUntil:
visible:
id: 'message-content-navthreadname'
timeout: 60000
- tapOn:
id: 'message-content-navthreadname'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-thread'
- timeout: 60000
-- tapOn:
- id: 'header-back'
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'thread'
+- runFlow: '../../helpers/go-back.yaml'
# should navigate to thread from threads view
-- extendedWaitUntil:
- visible:
- id: 'room-view-header-threads'
- timeout: 60000
-- tapOn:
- id: 'room-view-header-threads'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'room-view-header-threads'
+ IOS_LABEL: 'Threads'
- extendedWaitUntil:
visible:
id: 'thread-messages-view'
@@ -226,12 +290,11 @@ tags:
timeout: 60000
- tapOn:
id: 'thread-messages-view-thread'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-thread'
- timeout: 60000
-- tapOn:
- id: 'header-back'
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'thread'
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'room-view-messages'
@@ -244,15 +307,14 @@ tags:
timeout: 60000
- tapOn:
id: 'message-thread-button-thread'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-thread'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'thread'
- tapOn:
id: 'message-composer-input-thread'
- inputText: 'draftthread'
-- tapOn:
- id: 'header-back'
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
@@ -260,10 +322,10 @@ tags:
timeout: 60000
- tapOn:
id: 'message-thread-button-thread'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-thread'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'thread'
- extendedWaitUntil:
visible:
text: '.*draftthread.*'
@@ -271,8 +333,7 @@ tags:
- tapOn:
id: 'message-composer-input-thread'
- eraseText
-- tapOn:
- id: 'header-back'
+- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
@@ -280,10 +341,10 @@ tags:
timeout: 60000
- tapOn:
id: 'message-thread-button-thread'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-thread'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'thread'
- extendedWaitUntil:
notVisible:
text: '.*draftthread.*'
@@ -292,16 +353,27 @@ tags:
visible:
text: '.*Add thread reply.*'
timeout: 60000
-- tapOn:
- id: 'header-back'
-- extendedWaitUntil:
- notVisible:
- id: 'room-view-title-thread'
- timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow: '../../helpers/go-back.yaml'
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - extendedWaitUntil:
+ notVisible:
+ id: 'room-view-title-thread'
+ timeout: 60000
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - extendedWaitUntil:
+ notVisible:
+ text: 'thread'
+ timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
# should create thread delete the message and the thread show the correct number of messages
- runFlow:
@@ -324,10 +396,10 @@ tags:
timeout: 60000
- tapOn:
text: 'Reply in thread'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-thread-message-count'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'thread-message-count'
- extendedWaitUntil:
visible:
id: 'message-composer-input-thread'
@@ -374,12 +446,11 @@ tags:
text: '.*You will not be able to recover this message.*'
timeout: 60000
- tapOn: 'DELETE'
-- tapOn:
- id: 'header-back'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.room.name}'
- timeout: 60000
+- runFlow: '../../helpers/go-back.yaml'
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.room.name}
- extendedWaitUntil:
visible:
id: 'thread-count-0'
diff --git a/.maestro/tests/room/unread-badge.yaml b/.maestro/tests/room/unread-badge.yaml
index c52cfff178b..f0054edfa1b 100644
--- a/.maestro/tests/room/unread-badge.yaml
+++ b/.maestro/tests/room/unread-badge.yaml
@@ -34,9 +34,7 @@ tags:
timeout: 60000
- tapOn:
id: 'rooms-list-view-item-${output.otherUser.username}'
-- tapOn:
- id: header-back
- retryTapIfNoChange: true
+- runFlow: '../../helpers/go-back.yaml'
- waitForAnimationToEnd:
timeout: 5000
- extendedWaitUntil:
@@ -62,9 +60,7 @@ tags:
timeout: 60000
- tapOn:
id: 'rooms-list-view-item-${output.otherUser.username}'
-- tapOn:
- id: header-back
- retryTapIfNoChange: true
+- runFlow: '../../helpers/go-back.yaml'
- waitForAnimationToEnd:
timeout: 5000
- extendedWaitUntil:
@@ -91,9 +87,7 @@ tags:
timeout: 60000
- tapOn:
id: 'rooms-list-view-item-${output.otherUser.username}'
-- tapOn:
- id: header-back
- retryTapIfNoChange: true
+- runFlow: '../../helpers/go-back.yaml'
- waitForAnimationToEnd:
timeout: 5000
- extendedWaitUntil:
@@ -120,9 +114,7 @@ tags:
timeout: 60000
- tapOn:
id: 'rooms-list-view-item-${output.otherUser.username}'
-- tapOn:
- id: header-back
- retryTapIfNoChange: true
+- runFlow: '../../helpers/go-back.yaml'
- waitForAnimationToEnd:
timeout: 5000
- extendedWaitUntil:
diff --git a/.maestro/tests/room/utils/clear-cache.yaml b/.maestro/tests/room/utils/clear-cache.yaml
index bf087ac3e01..d54f70f408a 100644
--- a/.maestro/tests/room/utils/clear-cache.yaml
+++ b/.maestro/tests/room/utils/clear-cache.yaml
@@ -8,14 +8,16 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- tapOn:
- id: 'header-back'
+- runFlow: '../../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
id: 'rooms-list-view'
timeout: 60000
-- tapOn:
- id: 'rooms-list-view-sidebar'
+- runFlow:
+ file: '../../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-sidebar'
+ IOS_LABEL: 'Menu'
- waitForAnimationToEnd:
timeout: 60000
- extendedWaitUntil:
diff --git a/.maestro/tests/room/utils/expect-thread-messages.yaml b/.maestro/tests/room/utils/expect-thread-messages.yaml
index 5f3fbc3ae44..ec204c19e36 100644
--- a/.maestro/tests/room/utils/expect-thread-messages.yaml
+++ b/.maestro/tests/room/utils/expect-thread-messages.yaml
@@ -4,10 +4,10 @@ tags:
- 'util'
---
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-thread 1'
- timeout: 60000
+- runFlow:
+ file: '../../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: 'thread 1'
- extendedWaitUntil:
visible:
id: '.*message-content-${message}.*'
diff --git a/.maestro/tests/room/utils/navigate-to-info-view.yaml b/.maestro/tests/room/utils/navigate-to-info-view.yaml
index c2b33f0a718..71308b4d3dd 100644
--- a/.maestro/tests/room/utils/navigate-to-info-view.yaml
+++ b/.maestro/tests/room/utils/navigate-to-info-view.yaml
@@ -4,16 +4,7 @@ tags:
- 'util'
---
-- extendedWaitUntil:
- visible:
- id: room-header
- timeout: 60000
-- tapOn:
- id: room-header
-- extendedWaitUntil:
- visible:
- id: room-actions-view
- timeout: 60000
+- runFlow: '../../../helpers/open-room-actions.yaml'
- assertVisible:
id: room-actions-info
- tapOn:
diff --git a/.maestro/tests/room/utils/navigate-to-room-info.yaml b/.maestro/tests/room/utils/navigate-to-room-info.yaml
index b440852dcbe..4c419f0c9d2 100644
--- a/.maestro/tests/room/utils/navigate-to-room-info.yaml
+++ b/.maestro/tests/room/utils/navigate-to-room-info.yaml
@@ -8,16 +8,7 @@ tags:
file: '../../../helpers/search-and-navigate-room.yaml'
env:
ROOM: ${ROOM}
-- extendedWaitUntil:
- visible:
- id: 'room-header'
- timeout: 60000
-- tapOn:
- id: 'room-header'
-- extendedWaitUntil:
- visible:
- id: 'room-actions-view'
- timeout: 60000
+- runFlow: '../../../helpers/open-room-actions.yaml'
- extendedWaitUntil:
visible:
id: 'room-actions-info'
diff --git a/.maestro/tests/teams/convert-team.yaml b/.maestro/tests/teams/convert-team.yaml
index 9820ad29154..0442b800a1b 100644
--- a/.maestro/tests/teams/convert-team.yaml
+++ b/.maestro/tests/teams/convert-team.yaml
@@ -42,10 +42,10 @@ tags:
text: 'Convert'
- tapOn:
text: 'Convert'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.publicChannelToBeConverted}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.publicChannelToBeConverted}
- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
@@ -74,10 +74,10 @@ tags:
text: 'Convert'
- tapOn:
text: 'Convert'
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.toBeConverted}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.toBeConverted}
- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
@@ -124,12 +124,7 @@ tags:
text: 'Yes, move it!'
- tapOn:
text: 'Yes, move it!'
-- extendedWaitUntil:
- visible:
- id: 'room-header'
- timeout: 60000
-- tapOn:
- id: 'room-header'
+- runFlow: '../../helpers/open-room-actions.yaml'
- extendedWaitUntil:
visible:
id: 'room-actions-teams'
@@ -178,10 +173,10 @@ tags:
visible:
id: 'room-view'
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: 'room-view-title-${output.toBeConverted}'
- timeout: 60000
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.toBeConverted}
- runFlow: '../../helpers/go-back.yaml'
- extendedWaitUntil:
visible:
diff --git a/.maestro/tests/teams/create-team.yaml b/.maestro/tests/teams/create-team.yaml
index a587cd06daf..fbc106dd3ff 100644
--- a/.maestro/tests/teams/create-team.yaml
+++ b/.maestro/tests/teams/create-team.yaml
@@ -17,10 +17,11 @@ tags:
USERNAME: ${output.user.username}
PASSWORD: ${output.user.password}
-- assertVisible:
- id: 'rooms-list-view-create-channel'
-- tapOn:
- id: 'rooms-list-view-create-channel'
+- runFlow:
+ file: '../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-create-channel'
+ IOS_LABEL: 'Create new channel, team, direct message or discussion'
- assertVisible:
id: 'new-message-view-create-team'
- tapOn:
@@ -39,10 +40,11 @@ tags:
id: 'create-channel-submit'
- assertVisible:
id: 'room-view-messages'
-- assertVisible:
- id: 'room-view-title-${output.teamname}'
-- tapOn:
- id: 'room-header'
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.teamname}
+- runFlow: '../../helpers/open-room-actions.yaml'
- assertVisible:
id: 'room-actions-info'
- tapOn:
diff --git a/.maestro/tests/teams/team.yaml b/.maestro/tests/teams/team.yaml
index 91274b1131c..4b64887b3ce 100644
--- a/.maestro/tests/teams/team.yaml
+++ b/.maestro/tests/teams/team.yaml
@@ -25,18 +25,39 @@ tags:
ROOM: ${output.team}
# Team Header
-- assertVisible:
- id: room-header
-- assertVisible:
- id: room-view-header-call
-- assertVisible:
- id: room-view-header-threads
-- assertVisible:
- id: room-view-search
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - assertVisible:
+ id: room-header
+ - assertVisible:
+ id: room-view-search
+ - assertVisible:
+ id: room-view-header-call
+ - assertVisible:
+ id: room-view-header-threads
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - assertVisible:
+ text: '.*Call.*'
+ - assertVisible:
+ text: 'Threads'
+ - tapOn:
+ text: 'More'
+ - extendedWaitUntil:
+ visible:
+ id: 'action-sheet'
+ timeout: 60000
+ - assertVisible:
+ text: 'Search messages'
+ - tapOn:
+ id: 'action-sheet-handle'
# Team Action Usage
-- tapOn:
- id: room-header
+- runFlow: '../../helpers/open-room-actions.yaml'
- tapOn:
id: room-actions-teams
- extendedWaitUntil:
@@ -46,15 +67,30 @@ tags:
label: Should navigate to team channels view
# Team Channels Header
-- assertVisible:
- id: room-header
- label: Should have actions button
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - assertVisible:
+ id: room-header
+ label: Should have actions button
- assertVisible:
id: team-channels-view-create
label: Should have team channels button
-- assertVisible:
- id: team-channels-view-search
- label: Should have search button
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - assertVisible:
+ id: magnifyingglass
+ label: Should have system search bar
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - assertVisible:
+ id: team-channels-view-search
+ label: Should have search button
# Team Channels Header Usage
- tapOn:
@@ -78,13 +114,11 @@ tags:
# Create new channel for team
- tapOn:
id: add-channel-team-view-create-channel
-- assertVisible:
- id: select-users-view-search
-- tapOn:
- id: select-users-view-search
-- inputText:
- id: select-users-view-search
- text: rocket.cat
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'select-users-view-search'
+- inputText: rocket.cat
- extendedWaitUntil:
visible:
id: select-users-view-item-rocket.cat
@@ -119,10 +153,19 @@ tags:
id: room-view
timeout: 60000
label: should navigate to room view
-- assertVisible:
- id: room-header
-- tapOn:
- id: room-header
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - assertVisible:
+ id: room-header
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - assertVisible:
+ text: 'More'
+- runFlow: '../../helpers/open-room-actions.yaml'
- assertVisible:
id: room-actions-teams
- tapOn:
@@ -141,18 +184,42 @@ tags:
id: rooms-list-view-item-${output.privateRoomName}
- tapOn:
id: rooms-list-view-item-${output.privateRoomName}
-- extendedWaitUntil:
- visible:
- id: room-view-title-${output.privateRoomName}
- timeout: 60000
-- assertVisible:
- id: room-view-title-${output.privateRoomName}
-- assertVisible:
- id: room-view-header-call
-- assertVisible:
- id: room-view-header-threads
-- assertVisible:
- id: room-view-search
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.privateRoomName}
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.privateRoomName}
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - assertVisible:
+ id: room-view-header-call
+ - assertVisible:
+ id: room-view-header-threads
+ - assertVisible:
+ id: room-view-search
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - assertVisible:
+ text: '.*Call.*'
+ - assertVisible:
+ text: 'Threads'
+ - tapOn:
+ text: 'More'
+ - extendedWaitUntil:
+ visible:
+ id: 'action-sheet'
+ timeout: 60000
+ - assertVisible:
+ text: 'Search messages'
+ - tapOn:
+ id: 'action-sheet-handle'
- runFlow: '../../helpers/go-back.yaml'
# should add existing channel to team
@@ -160,10 +227,19 @@ tags:
file: '../../helpers/navigate-to-room.yaml'
env:
ROOM: ${output.team}
-- assertVisible:
- id: room-header
-- tapOn:
- id: room-header
+- runFlow:
+ when:
+ platform: android
+ commands:
+ - assertVisible:
+ id: room-header
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - assertVisible:
+ text: 'More'
+- runFlow: '../../helpers/open-room-actions.yaml'
- assertVisible:
id: room-actions-teams
- tapOn:
@@ -198,12 +274,11 @@ tags:
label: should have submit button
- tapOn:
id: add-existing-channel-view-submit
-- extendedWaitUntil:
- visible:
- id: room-view-title-${output.team}
- timeout: 60000
-- tapOn:
- id: room-header
+- runFlow:
+ file: '../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${output.team}
+- runFlow: '../../helpers/open-room-actions.yaml'
- assertVisible:
id: room-actions-teams
- tapOn:
@@ -272,12 +347,7 @@ tags:
visible:
id: room-view
timeout: 60000
-- tapOn:
- id: room-header
-- extendedWaitUntil:
- visible:
- id: room-actions-view
- timeout: 60000
+- runFlow: '../../helpers/open-room-actions.yaml'
# should add users to the team
- tapOn:
@@ -292,10 +362,10 @@ tags:
timeout: 60000
- tapOn:
id: room-actions-add-user
-- assertVisible:
- id: select-users-view-search
-- tapOn:
- id: select-users-view-search
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'select-users-view-search'
- inputText: rocket.cat
- runFlow: '../../helpers/hide-keyboard.yaml'
- extendedWaitUntil:
@@ -308,12 +378,10 @@ tags:
visible:
id: selected-user-rocket.cat
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: select-users-view-search
- timeout: 60000
-- tapOn:
- id: select-users-view-search
+- runFlow:
+ file: '../../helpers/tap-search-field.yaml'
+ env:
+ SEARCH_ID: 'select-users-view-search'
- eraseText
- inputText: ${output.secondUser.username}
- runFlow: '../../helpers/hide-keyboard.yaml'
@@ -332,16 +400,12 @@ tags:
- tapOn:
id: selected-users-view-submit
-- assertVisible:
- id: room-members-view-filter
-- tapOn:
- id: room-members-view-filter
-- extendedWaitUntil:
- visible:
- id: room-members-view-toggle-status-all
- timeout: 60000
-- tapOn:
- id: room-members-view-toggle-status-all
+- runFlow:
+ file: '../../helpers/select-header-filter.yaml'
+ env:
+ ANDROID_FILTER_ID: 'room-members-view-filter'
+ ANDROID_OPTION_ID: 'room-members-view-toggle-status-all'
+ IOS_OPTION: 'All'
- extendedWaitUntil:
visible:
id: room-members-view-item-${output.secondUser.username}
diff --git a/.maestro/tests/teams/utils/create-channel.yaml b/.maestro/tests/teams/utils/create-channel.yaml
index b84015412ad..63cadb5feaf 100644
--- a/.maestro/tests/teams/utils/create-channel.yaml
+++ b/.maestro/tests/teams/utils/create-channel.yaml
@@ -3,12 +3,11 @@ tags:
- 'util'
---
-- extendedWaitUntil:
- visible:
- id: rooms-list-view-create-channel
- timeout: 60000
-- tapOn:
- id: rooms-list-view-create-channel
+- runFlow:
+ file: '../../../helpers/tap-header-item.yaml'
+ env:
+ ANDROID_ID: 'rooms-list-view-create-channel'
+ IOS_LABEL: 'Create new channel, team, direct message or discussion'
- extendedWaitUntil:
visible:
id: new-message-view
@@ -54,10 +53,10 @@ tags:
visible:
id: room-view
timeout: 60000
-- extendedWaitUntil:
- visible:
- id: room-view-title-${channelName}
- timeout: 60000
+- runFlow:
+ file: '../../../helpers/assert-room-title.yaml'
+ env:
+ ROOM: ${channelName}
- runFlow: ../../../helpers/go-back.yaml
- extendedWaitUntil:
visible:
diff --git a/.sniffler/config.json b/.sniffler/config.json
index f7726236c8e..0fc3c6ca471 100644
--- a/.sniffler/config.json
+++ b/.sniffler/config.json
@@ -28,6 +28,7 @@
"app/lib/methods/getUsersPresence.ts",
"app/sagas/index.js",
"app/sagas/init.js",
+ "app/sagas/preloadNativeHeaderIcons.js",
"app/lib/methods/loggedInServer.ts",
"app/sagas/state.js",
"app/sagas/selectServer.ts",
diff --git a/.sniffler/test-map.json b/.sniffler/test-map.json
index 4ac3ebed352..bd67980093a 100644
--- a/.sniffler/test-map.json
+++ b/.sniffler/test-map.json
@@ -1,7 +1,7 @@
[
{
"test": ".maestro/tests/accessibilityAndAppearance/ToastsAndDialogs.yml",
- "dependsOn": ["app/views/StatusView/**"]
+ "dependsOn": ["app/views/StatusView/**", "app/views/SidebarView/**"]
},
{
"test": ".maestro/tests/assorted/accessibility-and-appearance.yaml",
@@ -406,7 +406,7 @@
"test": ".maestro/tests/teams/convert-team.yaml",
"dependsOn": [
"app/views/RoomActionsView/**",
- "app/views/SelectListView.tsx",
+ "app/views/SelectListView/**",
"app/views/CreateChannelView/**",
"app/views/NewMessageView/**",
"app/views/SelectedUsersView/**",
@@ -436,7 +436,7 @@
"app/views/AddExistingChannelView/**",
"app/views/RoomMembersView/**",
"app/views/SelectedUsersView/**",
- "app/views/SelectListView.tsx",
+ "app/views/SelectListView/**",
"app/views/CreateChannelView/**",
"app/sagas/room.js",
"app/sagas/createChannel.js"
diff --git a/CLAUDE.md b/CLAUDE.md
index 048929f381f..6391983df5b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
-Rocket.Chat React Native mobile client. Single-package React Native app (not a monorepo) using pnpm. Supports iOS 13.4+ and Android 6.0+.
+Rocket.Chat React Native mobile client. Single-package React Native app (not a monorepo) using pnpm. Supports iOS 16.4+ and Android 6.0+.
Read CONTEXT.md.
diff --git a/Gemfile.lock b/Gemfile.lock
index a85e179d53b..7b85556bf9a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -178,7 +178,7 @@ GEM
xml-simple
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
- ffi (1.17.1)
+ ffi (1.17.4)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
diff --git a/README.md b/README.md
index ed7fb227d16..3ce08b95d7f 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Rocket.Chat Mobile
- **Supported server versions:** 0.70.0+
-- **Supported iOS versions**: 13.4+
+- **Supported iOS versions**: 16.4+
- **Supported Android versions**: 6.0+
## Download
diff --git a/__mocks__/react-native-mmkv.js b/__mocks__/react-native-mmkv.js
index c0e1cedee61..2426b2b20e6 100644
--- a/__mocks__/react-native-mmkv.js
+++ b/__mocks__/react-native-mmkv.js
@@ -1,27 +1,16 @@
-// Mock for react-native-mmkv
-const { useState, useEffect, useRef, useMemo, useCallback } = require('react');
+const { useState, useEffect, useCallback } = require('react');
// Shared storage between instances with the same id
const storageInstances = new Map();
-let defaultInstance = null;
-export const Mode = {
- SINGLE_PROCESS: 'single-process',
- MULTI_PROCESS: 'multi-process'
-};
-
-// MMKV Instance class
class MMKVInstance {
constructor(config = {}) {
- const { id = 'mmkv.default', path, encryptionKey, mode, readOnly } = config;
+ const { id = 'mmkv.default', path, encryptionKey, mode } = config;
this.id = id;
this.path = path;
this.encryptionKey = encryptionKey;
this.mode = mode;
- this.isReadOnly = readOnly || false;
- this._size = 0;
- // Share storage between instances with the same id
if (!storageInstances.has(this.id)) {
storageInstances.set(this.id, {
storage: new Map(),
@@ -34,16 +23,8 @@ class MMKVInstance {
this.listeners = instance.listeners;
}
- get size() {
- return this._size;
- }
-
set(key, value) {
- if (this.isReadOnly) {
- throw new Error('Cannot set value in read-only instance');
- }
this.storage.set(key, value);
- this._size = this.storage.size;
this.notifyListeners(key);
}
@@ -62,22 +43,13 @@ class MMKVInstance {
return typeof value === 'boolean' ? value : undefined;
}
- getBuffer(key) {
- const value = this.storage.get(key);
- return value instanceof ArrayBuffer ? value : undefined;
- }
-
contains(key) {
return this.storage.has(key);
}
remove(key) {
- if (this.isReadOnly) {
- throw new Error('Cannot remove value in read-only instance');
- }
const deleted = this.storage.delete(key);
if (deleted) {
- this._size = this.storage.size;
this.notifyListeners(key);
}
return deleted;
@@ -88,39 +60,10 @@ class MMKVInstance {
}
clearAll() {
- if (this.isReadOnly) {
- throw new Error('Cannot clear read-only instance');
- }
this.storage.clear();
- this._size = 0;
- // Notify about clear (pass undefined to indicate clear all)
this.notifyListeners(undefined);
}
- recrypt(key) {
- this.encryptionKey = key;
- }
-
- trim() {
- // No-op for mock
- }
-
- importAllFrom(other) {
- if (this.isReadOnly) {
- throw new Error('Cannot import values into read-only instance');
- }
- let count = 0;
- for (const [key, value] of other.storage.entries()) {
- this.storage.set(key, value);
- count++;
- }
- this._size = this.storage.size;
- if (count > 0) {
- this.notifyListeners(undefined);
- }
- return count;
- }
-
addOnValueChangedListener(callback) {
this.listeners.push(callback);
return {
@@ -144,202 +87,37 @@ class MMKVInstance {
}
}
-// Factory function to create MMKV instances
export function createMMKV(configuration) {
return new MMKVInstance(configuration);
}
-// Get default instance
-function getDefaultMMKVInstance() {
- if (defaultInstance == null) {
- defaultInstance = createMMKV();
- }
- return defaultInstance;
-}
-
-// Top-level functions
-export function existsMMKV(id) {
- return storageInstances.has(id);
-}
-
-export function deleteMMKV(id) {
- const instance = storageInstances.get(id);
- instance?.storage.clear();
- instance?.listeners.splice(0);
-
- const deleted = storageInstances.delete(id);
- if (defaultInstance?.id === id) {
- defaultInstance = null;
- }
- return deleted;
-}
+export function useMMKVString(key, instance) {
+ const [value, setValue] = useState(() => instance.getString(key));
-// Helper function for configuration comparison
-function isConfigurationEqual(left, right) {
- if (left == null || right == null) {
- return left == null && right == null;
- }
-
- return (
- left.encryptionKey === right.encryptionKey &&
- left.id === right.id &&
- left.path === right.path &&
- left.mode === right.mode &&
- left.readOnly === right.readOnly
+ const set = useCallback(
+ v => {
+ const newValue = typeof v === 'function' ? v(instance.getString(key)) : v;
+ if (newValue === undefined) {
+ instance.remove(key);
+ return;
+ }
+ if (typeof newValue !== 'string') {
+ throw new Error(`MMKV: Type ${typeof newValue} is not supported!`);
+ }
+ instance.set(key, newValue);
+ },
+ [key, instance]
);
-}
-
-/**
- * Use the default, shared MMKV instance or a custom instance with configuration
- */
-export function useMMKV(configuration) {
- const configurationRef = useRef();
- const instanceRef = useRef();
-
- if (configuration == null) {
- return getDefaultMMKVInstance();
- }
-
- if (!instanceRef.current || !isConfigurationEqual(configurationRef.current, configuration)) {
- configurationRef.current = configuration;
- instanceRef.current = createMMKV(configuration);
- }
-
- return instanceRef.current;
-}
-
-/**
- * Listen for changes in the given MMKV storage instance
- */
-export function useMMKVListener(valueChangedListener, instance) {
- const ref = useRef(valueChangedListener);
- const mmkv = instance ?? getDefaultMMKVInstance();
useEffect(() => {
- ref.current = valueChangedListener;
- }, [valueChangedListener]);
-
- useEffect(() => {
- const listener = mmkv.addOnValueChangedListener(changedKey => {
- ref.current(changedKey);
+ setValue(instance.getString(key));
+ const listener = instance.addOnValueChangedListener(changedKey => {
+ if (changedKey === undefined || changedKey === key) {
+ setValue(instance.getString(key));
+ }
});
return () => listener.remove();
- }, [mmkv]);
-}
+ }, [key, instance]);
-/**
- * Get a list of all keys that exist in the given MMKV instance
- */
-export function useMMKVKeys(instance) {
- const mmkv = instance ?? getDefaultMMKVInstance();
- const [allKeys, setKeys] = useState(() => mmkv.getAllKeys());
-
- useEffect(() => {
- setKeys(mmkv.getAllKeys());
- }, [mmkv]);
-
- useMMKVListener(key => {
- if (key === undefined) {
- setKeys(() => mmkv.getAllKeys());
- return;
- }
-
- const currentlyHasKey = allKeys.includes(key);
- const hasKey = mmkv.contains(key);
- if (hasKey !== currentlyHasKey) {
- setKeys(() => mmkv.getAllKeys());
- }
- }, mmkv);
-
- return allKeys;
+ return [value, set];
}
-
-/**
- * Create a custom MMKV hook for a specific type
- */
-function createMMKVHook(getter, onSet) {
- return (key, instance) => {
- const mmkv = instance ?? getDefaultMMKVInstance();
-
- const [bump, setBump] = useState(0);
- const value = useMemo(() => getter(mmkv, key), [mmkv, key, bump]);
-
- const set = useCallback(
- v => {
- const newValue = typeof v === 'function' ? v(getter(mmkv, key)) : v;
- if (onSet) {
- onSet(mmkv, key, newValue);
- return;
- }
- switch (typeof newValue) {
- case 'number':
- case 'string':
- case 'boolean':
- mmkv.set(key, newValue);
- break;
- case 'undefined':
- mmkv.remove(key);
- break;
- case 'object':
- if (newValue instanceof ArrayBuffer) {
- mmkv.set(key, newValue);
- break;
- } else {
- throw new Error(`MMKV: Type object (${newValue}) is not supported!`);
- }
- default:
- throw new Error(`MMKV: Type ${typeof newValue} is not supported!`);
- }
- },
- [key, mmkv, onSet]
- );
-
- useEffect(() => {
- const listener = mmkv.addOnValueChangedListener(changedKey => {
- if (changedKey === undefined || changedKey === key) {
- setBump(b => b + 1);
- }
- });
- return () => listener.remove();
- }, [key, mmkv]);
-
- return [value, set];
- };
-}
-
-/**
- * Use the string value of the given key from the given MMKV storage instance
- */
-export const useMMKVString = createMMKVHook((instance, key) => instance.getString(key));
-
-/**
- * Use the number value of the given key from the given MMKV storage instance
- */
-export const useMMKVNumber = createMMKVHook((instance, key) => instance.getNumber(key));
-
-/**
- * Use the boolean value of the given key from the given MMKV storage instance
- */
-export const useMMKVBoolean = createMMKVHook((instance, key) => instance.getBoolean(key));
-
-/**
- * Use the object value (JSON stringified) of the given key from the given MMKV storage instance
- */
-export const useMMKVObject = createMMKVHook(
- (instance, key) => {
- const stored = instance.getString(key);
- return stored ? JSON.parse(stored) : undefined;
- },
- (mmkv, key, value) => {
- if (value === undefined) {
- mmkv.remove(key);
- return;
- }
- mmkv.set(key, JSON.stringify(value));
- }
-);
-
-/**
- * Use the buffer value of the given key from the given MMKV storage instance
- */
-export const useMMKVBuffer = createMMKVHook((instance, key) => instance.getBuffer(key));
diff --git a/__mocks__/react-native-plain-text.js b/__mocks__/react-native-plain-text.js
new file mode 100644
index 00000000000..b8767cc1b5f
--- /dev/null
+++ b/__mocks__/react-native-plain-text.js
@@ -0,0 +1,3 @@
+const { Text } = require('react-native');
+
+module.exports = { PlainText: Text };
diff --git a/android/app/build.gradle b/android/app/build.gradle
index d282923fc23..ffbf6561738 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -142,9 +142,9 @@ dependencies {
implementation project(':watermelondb-jsi')
implementation "com.google.code.gson:gson:2.8.9"
- implementation "io.github.zhongwuzw:mmkv:2.2.4"
+ implementation "io.github.zhongwuzw:mmkv:2.4.1"
implementation "com.github.bumptech.glide:glide:${rootProject.ext.glideVersion}"
- implementation 'com.facebook.soloader:soloader:0.10.4'
+ implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0"
// For SecureKeystore (EncryptedSharedPreferences)
implementation 'androidx.security:security-crypto:1.1.0'
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 0e9fdce9400..c6a739a6ef3 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -4,6 +4,10 @@
+
diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
index 4ed84b04061..2e54ab1cd40 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
+++ b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
@@ -22,7 +22,7 @@ import chat.rocket.reactnative.storage.SecureStoragePackage;
import chat.rocket.reactnative.notification.VideoConfTurboPackage
import chat.rocket.reactnative.notification.PushNotificationTurboPackage
import chat.rocket.reactnative.VoipTurboPackage
-import chat.rocket.reactnative.scroll.InvertedScrollPackage
+import chat.rocket.reactnative.scroll.VisualOrderScrollPackage
import chat.rocket.reactnative.input.ExternalInputPackage
import chat.rocket.reactnative.biometric.BiometricEnrollmentPackage
@@ -50,7 +50,7 @@ open class MainApplication : Application(), ReactApplication {
add(PushNotificationTurboPackage())
add(VoipTurboPackage())
add(SecureStoragePackage())
- add(InvertedScrollPackage())
+ add(VisualOrderScrollPackage())
add(ExternalInputPackage())
add(BiometricEnrollmentPackage())
}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java b/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java
deleted file mode 100644
index 3e097bc0420..00000000000
--- a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package chat.rocket.reactnative.scroll;
-
-import android.graphics.Rect;
-import android.view.MotionEvent;
-import android.view.View;
-import com.facebook.react.views.view.ReactViewGroup;
-import java.util.ArrayList;
-import java.util.Collections;
-
-/**
- * Content view for inverted FlatLists. Reports its children to accessibility in reversed order so
- * TalkBack traversal matches the visual order (newest-first) when used inside InvertedScrollView.
- */
-public class InvertedScrollContentView extends ReactViewGroup {
-
- private boolean mIsInvertedContent = false;
-
- public InvertedScrollContentView(android.content.Context context) {
- super(context);
- }
-
- public void setIsInvertedContent(boolean isInverted) {
- mIsInvertedContent = isInverted;
- }
-
- @Override
- public void addChildrenForAccessibility(ArrayList outChildren) {
- super.addChildrenForAccessibility(outChildren);
- if (mIsInvertedContent) {
- Collections.reverse(outChildren);
- }
- }
-
- @Override
- protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
- if (mIsInvertedContent) {
- for (int i = getChildCount() - 1; i >= 0; i--) {
- View child = getChildAt(i);
- if (child.getVisibility() == VISIBLE) {
- if (child.requestFocus(direction, previouslyFocusedRect)) {
- return true;
- }
- }
- }
- return false;
- }
- return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
- }
-
- @Override
- public void addFocusables(ArrayList views, int direction, int focusableMode) {
- super.addFocusables(views, direction, focusableMode);
- if (mIsInvertedContent) {
- // Find indices of focusables that are children of this view
- ArrayList childIndices = new ArrayList<>();
- for (int i = 0; i < views.size(); i++) {
- View v = views.get(i);
- if (v.getParent() == this) {
- childIndices.add(i);
- }
- }
- // Reverse only the sublist of children focusables
- int n = childIndices.size();
- for (int i = 0; i < n / 2; i++) {
- int idx1 = childIndices.get(i);
- int idx2 = childIndices.get(n - 1 - i);
- View temp = views.get(idx1);
- views.set(idx1, views.get(idx2));
- views.set(idx2, temp);
- }
- }
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- // Match the stock vertical Android ScrollView content host, which is a plain non-clickable
- // View. This container exists only to preserve inverted accessibility/focus order and should
- // never consume fallback taps that miss transformed children after scrolling.
- return false;
- }
-}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java b/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java
deleted file mode 100644
index 095c2463424..00000000000
--- a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package chat.rocket.reactnative.scroll;
-
-import com.facebook.react.module.annotations.ReactModule;
-import com.facebook.react.uimanager.ThemedReactContext;
-import com.facebook.react.uimanager.annotations.ReactProp;
-import com.facebook.react.views.view.ReactViewManager;
-
-/**
- * View manager for InvertedScrollContentView. Behaves like a View but reports children in reversed
- * order for accessibility so TalkBack matches the visual order in inverted lists.
- */
-@ReactModule(name = InvertedScrollContentViewManager.REACT_CLASS)
-public class InvertedScrollContentViewManager extends ReactViewManager {
-
- public static final String REACT_CLASS = "InvertedScrollContentView";
-
- @Override
- public String getName() {
- return REACT_CLASS;
- }
-
- @Override
- public InvertedScrollContentView createViewInstance(ThemedReactContext context) {
- return new InvertedScrollContentView(context);
- }
-
- @ReactProp(name = "isInvertedContent")
- public void setIsInvertedContent(InvertedScrollContentView view, boolean isInverted) {
- view.setIsInvertedContent(isInverted);
- }
-}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java b/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java
deleted file mode 100644
index 30ad13084d7..00000000000
--- a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package chat.rocket.reactnative.scroll;
-
-import androidx.annotation.Nullable;
-import com.facebook.react.module.annotations.ReactModule;
-import com.facebook.react.uimanager.ThemedReactContext;
-import com.facebook.react.uimanager.annotations.ReactProp;
-import com.facebook.react.views.scroll.ReactScrollViewManager;
-
-/**
- * View manager for {@link InvertedScrollView}. Registers as "InvertedScrollView" to avoid
- * collision with core RCTScrollView. Inherits all ScrollView props from ReactScrollViewManager;
- * FlatList passes isInvertedVirtualizedList when inverted, which is applied by the parent setter.
- */
-@ReactModule(name = InvertedScrollViewManager.REACT_CLASS)
-public class InvertedScrollViewManager extends ReactScrollViewManager {
-
- public static final String REACT_CLASS = "InvertedScrollView";
-
- @Override
- public String getName() {
- return REACT_CLASS;
- }
-
- @Override
- public InvertedScrollView createViewInstance(ThemedReactContext context) {
- return new InvertedScrollView(context);
- }
-
- @ReactProp(name = "exitFocusNativeId")
- public void setExitFocusNativeId(InvertedScrollView view, @Nullable String nativeId) {
- view.setExitFocusNativeId(nativeId);
- }
-}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollPackage.java b/android/app/src/main/java/chat/rocket/reactnative/scroll/VisualOrderScrollPackage.java
similarity index 67%
rename from android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollPackage.java
rename to android/app/src/main/java/chat/rocket/reactnative/scroll/VisualOrderScrollPackage.java
index 05e6a7be0d5..2f475154882 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollPackage.java
+++ b/android/app/src/main/java/chat/rocket/reactnative/scroll/VisualOrderScrollPackage.java
@@ -7,7 +7,7 @@
import java.util.Collections;
import java.util.List;
-public class InvertedScrollPackage implements ReactPackage {
+public class VisualOrderScrollPackage implements ReactPackage {
@Override
public List createNativeModules(ReactApplicationContext reactContext) {
@@ -16,9 +16,6 @@ public List createNativeModules(ReactApplicationContext reactConte
@Override
public List createViewManagers(ReactApplicationContext reactContext) {
- List managers = new java.util.ArrayList<>();
- managers.add(new InvertedScrollViewManager());
- managers.add(new InvertedScrollContentViewManager());
- return managers;
+ return Collections.singletonList(new VisualOrderScrollViewManager());
}
}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java b/android/app/src/main/java/chat/rocket/reactnative/scroll/VisualOrderScrollView.java
similarity index 55%
rename from android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java
rename to android/app/src/main/java/chat/rocket/reactnative/scroll/VisualOrderScrollView.java
index 9ab9731390f..a9dddd487a0 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java
+++ b/android/app/src/main/java/chat/rocket/reactnative/scroll/VisualOrderScrollView.java
@@ -8,23 +8,20 @@
import androidx.annotation.Nullable;
import com.facebook.react.uimanager.util.ReactFindViewUtil;
import com.facebook.react.views.scroll.ReactScrollView;
+import java.util.ArrayList;
+import java.util.Comparator;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
-/**
- * Custom ScrollView for inverted FlatLists that corrects keyboard navigation so it follows
- * the visual order instead of the inverted view-tree order.
- *
- * Both Tab/Shift+Tab and DPAD arrows navigate between FlatList cells (direct children of the
- * content view) to avoid loops caused by inner focusable elements within a single message.
- * Boundary exit uses ReactFindViewUtil to find a tagged exit-target view by nativeID.
- */
-public class InvertedScrollView extends ReactScrollView {
+public class VisualOrderScrollView extends ReactScrollView {
+
+ private static final Comparator TOP_TO_BOTTOM = Comparator.comparingInt(VisualOrderScrollView::screenTop);
private final Map mKeyConsumedMap = new HashMap<>();
private volatile @Nullable String mExitFocusNativeId;
- public InvertedScrollView(Context context) {
+ public VisualOrderScrollView(Context context) {
super(context);
}
@@ -56,32 +53,23 @@ public boolean dispatchKeyEvent(KeyEvent event) {
return super.dispatchKeyEvent(event);
}
- /**
- * Shared navigation logic for Tab and DPAD.
- * @param isForward true = visual down (Tab / DPAD_DOWN), false = visual up (Shift+Tab / DPAD_UP)
- */
private boolean handleCellNavigation(boolean isForward) {
View focused = findFocus();
if (focused == null || getChildCount() == 0) {
return false;
}
- View firstChild = getChildAt(0);
- if (!(firstChild instanceof ViewGroup)) {
- return false;
- }
- ViewGroup contentView = (ViewGroup) firstChild;
- int cellIndex = findContainingCellIndex(contentView, focused);
- if (cellIndex < 0) {
+ View cell = findContainingCell(getChildAt(0), focused);
+ if (cell == null) {
return false;
}
- int step = isForward ? -1 : 1;
- int focusDir = isForward ? View.FOCUS_UP : View.FOCUS_DOWN;
+ List cells = visibleCellsTopToBottom((ViewGroup) cell.getParent());
+ int step = isForward ? 1 : -1;
+ int focusDirection = isForward ? View.FOCUS_DOWN : View.FOCUS_UP;
- for (int i = cellIndex + step; i >= 0 && i < contentView.getChildCount(); i += step) {
- View cell = contentView.getChildAt(i);
- if (cell != null && cell.getVisibility() == VISIBLE && cell.requestFocus(focusDir)) {
+ for (int i = cells.indexOf(cell) + step; i >= 0 && i < cells.size(); i += step) {
+ if (cells.get(i).requestFocus(focusDirection)) {
return true;
}
}
@@ -95,23 +83,43 @@ private boolean handleCellNavigation(boolean isForward) {
return false;
}
- private int findContainingCellIndex(ViewGroup contentView, View focused) {
+ private @Nullable View findContainingCell(View contentView, View focused) {
View current = focused;
- while (current != null && current.getParent() != contentView) {
- ViewParent p = current.getParent();
- if (p instanceof View) {
- current = (View) p;
- } else {
- return -1;
+ while (current != null) {
+ ViewParent parent = current.getParent();
+ if (!(parent instanceof View)) {
+ return null;
}
+ if (parent.getParent() == contentView) {
+ return current;
+ }
+ current = (View) parent;
}
- return current != null ? contentView.indexOfChild(current) : -1;
+ return null;
}
- private View findExitTarget() {
+ private List visibleCellsTopToBottom(ViewGroup cellsParent) {
+ List cells = new ArrayList<>();
+ for (int i = 0; i < cellsParent.getChildCount(); i++) {
+ View child = cellsParent.getChildAt(i);
+ if (child.getVisibility() == VISIBLE) {
+ cells.add(child);
+ }
+ }
+ cells.sort(TOP_TO_BOTTOM);
+ return cells;
+ }
+
+ private @Nullable View findExitTarget() {
if (mExitFocusNativeId != null) {
return ReactFindViewUtil.findView(getRootView(), mExitFocusNativeId);
}
return null;
}
+
+ private static int screenTop(View view) {
+ int[] location = new int[2];
+ view.getLocationOnScreen(location);
+ return location[1];
+ }
}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/scroll/VisualOrderScrollViewManager.java b/android/app/src/main/java/chat/rocket/reactnative/scroll/VisualOrderScrollViewManager.java
new file mode 100644
index 00000000000..fa038838e6f
--- /dev/null
+++ b/android/app/src/main/java/chat/rocket/reactnative/scroll/VisualOrderScrollViewManager.java
@@ -0,0 +1,28 @@
+package chat.rocket.reactnative.scroll;
+
+import androidx.annotation.Nullable;
+import com.facebook.react.module.annotations.ReactModule;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.annotations.ReactProp;
+import com.facebook.react.views.scroll.ReactScrollViewManager;
+
+@ReactModule(name = VisualOrderScrollViewManager.REACT_CLASS)
+public class VisualOrderScrollViewManager extends ReactScrollViewManager {
+
+ public static final String REACT_CLASS = "VisualOrderScrollView";
+
+ @Override
+ public String getName() {
+ return REACT_CLASS;
+ }
+
+ @Override
+ public VisualOrderScrollView createViewInstance(ThemedReactContext context) {
+ return new VisualOrderScrollView(context);
+ }
+
+ @ReactProp(name = "exitFocusNativeId")
+ public void setExitFocusNativeId(VisualOrderScrollView view, @Nullable String nativeId) {
+ view.setExitFocusNativeId(nativeId);
+ }
+}
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index 42f0071e3f1..bc79d447cc6 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/android/settings.gradle b/android/settings.gradle
index 9bb0709a8db..291937214cc 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -1,6 +1,14 @@
-pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
-plugins { id("com.facebook.react.settings") }
-extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
+pluginManagement {
+ includeBuild("../node_modules/@react-native/gradle-plugin")
+ includeBuild("../node_modules/expo-modules-autolinking/android/expo-gradle-plugin")
+}
+plugins {
+ id("com.facebook.react.settings")
+ id("expo-autolinking-settings")
+}
+extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
+ ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
+}
rootProject.name = 'RocketChatRN'
include ':watermelondb-jsi'
project(':watermelondb-jsi').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android-jsi')
@@ -8,5 +16,4 @@ include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')
-apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle");
-useExpoModules()
\ No newline at end of file
+expoAutolinking.useExpoModules()
diff --git a/app/containers/ActionSheet/ActionSheetContentWithInputAndSubmit/index.tsx b/app/containers/ActionSheet/ActionSheetContentWithInputAndSubmit/index.tsx
index ac33d69fb79..7bcf87bd2dd 100644
--- a/app/containers/ActionSheet/ActionSheetContentWithInputAndSubmit/index.tsx
+++ b/app/containers/ActionSheet/ActionSheetContentWithInputAndSubmit/index.tsx
@@ -1,5 +1,6 @@
import { useState, useRef, createRef, type ReactElement } from 'react';
-import { StyleSheet, Text, type TextInputProps, View } from 'react-native';
+import { StyleSheet, type TextInputProps, View } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import { CustomIcon, type TIconsName } from '~/containers/CustomIcon';
import i18n from '~/i18n';
@@ -167,9 +168,13 @@ const ActionSheetContentWithInputAndSubmit = ({
<>
{iconName ? : null}
- {title}
+
+ {title}
+
- {description ? {description} : null}
+ {description ? (
+ {description}
+ ) : null}
{customText}
>
{showInput ? renderInputs() : null}
diff --git a/app/containers/ActionSheet/BottomSheetContent.tsx b/app/containers/ActionSheet/BottomSheetContent.tsx
index 23a80ebbf45..e8b3a745439 100644
--- a/app/containers/ActionSheet/BottomSheetContent.tsx
+++ b/app/containers/ActionSheet/BottomSheetContent.tsx
@@ -1,4 +1,5 @@
-import { FlatList, Text, View, type ViewProps } from 'react-native';
+import { FlatList, View, type ViewProps } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import { memo, type ReactElement } from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -49,7 +50,7 @@ const BottomSheetContent = memo(
onPress={hide}
style={[styles.button, { backgroundColor: colors.surfaceHover, height }]}
accessibilityLabel={I18n.t('Cancel')}>
- {I18n.t('Cancel')}
+ {I18n.t('Cancel')}
) : null;
diff --git a/app/containers/ActionSheet/Handle.tsx b/app/containers/ActionSheet/Handle.tsx
index 874a655028c..3d4d23a1192 100644
--- a/app/containers/ActionSheet/Handle.tsx
+++ b/app/containers/ActionSheet/Handle.tsx
@@ -1,7 +1,7 @@
-import { forwardRef, type ComponentType, type Ref } from 'react';
+import { forwardRef } from 'react';
import { View } from 'react-native';
-import { RectButton } from 'react-native-gesture-handler';
+import { RectButton } from '~/containers/GestureButtons';
import styles from './styles';
import { themes } from '~/lib/constants/colors';
import { useTheme } from '~/theme';
@@ -13,7 +13,7 @@ export const Handle = forwardRef void }>(({ onPress }, re
// We should use RectButton from gesture-handler to avoid issues with the keyboard
return (
}
+ ref={ref}
onPress={onPress}
style={styles.handle}
testID='action-sheet-handle'
diff --git a/app/containers/ActionSheet/Item.tsx b/app/containers/ActionSheet/Item.tsx
index e7db8158fbe..8afcb338411 100644
--- a/app/containers/ActionSheet/Item.tsx
+++ b/app/containers/ActionSheet/Item.tsx
@@ -1,5 +1,6 @@
import { memo } from 'react';
-import { Text, View } from 'react-native';
+import { View } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import { CustomIcon } from '../CustomIcon';
import { useTheme } from '~/theme';
@@ -49,15 +50,15 @@ export const Item = memo(({ item, hide }: IActionSheetItem) => {
testID={item.testID}>
{item.icon ? : null}
-
+
{item.title}
-
+
{item?.subtitle ? (
-
{item.subtitle}
-
+
) : null}
{item.right ? {item.right ? item.right() : null} : null}
diff --git a/app/containers/ActionSheet/styles.ts b/app/containers/ActionSheet/styles.ts
index be6d260b9e3..ecbb9be14e3 100644
--- a/app/containers/ActionSheet/styles.ts
+++ b/app/containers/ActionSheet/styles.ts
@@ -39,7 +39,7 @@ export default StyleSheet.create({
margin: 8
},
backdrop: {
- ...StyleSheet.absoluteFillObject
+ ...StyleSheet.absoluteFill
},
button: {
marginHorizontal: 16,
diff --git a/app/containers/AltTextLabel.tsx b/app/containers/AltTextLabel.tsx
index 2570186dbc9..cda34144d49 100644
--- a/app/containers/AltTextLabel.tsx
+++ b/app/containers/AltTextLabel.tsx
@@ -1,4 +1,5 @@
-import { Pressable, StyleSheet, Text, View } from 'react-native';
+import { Pressable, StyleSheet, View } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import { useActionSheet } from './ActionSheet';
import I18n from '../i18n';
@@ -42,8 +43,8 @@ const AltTextActionSheetContent = ({ altText }: { altText: string }) => {
return (
- {I18n.t('Alt_text')}
- {altText}
+ {I18n.t('Alt_text')}
+ {altText}
);
};
@@ -75,7 +76,7 @@ const AltTextLabel = ({ altText, testID }: TAltTextLabelProps) => {
accessibilityRole='button'
accessibilityLabel={I18n.t('Alt_text')}
style={[styles.container, { backgroundColor: colors.surfaceNeutral }]}>
- {I18n.t('Alt')}
+ {I18n.t('Alt')}
);
};
diff --git a/app/containers/AudioPlayer/Seek.test.tsx b/app/containers/AudioPlayer/Seek.test.tsx
index ce7668c55a7..489f489763d 100644
--- a/app/containers/AudioPlayer/Seek.test.tsx
+++ b/app/containers/AudioPlayer/Seek.test.tsx
@@ -1,44 +1,48 @@
import { act, render, renderHook } from '@testing-library/react-native';
-import { Gesture, State } from 'react-native-gesture-handler';
+import { State, usePanGesture } from 'react-native-gesture-handler';
import { fireGestureHandler } from 'react-native-gesture-handler/jest-utils';
import { useSharedValue } from 'react-native-reanimated';
import Seek from './Seek';
jest.mock('~/theme', () => ({ useTheme: () => ({ colors: {} }) }));
+jest.mock('react-native-gesture-handler', () => {
+ const actual = jest.requireActual('react-native-gesture-handler');
+ return { ...actual, usePanGesture: jest.fn(actual.usePanGesture) };
+});
const setup = () => {
- const pan = jest.spyOn(Gesture, 'Pan');
const { result } = renderHook(() => ({ currentTime: useSharedValue(30), duration: useSharedValue(120) }));
const { currentTime, duration } = result.current;
const onChangeTime = jest.fn();
render();
- return { gesture: pan.mock.results[0].value, currentTime, onChangeTime };
+ const { results } = jest.mocked(usePanGesture).mock;
+ return { gesture: results[results.length - 1].value, currentTime, onChangeTime };
};
-afterEach(() => jest.restoreAllMocks());
+afterEach(() => jest.clearAllMocks());
-test('a tap that never starts dragging preserves the paused position', () => {
+test('a tap that never starts dragging preserves the paused position', async () => {
const { gesture, currentTime, onChangeTime } = setup();
- act(() => fireGestureHandler(gesture, [{ state: State.BEGAN }, { state: State.FAILED }]));
+ await act(() => fireGestureHandler(gesture, [{ state: State.BEGAN }, { state: State.FAILED }]));
expect(currentTime.value).toBe(30);
expect(onChangeTime).not.toHaveBeenCalled();
});
-test('a cancelled drag restores its starting position without seeking', () => {
+test('a cancelled drag restores its starting position without seeking', async () => {
const { gesture, currentTime, onChangeTime } = setup();
- act(() => fireGestureHandler(gesture, [{ state: State.BEGAN }, { state: State.ACTIVE }, { state: State.CANCELLED }]));
+ await act(() => fireGestureHandler(gesture, [{ state: State.BEGAN }, { state: State.ACTIVE }, { state: State.CANCELLED }]));
expect(currentTime.value).toBe(30);
expect(onChangeTime).not.toHaveBeenCalled();
currentTime.value = 45;
- act(() => fireGestureHandler(gesture, [{ state: State.BEGAN }, { state: State.FAILED }]));
+ await act(() => fireGestureHandler(gesture, [{ state: State.BEGAN }, { state: State.FAILED }]));
expect(currentTime.value).toBe(45);
});
-test('a completed drag seeks in seconds', () => {
+test('a completed drag seeks in seconds', async () => {
const { gesture, onChangeTime } = setup();
- act(() => fireGestureHandler(gesture, [{ state: State.BEGAN }, { state: State.ACTIVE }, { state: State.END }]));
+ await act(() => fireGestureHandler(gesture, [{ state: State.BEGAN }, { state: State.ACTIVE }, { state: State.END }]));
expect(onChangeTime).toHaveBeenCalledTimes(1);
expect(onChangeTime).toHaveBeenCalledWith(30);
});
diff --git a/app/containers/AudioPlayer/Seek.tsx b/app/containers/AudioPlayer/Seek.tsx
index ebef805872b..31c884f93f7 100644
--- a/app/containers/AudioPlayer/Seek.tsx
+++ b/app/containers/AudioPlayer/Seek.tsx
@@ -1,5 +1,5 @@
import { type LayoutChangeEvent, View, TextInput, type TextInputProps, TouchableNativeFeedback } from 'react-native';
-import { Gesture, GestureDetector } from 'react-native-gesture-handler';
+import { GestureDetector, usePanGesture } from 'react-native-gesture-handler';
import Animated, {
type SharedValue,
useAnimatedProps,
@@ -65,31 +65,32 @@ const Seek = ({ currentTime, duration, loaded = false, onChangeTime }: ISeek) =>
maxWidth.value = width;
};
- const panGesture = Gesture.Pan()
- .enabled(loaded)
- .activeOffsetX([-ACTIVE_OFFSET_X, ACTIVE_OFFSET_X])
- .onStart(() => {
+ const panGesture = usePanGesture({
+ enabled: loaded,
+ activeOffsetX: [-ACTIVE_OFFSET_X, ACTIVE_OFFSET_X],
+ onActivate: () => {
isPanning.value = true;
contextX.value = translateX.value;
savedTranslateX.value = translateX.value;
savedCurrentTime.value = currentTime.value;
scale.value = withTiming(1.3, { duration: 150 });
- })
- .onUpdate(event => {
+ },
+ onUpdate: event => {
const newX = contextX.value + event.translationX;
translateX.value = clamp(newX, 0, maxWidth.value);
- })
- .onFinalize((_, didSucceed) => {
+ },
+ onFinalize: event => {
if (!isPanning.value) return;
isPanning.value = false;
scale.value = withTiming(1, { duration: 150 });
- if (didSucceed) {
- scheduleOnRN(onChangeTime, currentTime.value);
- } else {
+ if (event.canceled) {
translateX.value = savedTranslateX.value;
currentTime.value = savedCurrentTime.value;
+ } else {
+ scheduleOnRN(onChangeTime, currentTime.value);
}
- });
+ }
+ });
useDerivedValue(() => {
if (isPanning.value) {
diff --git a/app/containers/Avatar/Avatar.tsx b/app/containers/Avatar/Avatar.tsx
index 926a74d841e..15c1dddd9fc 100644
--- a/app/containers/Avatar/Avatar.tsx
+++ b/app/containers/Avatar/Avatar.tsx
@@ -10,6 +10,7 @@ import { SubscriptionType } from '~/definitions';
import { type IAvatar } from './interfaces';
import I18n from '~/i18n';
import Touch from '../Touch';
+import { isIOS26OrLater } from '~/lib/methods/helpers/deviceInfo';
const Avatar = memo(
({
@@ -28,7 +29,7 @@ const Avatar = memo(
serverVersion,
text,
size = 25,
- borderRadius = 4,
+ borderRadius = isIOS26OrLater ? 8 : 4,
type = SubscriptionType.DIRECT,
avatarExternalProviderUrl,
roomAvatarExternalProviderUrl,
diff --git a/app/containers/Avatar/__snapshots__/Avatar.test.tsx.snap b/app/containers/Avatar/__snapshots__/Avatar.test.tsx.snap
index ecb8f0874b4..7be341954a9 100644
--- a/app/containers/Avatar/__snapshots__/Avatar.test.tsx.snap
+++ b/app/containers/Avatar/__snapshots__/Avatar.test.tsx.snap
@@ -34,6 +34,7 @@ exports[`Story Snapshots: AvatarBase64 should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -51,6 +52,8 @@ exports[`Story Snapshots: AvatarBase64 should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -91,6 +94,7 @@ exports[`Story Snapshots: AvatarExternalProviderUrl should match snapshot 1`] =
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -108,6 +112,8 @@ exports[`Story Snapshots: AvatarExternalProviderUrl should match snapshot 1`] =
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -148,6 +154,7 @@ exports[`Story Snapshots: AvatarPath should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -165,6 +172,8 @@ exports[`Story Snapshots: AvatarPath should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -205,6 +214,7 @@ exports[`Story Snapshots: AvatarRoomId should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -222,6 +232,8 @@ exports[`Story Snapshots: AvatarRoomId should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -262,6 +274,7 @@ exports[`Story Snapshots: AvatarText should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -279,6 +292,8 @@ exports[`Story Snapshots: AvatarText should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -319,6 +334,7 @@ exports[`Story Snapshots: AvatarUrl should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -336,6 +352,8 @@ exports[`Story Snapshots: AvatarUrl should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -376,6 +394,7 @@ exports[`Story Snapshots: Channel should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -393,6 +412,8 @@ exports[`Story Snapshots: Channel should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -433,6 +454,7 @@ exports[`Story Snapshots: Children should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -450,6 +472,8 @@ exports[`Story Snapshots: Children should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -529,6 +553,7 @@ exports[`Story Snapshots: CustomBorderRadius should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -546,6 +571,8 @@ exports[`Story Snapshots: CustomBorderRadius should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -588,6 +615,7 @@ exports[`Story Snapshots: CustomStyle should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -605,6 +633,8 @@ exports[`Story Snapshots: CustomStyle should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -645,6 +675,7 @@ exports[`Story Snapshots: Direct should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -662,6 +693,8 @@ exports[`Story Snapshots: Direct should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -712,6 +745,7 @@ exports[`Story Snapshots: Emoji should match snapshot 1`] = `
onLoadStart={[Function]}
onProgress={[Function]}
placeholder={[]}
+ sfEffect={null}
source={
[
{
@@ -726,6 +760,8 @@ exports[`Story Snapshots: Emoji should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -767,6 +803,7 @@ exports[`Story Snapshots: RoomAvatarExternalProviderUrl should match snapshot 1`
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -784,6 +821,8 @@ exports[`Story Snapshots: RoomAvatarExternalProviderUrl should match snapshot 1`
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -824,6 +863,7 @@ exports[`Story Snapshots: Static should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -841,6 +881,8 @@ exports[`Story Snapshots: Static should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -884,60 +926,63 @@ exports[`Story Snapshots: Touchable should match snapshot 1`] = `
]
}
>
-
-
-
+
`;
@@ -1021,6 +1069,7 @@ exports[`Story Snapshots: WithETag should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -1038,6 +1087,8 @@ exports[`Story Snapshots: WithETag should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -1078,6 +1129,7 @@ exports[`Story Snapshots: WithoutETag should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -1095,6 +1147,8 @@ exports[`Story Snapshots: WithoutETag should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
@@ -1135,6 +1189,7 @@ exports[`Story Snapshots: WrongServer should match snapshot 1`] = `
onProgress={[Function]}
placeholder={[]}
priority="high"
+ sfEffect={null}
source={
[
{
@@ -1152,6 +1207,8 @@ exports[`Story Snapshots: WrongServer should match snapshot 1`] = `
"width": 56,
}
}
+ symbolSize={null}
+ symbolWeight={null}
transition={null}
width={56}
/>
diff --git a/app/containers/BackgroundContainer/__snapshots__/index.test.tsx.snap b/app/containers/BackgroundContainer/__snapshots__/index.test.tsx.snap
index 0a5d30836a0..d0e6e88fac0 100644
--- a/app/containers/BackgroundContainer/__snapshots__/index.test.tsx.snap
+++ b/app/containers/BackgroundContainer/__snapshots__/index.test.tsx.snap
@@ -42,6 +42,16 @@ exports[`Story Snapshots: Basic should match snapshot 1`] = `
}
/>
+
+
+
`;
@@ -87,26 +97,37 @@ exports[`Story Snapshots: BlackThemeLoading should match snapshot 1`] = `
}
/>
-
+ contentInsetAdjustmentBehavior="automatic"
+ >
+
+
+
+
`;
@@ -152,32 +173,46 @@ exports[`Story Snapshots: BlackThemeText should match snapshot 1`] = `
}
/>
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
`;
@@ -223,26 +258,37 @@ exports[`Story Snapshots: DarkThemeLoading should match snapshot 1`] = `
}
/>
-
+ contentInsetAdjustmentBehavior="automatic"
+ >
+
+
+
+
`;
@@ -288,32 +334,46 @@ exports[`Story Snapshots: DarkThemeText should match snapshot 1`] = `
}
/>
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
`;
@@ -359,26 +419,37 @@ exports[`Story Snapshots: Loading should match snapshot 1`] = `
}
/>
-
+ contentInsetAdjustmentBehavior="automatic"
+ >
+
+
+
+
`;
@@ -424,32 +495,46 @@ exports[`Story Snapshots: LongText should match snapshot 1`] = `
}
/>
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
`;
@@ -495,31 +580,45 @@ exports[`Story Snapshots: Text should match snapshot 1`] = `
}
/>
-
- Text here
-
+
+
+ Text here
+
+
+
`;
diff --git a/app/containers/BackgroundContainer/index.tsx b/app/containers/BackgroundContainer/index.tsx
index 57a64dad1d5..364c344b930 100644
--- a/app/containers/BackgroundContainer/index.tsx
+++ b/app/containers/BackgroundContainer/index.tsx
@@ -1,5 +1,6 @@
import { type ReactElement } from 'react';
-import { ActivityIndicator, ImageBackground, StyleSheet, Text, View } from 'react-native';
+import { ActivityIndicator, ImageBackground, ScrollView, StyleSheet, View } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import { useTheme } from '~/theme';
import sharedStyles from '~/views/Styles';
@@ -36,9 +37,11 @@ const BackgroundContainer = ({ text, loading }: IBackgroundContainer): ReactElem
return (
- {text && !loading ? {text} : null}
- {/* @ts-ignore */}
- {loading ? : null}
+
+ {text && !loading ? {text} : null}
+ {/* @ts-ignore */}
+ {loading ? : null}
+
);
};
diff --git a/app/containers/Button/Button.test.tsx b/app/containers/Button/Button.test.tsx
index 06829aa76b3..e9d4e2c0c25 100644
--- a/app/containers/Button/Button.test.tsx
+++ b/app/containers/Button/Button.test.tsx
@@ -48,10 +48,9 @@ describe('ButtonTests', () => {
expect(ButtonTitle.props.children).toEqual(testProps.title);
});
- test('find button using accessibilityLabel', async () => {
- const { getByLabelText } = render();
- const Button = await getByLabelText(testProps.title);
- expect(Button).toBeTruthy();
+ test('find button using accessibilityLabel', () => {
+ const { getByTestId } = render();
+ expect(getByTestId(testProps.testID).props.accessibilityLabel).toBe(testProps.title);
});
test('title not visible while loading', async () => {
@@ -63,7 +62,7 @@ describe('ButtonTests', () => {
test('disabled button is in disabled state', async () => {
const { findByTestId } = render();
const button = await findByTestId(testProps.testID);
- expect(button.props.enabled).toBe(false);
+ expect(button).toBeDisabled();
});
test('should trigger onPress function on button press', async () => {
diff --git a/app/containers/Button/__snapshots__/Button.test.tsx.snap b/app/containers/Button/__snapshots__/Button.test.tsx.snap
index 1b0415bde25..a8ff522bd92 100644
--- a/app/containers/Button/__snapshots__/Button.test.tsx.snap
+++ b/app/containers/Button/__snapshots__/Button.test.tsx.snap
@@ -1,59 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Story Snapshots: CustomButton should match snapshot 1`] = `
-
-
Press me!
-
+
`;
exports[`Story Snapshots: DisabledButton should match snapshot 1`] = `
-
-
Press me!
-
+
`;
exports[`Story Snapshots: DisabledLoadingButton should match snapshot 1`] = `
-
-
-
+
`;
exports[`Story Snapshots: LoadingButton should match snapshot 1`] = `
-
-
-
+
`;
exports[`Story Snapshots: PrimaryButton should match snapshot 1`] = `
-
-
Press me!
-
+
`;
exports[`Story Snapshots: SecondaryButton should match snapshot 1`] = `
-
-
Press me!
-
+
`;
exports[`Story Snapshots: SmallButton should match snapshot 1`] = `
-
-
Press me!
-
+
`;
diff --git a/app/containers/Button/index.tsx b/app/containers/Button/index.tsx
index bbd625b5f1d..8e751797f73 100644
--- a/app/containers/Button/index.tsx
+++ b/app/containers/Button/index.tsx
@@ -1,13 +1,17 @@
import { type FC } from 'react';
-import { type StyleProp, StyleSheet, Text, type TextStyle, type ViewStyle } from 'react-native';
-import { RectButton, type RectButtonProps } from 'react-native-gesture-handler';
+import { type Insets, type StyleProp, StyleSheet, type TextStyle, type ViewStyle } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import { useTheme } from '~/theme';
import sharedStyles from '~/views/Styles';
import ActivityIndicator from '../ActivityIndicator';
+import { RectButton } from '../GestureButtons';
-interface IButtonProps extends Omit {
+interface IButtonProps {
title: string;
+ testID?: string;
+ hitSlop?: number | Insets;
+ accessibilityLabel?: string;
onPress: () => void;
type?: 'primary' | 'secondary';
backgroundColor?: string;
@@ -61,7 +65,9 @@ const Button: FC = ({
style,
styleText,
small,
- ...otherProps
+ testID,
+ hitSlop,
+ accessibilityLabel
}) => {
const { colors } = useTheme();
const isPrimary = type === 'primary';
@@ -90,12 +96,17 @@ const Button: FC = ({
return (
- {loading ? : {title}}
+ testID={testID}
+ hitSlop={hitSlop}
+ accessibilityLabel={accessibilityLabel ?? title}
+ accessibilityRole='button'>
+ {loading ? (
+
+ ) : (
+ {title}
+ )}
);
};
diff --git a/app/containers/CallHeader.tsx b/app/containers/CallHeader.tsx
index 51ccf7e3eb9..f89bd39ea43 100644
--- a/app/containers/CallHeader.tsx
+++ b/app/containers/CallHeader.tsx
@@ -1,5 +1,6 @@
import { type ReactElement } from 'react';
-import { StyleSheet, Text, View } from 'react-native';
+import { StyleSheet, View } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import { A11y } from 'react-native-a11y-order';
import { useAppSelector } from '../lib/hooks/useAppSelector';
@@ -44,7 +45,7 @@ export const CallHeader = ({ mic, cam, setCam, setMic, title, avatar, uid, name,
- {title}
+ {title}
{calling && direct ? : null}
@@ -54,7 +55,7 @@ export const CallHeader = ({ mic, cam, setCam, setMic, title, avatar, uid, name,
onPress={() => setCam(!cam)}
style={[style.iconCallContainerRight, { backgroundColor: handleColors(cam).button }]}
hitSlop={BUTTON_HIT_SLOP}
- enabled={!calling}>
+ disabled={calling}>
@@ -64,7 +65,7 @@ export const CallHeader = ({ mic, cam, setCam, setMic, title, avatar, uid, name,
onPress={() => setMic(!mic)}
style={[style.iconCallContainer, { backgroundColor: handleColors(mic).button }]}
hitSlop={BUTTON_HIT_SLOP}
- enabled={!calling}>
+ disabled={calling}>
@@ -73,9 +74,9 @@ export const CallHeader = ({ mic, cam, setCam, setMic, title, avatar, uid, name,
{direct ? : null}
-
+
{name}
-
+
diff --git a/app/containers/ChangePasswordRequired.tsx b/app/containers/ChangePasswordRequired.tsx
index 3433652c667..a3cc8f21fff 100644
--- a/app/containers/ChangePasswordRequired.tsx
+++ b/app/containers/ChangePasswordRequired.tsx
@@ -1,4 +1,5 @@
-import { StyleSheet, Text, View } from 'react-native';
+import { StyleSheet, View } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import { useDispatch } from 'react-redux';
import { type NavigationProp, type NavigationState } from '@react-navigation/native';
@@ -28,8 +29,10 @@ export const ChangePasswordRequired = ({ navigation }: IChangePasswordRequired)
- {I18n.t('You_need_to_change_your_password')}
- {I18n.t('To_continue_using_RocketChat')}
+
+ {I18n.t('You_need_to_change_your_password')}
+
+ {I18n.t('To_continue_using_RocketChat')}
-
-
+
+ Custom style applied
+
+
+
@@ -301,280 +235,216 @@ exports[`Story Snapshots: Default should match snapshot 1`] = `
>
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
- General
-
-
-
- A public room
-
-
-
+ ],
+ ],
+ {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ {},
+ ]
+ }
+ >
+
+
+
+ General
+
-
-
+
+ A public room
+
+
+
@@ -590,249 +460,183 @@ exports[`Story Snapshots: DirectMessage should match snapshot 1`] = `
>
-
-
-
+
+
-
-
-
-
-
-
-
- Alice Johnson
-
-
-
- Hey there!
-
-
-
+ Alice Johnson
+
-
-
+
+ Hey there!
+
+
+
@@ -848,302 +652,241 @@ exports[`Story Snapshots: LongRoomName should match snapshot 1`] = `
>
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
- This is a very very very very very very very long room name that should be truncated
-
-
-
- Long name test
-
-
-
- 9999 members
-
-
+ ],
+ ],
+ {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ {},
+ ]
+ }
+ >
+
+
+
+ This is a very very very very very very very long room name that should be truncated
+
-
-
+
+ Long name test
+
+
+
+ 9999 members
+
+
@@ -1159,185 +902,144 @@ exports[`Story Snapshots: OnlyTitle should match snapshot 1`] = `
>
-
-
-
-
-
+
-
-
-
-
-
- Just a title
-
-
-
-
+ "flexShrink": 1,
+ "fontSize": 17,
+ "fontWeight": "500",
+ },
+ {
+ "color": "#2F343D",
+ },
+ ]
+ }
+ >
+ Just a title
+
-
-
+
+
@@ -1353,280 +1055,216 @@ exports[`Story Snapshots: TeamMain should match snapshot 1`] = `
>
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
- Engineering Team
-
-
-
- Main team room
-
-
-
+ ],
+ ],
+ {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ {},
+ ]
+ }
+ >
+
+
+
+ Engineering Team
+
-
-
+
+ Main team room
+
+
+
@@ -1642,302 +1280,241 @@ exports[`Story Snapshots: WithRightLabel should match snapshot 1`] = `
>
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
- General
-
-
-
- Room with label
-
-
-
- 123 members
-
-
+ ],
+ ],
+ {
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ {},
+ ]
+ }
+ >
+
+
+
+ General
+
-
-
+
+ Room with label
+
+
+
+ 123 members
+
+
@@ -1953,241 +1530,201 @@ exports[`Story Snapshots: WithoutDescription should match snapshot 1`] = `
>
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
- No description room
-
-
-
-
+ "fontFamily": "custom",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ {},
+ ]
+ }
+ >
+
+
+
+ No description room
+
-
-
+
+
diff --git a/app/containers/DirectoryItem/index.ios.tsx b/app/containers/DirectoryItem/index.ios.tsx
new file mode 100644
index 00000000000..cc1492c1c51
--- /dev/null
+++ b/app/containers/DirectoryItem/index.ios.tsx
@@ -0,0 +1,66 @@
+import { type ReactElement } from 'react';
+import { Text, type ViewStyle } from 'react-native';
+
+import Avatar from '../Avatar';
+import RoomTypeIcon from '../RoomTypeIcon';
+import NativeListRow from '../NativeListRow';
+import { AVATAR_SIZE } from '../NativeListRow/constants';
+import styles from './styles';
+import { useTheme } from '~/theme';
+import usePreviewFormatText from '~/lib/hooks/usePreviewFormatText';
+
+export { ROW_HEIGHT } from '../NativeListRow/constants';
+
+interface IDirectoryItem {
+ title: string;
+ description?: string;
+ avatar?: string;
+ type: string;
+ onPress(): void;
+ testID: string;
+ style?: ViewStyle;
+ rightLabel?: string;
+ rid?: string;
+ teamMain?: boolean;
+ isFirst?: boolean;
+ isLast?: boolean;
+}
+
+const DirectoryItem = ({
+ title,
+ description,
+ avatar,
+ onPress,
+ testID,
+ rightLabel,
+ type,
+ rid,
+ teamMain,
+ isFirst,
+ isLast
+}: IDirectoryItem): ReactElement => {
+ const { colors } = useTheme();
+ const formattedDescription = usePreviewFormatText(description ?? '');
+ return (
+ }
+ titleLeading={type !== 'd' ? : undefined}
+ trailing={
+ rightLabel ? (
+
+ {rightLabel}
+
+ ) : undefined
+ }
+ />
+ );
+};
+
+export default DirectoryItem;
diff --git a/app/containers/DirectoryItem/index.tsx b/app/containers/DirectoryItem/index.tsx
index 77881bd3924..d239e568638 100644
--- a/app/containers/DirectoryItem/index.tsx
+++ b/app/containers/DirectoryItem/index.tsx
@@ -1,5 +1,6 @@
import { memo, type ReactElement } from 'react';
-import { PixelRatio, Text, View, type ViewStyle } from 'react-native';
+import { PixelRatio, View, type ViewStyle } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import Touch from '../Touch';
import Avatar from '../Avatar';
@@ -26,13 +27,15 @@ interface IDirectoryItem {
rightLabel?: string;
rid?: string;
teamMain?: boolean;
+ isFirst?: boolean;
+ isLast?: boolean;
}
const DirectoryItemLabel = memo(({ text, color }: IDirectoryItemLabel) => {
if (!text) {
return null;
}
- return {text};
+ return {text};
});
const DirectoryItem = ({
@@ -59,9 +62,9 @@ const DirectoryItem = ({
{type !== 'd' ? : null}
-
+
{title}
-
+
{description ? (
{
if (customEmoji) {
return ;
}
- return {formatShortnameToUnicode(`:${emoji}:`)};
+ return {formatShortnameToUnicode(`:${emoji}:`)};
};
diff --git a/app/containers/EmojiPicker/index.tsx b/app/containers/EmojiPicker/index.tsx
index 52fb60422bf..dd6a875648b 100644
--- a/app/containers/EmojiPicker/index.tsx
+++ b/app/containers/EmojiPicker/index.tsx
@@ -49,15 +49,9 @@ const EmojiPicker = ({
);
const renderTabItem = (tab: Route, color: string) => (
-
+
+
+
);
return (
diff --git a/app/containers/GestureButtons/BorderlessButton.tsx b/app/containers/GestureButtons/BorderlessButton.tsx
new file mode 100644
index 00000000000..a960f434169
--- /dev/null
+++ b/app/containers/GestureButtons/BorderlessButton.tsx
@@ -0,0 +1,44 @@
+import { Touchable } from 'react-native-gesture-handler';
+
+import { isAndroid } from '~/lib/methods/helpers/deviceInfo';
+import { type IGestureButtonProps } from './types';
+
+const BorderlessButton = ({
+ children,
+ ref,
+ onPress,
+ onLongPress,
+ disabled,
+ style,
+ hitSlop,
+ testID,
+ accessible,
+ accessibilityLabel,
+ accessibilityHint,
+ accessibilityRole,
+ accessibilityState,
+ rippleColor,
+ foreground,
+ activeOpacity = 0.3
+}: IGestureButtonProps) => (
+
+ {children}
+
+);
+
+export default BorderlessButton;
diff --git a/app/containers/GestureButtons/RectButton.tsx b/app/containers/GestureButtons/RectButton.tsx
new file mode 100644
index 00000000000..65da8156168
--- /dev/null
+++ b/app/containers/GestureButtons/RectButton.tsx
@@ -0,0 +1,50 @@
+import { Touchable } from 'react-native-gesture-handler';
+
+import { isAndroid } from '~/lib/methods/helpers/deviceInfo';
+import { type IGestureButtonProps } from './types';
+
+interface IRectButtonProps extends IGestureButtonProps {
+ underlayColor?: string;
+}
+
+const RectButton = ({
+ children,
+ ref,
+ onPress,
+ onLongPress,
+ disabled,
+ style,
+ hitSlop,
+ testID,
+ accessible,
+ accessibilityLabel,
+ accessibilityHint,
+ accessibilityRole,
+ accessibilityState,
+ rippleColor,
+ foreground,
+ underlayColor = 'black',
+ activeOpacity = 0.105
+}: IRectButtonProps) => (
+
+ {children}
+
+);
+
+export default RectButton;
diff --git a/app/containers/GestureButtons/index.ts b/app/containers/GestureButtons/index.ts
new file mode 100644
index 00000000000..868743c5260
--- /dev/null
+++ b/app/containers/GestureButtons/index.ts
@@ -0,0 +1,2 @@
+export { default as RectButton } from './RectButton';
+export { default as BorderlessButton } from './BorderlessButton';
diff --git a/app/containers/GestureButtons/types.ts b/app/containers/GestureButtons/types.ts
new file mode 100644
index 00000000000..a9060235ddf
--- /dev/null
+++ b/app/containers/GestureButtons/types.ts
@@ -0,0 +1,29 @@
+import { type ReactNode, type Ref } from 'react';
+import { type TouchableProps } from 'react-native-gesture-handler';
+import {
+ type AccessibilityRole,
+ type AccessibilityState,
+ type Insets,
+ type StyleProp,
+ type View,
+ type ViewStyle
+} from 'react-native';
+
+export interface IGestureButtonProps {
+ children?: ReactNode;
+ ref?: Ref;
+ onPress?: TouchableProps['onPress'];
+ onLongPress?: TouchableProps['onLongPress'];
+ disabled?: boolean;
+ style?: StyleProp;
+ hitSlop?: number | Insets;
+ testID?: string;
+ accessible?: boolean;
+ accessibilityLabel?: string;
+ accessibilityHint?: string;
+ accessibilityRole?: AccessibilityRole;
+ accessibilityState?: AccessibilityState;
+ rippleColor?: string;
+ foreground?: boolean;
+ activeOpacity?: number;
+}
diff --git a/app/containers/Header/components/HeaderButton/HeaderButtonItem.tsx b/app/containers/Header/components/HeaderButton/HeaderButtonItem.tsx
index 7c7c0a3224b..54394ce983b 100644
--- a/app/containers/Header/components/HeaderButton/HeaderButtonItem.tsx
+++ b/app/containers/Header/components/HeaderButton/HeaderButtonItem.tsx
@@ -1,7 +1,7 @@
import { memo, type ReactElement } from 'react';
import { Platform, StyleSheet, Text, View } from 'react-native';
-import { BorderlessButton } from 'react-native-gesture-handler';
+import { BorderlessButton } from '~/containers/GestureButtons';
import { CustomIcon, type TIconsName } from '~/containers/CustomIcon';
import { useTheme } from '~/theme';
import sharedStyles from '~/views/Styles';
@@ -55,9 +55,10 @@ const Item = memo(
}: IHeaderButtonItem): ReactElement => {
const { colors } = useTheme();
return (
-
+
-
-
-
+
-
-
+
-
+
-
-
+
@@ -758,14 +822,29 @@ exports[`Story Snapshots: Common should match snapshot 1`] = `
]
}
>
-
-
+
@@ -974,14 +1051,29 @@ exports[`Story Snapshots: Common should match snapshot 1`] = `
]
}
>
-
-
+
-
-
+
-
-
+
@@ -1640,14 +1758,29 @@ exports[`Story Snapshots: Common should match snapshot 1`] = `
]
}
>
-
-
+
@@ -1849,14 +1981,29 @@ exports[`Story Snapshots: Common should match snapshot 1`] = `
]
}
>
-
-
+
@@ -2058,14 +2204,29 @@ exports[`Story Snapshots: Common should match snapshot 1`] = `
]
}
>
-
-
+
@@ -2234,14 +2393,29 @@ exports[`Story Snapshots: Icons should match snapshot 1`] = `
]
}
>
-
-
+
-
-
+
@@ -2529,14 +2716,29 @@ exports[`Story Snapshots: Icons should match snapshot 1`] = `
]
}
>
-
-
-
+
-
+
-
-
-
+
-
+
@@ -3011,14 +3254,29 @@ exports[`Story Snapshots: Themes should match snapshot 1`] = `
]
}
>
-
-
+
-
-
+
-
-
-
+
-
+
@@ -3562,14 +3863,29 @@ exports[`Story Snapshots: Themes should match snapshot 1`] = `
]
}
>
-
-
+
-
-
+
-
-
-
+
-
+
@@ -4113,14 +4472,29 @@ exports[`Story Snapshots: Themes should match snapshot 1`] = `
]
}
>
-
-
+
-
-
+
-
-
-
+
-
+
@@ -4623,14 +5040,29 @@ exports[`Story Snapshots: Title should match snapshot 1`] = `
]
}
>
-
-
+
-
-
+
@@ -4908,14 +5353,29 @@ exports[`Story Snapshots: Title should match snapshot 1`] = `
]
}
>
-
-
-
+
-
+
-
-
-
+
-
+
diff --git a/app/containers/Header/components/HeaderTitle/index.tsx b/app/containers/Header/components/HeaderTitle/index.tsx
index 400b1adba5c..48f52621ba4 100644
--- a/app/containers/Header/components/HeaderTitle/index.tsx
+++ b/app/containers/Header/components/HeaderTitle/index.tsx
@@ -1,5 +1,6 @@
import { memo, type ReactNode } from 'react';
-import { Text, View } from 'react-native';
+import { View } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import { isAndroid } from '~/lib/methods/helpers';
import { useTheme } from '~/theme';
@@ -18,26 +19,26 @@ const HeaderTitle = memo(({ headerTitle }: IHeaderTitle) => {
if (typeof headerTitle === 'string') {
if (isAndroid) {
return (
-
{headerTitle}
-
+
);
}
return (
-
{headerTitle}
-
+
);
}
diff --git a/app/containers/ImageViewer/ImageViewer.tsx b/app/containers/ImageViewer/ImageViewer.tsx
index 5fcb5585e93..f34e3ae7af5 100644
--- a/app/containers/ImageViewer/ImageViewer.tsx
+++ b/app/containers/ImageViewer/ImageViewer.tsx
@@ -1,6 +1,12 @@
import { useRef, useState, type ReactElement } from 'react';
import { type LayoutChangeEvent, StyleSheet, type StyleProp, type ViewStyle, View } from 'react-native';
-import { Gesture, GestureDetector } from 'react-native-gesture-handler';
+import {
+ GestureDetector,
+ usePanGesture,
+ usePinchGesture,
+ useSimultaneousGestures,
+ useTapGesture
+} from 'react-native-gesture-handler';
import Animated, { withTiming, useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
import { Image, type ImageStyle } from 'expo-image';
@@ -91,36 +97,38 @@ export const ImageViewer = ({ uri = '', width, height, altText, isAnimated, ...p
return Math.max(Math.min(value, max), min);
};
- const pinchGesture = Gesture.Pinch()
- .onUpdate(event => {
+ const pinchGesture = usePinchGesture({
+ onUpdate: event => {
scale.value = clamp(scaleOffset.value * (event.scale > 0 ? event.scale : 1), 1, 4);
- })
- .onEnd(() => {
+ },
+ onDeactivate: () => {
scaleOffset.value = scale.value > 0 ? scale.value : 1;
- });
+ }
+ });
- const panGesture = Gesture.Pan()
- .maxPointers(2)
- .onStart(() => {
+ const panGesture = usePanGesture({
+ maxPointers: 2,
+ onActivate: () => {
translationX.value = offsetX.value;
translationY.value = offsetY.value;
- })
- .onUpdate(event => {
+ },
+ onUpdate: event => {
const scaleFactor = scale.value - 1;
translationX.value = clamp(event.translationX + offsetX.value, -scaleFactor * centerX, scaleFactor * centerX);
translationY.value = clamp(event.translationY + offsetY.value, -scaleFactor * centerY, scaleFactor * centerY);
- })
- .onEnd(() => {
+ },
+ onDeactivate: () => {
offsetX.value = translationX.value;
offsetY.value = translationY.value;
if (scale.value === 1) resetScaleAnimation();
- });
+ }
+ });
- const doubleTapGesture = Gesture.Tap()
- .numberOfTaps(2)
- .maxDelay(120)
- .maxDistance(70)
- .onEnd(event => {
+ const doubleTapGesture = useTapGesture({
+ numberOfTaps: 2,
+ maxDelay: 120,
+ maxDistance: 70,
+ onDeactivate: event => {
if (scaleOffset.value > 1) resetScaleAnimation();
else {
scale.value = withTiming(2, { duration: 200 });
@@ -128,9 +136,10 @@ export const ImageViewer = ({ uri = '', width, height, altText, isAnimated, ...p
offsetX.value = centerX - event.x;
scaleOffset.value = 2;
}
- });
+ }
+ });
- const gesture = Gesture.Simultaneous(pinchGesture, panGesture, doubleTapGesture);
+ const gesture = useSimultaneousGestures(pinchGesture, panGesture, doubleTapGesture);
const { colors } = useTheme();
diff --git a/app/containers/InAppNotification/IncomingCallNotification/index.tsx b/app/containers/InAppNotification/IncomingCallNotification/index.tsx
index 055de91a446..13cac6441a5 100644
--- a/app/containers/InAppNotification/IncomingCallNotification/index.tsx
+++ b/app/containers/InAppNotification/IncomingCallNotification/index.tsx
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState, memo, type ReactElement } from 'react';
-import { AccessibilityInfo, findNodeHandle, Text, View } from 'react-native';
+import { AccessibilityInfo, findNodeHandle, View } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch } from 'react-redux';
import { A11y } from 'react-native-a11y-order';
@@ -106,7 +107,7 @@ const IncomingCallHeader = memo(
}}
style={styles.cancelButton}
rectButtonStyle={styles.buttonContainer}>
- {i18n.t('decline')}
+ {i18n.t('decline')}
@@ -119,7 +120,7 @@ const IncomingCallHeader = memo(
}}
style={styles.acceptButton}
rectButtonStyle={styles.buttonContainer}>
- {i18n.t('accept')}
+ {i18n.t('accept')}
diff --git a/app/containers/InAppNotification/NotifierComponent.tsx b/app/containers/InAppNotification/NotifierComponent.tsx
index 0c806eabe6c..a7409dede9e 100644
--- a/app/containers/InAppNotification/NotifierComponent.tsx
+++ b/app/containers/InAppNotification/NotifierComponent.tsx
@@ -1,5 +1,6 @@
import { memo } from 'react';
-import { StyleSheet, Text, View } from 'react-native';
+import { StyleSheet, View } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Avatar from '../Avatar';
@@ -115,12 +116,12 @@ const NotifierComponent = memo(({ notification, isMasterDetail }: INotifierCompo
testID={`in-app-notification-${text}`}>
-
+
{title}
-
-
+
+
{text}
-
+
diff --git a/app/containers/InAppNotification/__snapshots__/NotifierComponent.test.tsx.snap b/app/containers/InAppNotification/__snapshots__/NotifierComponent.test.tsx.snap
index c8346155834..fc6125335d2 100644
--- a/app/containers/InAppNotification/__snapshots__/NotifierComponent.test.tsx.snap
+++ b/app/containers/InAppNotification/__snapshots__/NotifierComponent.test.tsx.snap
@@ -46,13 +46,29 @@ exports[`Story Snapshots: ChannelMessage should match snapshot 1`] = `
]
}
>
-
-
@@ -184,6 +191,9 @@ exports[`Story Snapshots: ChannelMessage should match snapshot 1`] = `
}
>
-
+
-
-
@@ -347,7 +363,7 @@ exports[`Story Snapshots: ChannelMessage should match snapshot 1`] = `
-
+
`;
@@ -398,13 +414,29 @@ exports[`Story Snapshots: DirectMessage should match snapshot 1`] = `
]
}
>
-
-
@@ -536,6 +559,9 @@ exports[`Story Snapshots: DirectMessage should match snapshot 1`] = `
}
>
-
+
-
-
@@ -699,7 +731,7 @@ exports[`Story Snapshots: DirectMessage should match snapshot 1`] = `
-
+
`;
@@ -750,13 +782,29 @@ exports[`Story Snapshots: WithBlackTheme should match snapshot 1`] = `
]
}
>
-
-
@@ -888,6 +927,9 @@ exports[`Story Snapshots: WithBlackTheme should match snapshot 1`] = `
}
>
-
+
-
-
@@ -1051,7 +1099,7 @@ exports[`Story Snapshots: WithBlackTheme should match snapshot 1`] = `
-
+
`;
@@ -1102,13 +1150,29 @@ exports[`Story Snapshots: WithDarkTheme should match snapshot 1`] = `
]
}
>
-
-
@@ -1240,6 +1295,9 @@ exports[`Story Snapshots: WithDarkTheme should match snapshot 1`] = `
}
>
-
+
-
-
@@ -1403,7 +1467,7 @@ exports[`Story Snapshots: WithDarkTheme should match snapshot 1`] = `
-
+
`;
diff --git a/app/containers/List/ListCheckbox.tsx b/app/containers/List/ListCheckbox.tsx
new file mode 100644
index 00000000000..fed58469189
--- /dev/null
+++ b/app/containers/List/ListCheckbox.tsx
@@ -0,0 +1,19 @@
+import { type ReactElement } from 'react';
+
+import { useTheme } from '~/theme';
+import ListIcon from './ListIcon';
+
+export interface IListCheckbox {
+ value: boolean;
+ onValueChange: (value: boolean) => void;
+ testID?: string;
+}
+
+const ListCheckbox = ({ value }: IListCheckbox): ReactElement => {
+ const { colors } = useTheme();
+ return ;
+};
+
+ListCheckbox.displayName = 'List.Checkbox';
+
+export default ListCheckbox;
diff --git a/app/containers/List/ListContainer.ios.tsx b/app/containers/List/ListContainer.ios.tsx
new file mode 100644
index 00000000000..fc53f9978a6
--- /dev/null
+++ b/app/containers/List/ListContainer.ios.tsx
@@ -0,0 +1,112 @@
+import { useState, type ReactElement } from 'react';
+import { StyleSheet, View } from 'react-native';
+import { Host } from '@expo/ui';
+import { Group, List, RNHostView } from '@expo/ui/swift-ui';
+import {
+ alignmentGuide,
+ frame,
+ listRowInsets,
+ listStyle,
+ onGeometryChange,
+ scrollContentBackground,
+ tag
+} from '@expo/ui/swift-ui/modifiers';
+
+import { useTheme } from '~/theme';
+import ListSection from './ListSection';
+import ListItem from './ListItem';
+import ListRadio from './ListRadio';
+import { isNativeListRow, isNativeListSection } from './native/rowMarkers';
+import { flattenListChildren, isListSeparator } from './listChildren';
+import { NativeListContext } from './native/context';
+import { ICON_SIZE, PADDING_HORIZONTAL } from './constants';
+
+const styles = StyleSheet.create({
+ host: {
+ flex: 1
+ }
+});
+
+const insetGroupedModifiers = [listStyle('insetGrouped')];
+const sidebarModifiers = [listStyle('sidebar')];
+const hiddenBackgroundModifiers = [scrollContentBackground('hidden')];
+
+export interface IListSelection {
+ selectedTag: string | null;
+}
+
+interface IListContainer {
+ children: (ReactElement | null)[] | ReactElement | null;
+ testID?: string;
+ selection?: IListSelection;
+ backgroundHidden?: boolean;
+}
+
+const isSection = (element: ReactElement) => element.type === ListSection || isNativeListSection(element.type);
+const isNativeRow = (element: ReactElement) =>
+ element.type === ListItem || element.type === ListRadio || isNativeListRow(element.type);
+const hasLeftIcon = (element: ReactElement) => Boolean((element.props as { left?: unknown }).left);
+const rowSelectionTag = (element: ReactElement) => {
+ const { selectionTag, testID } = element.props as { selectionTag?: string; testID?: string };
+ return selectionTag ?? testID;
+};
+
+const selectedTags = ({ selectedTag }: IListSelection) => (selectedTag ? [selectedTag] : []);
+
+const ListContainer = ({ children, testID, selection, backgroundHidden }: IListContainer) => {
+ const { theme } = useTheme();
+ const [rowWidth, setRowWidth] = useState(0);
+
+ const selectionTag = (row: ReactElement) => {
+ const rowTag = rowSelectionTag(row);
+ return rowTag && selection ? [tag(rowTag)] : [];
+ };
+
+ const rowModifiers = (row: ReactElement) => [
+ frame({ maxWidth: Number.MAX_SAFE_INTEGER }),
+ alignmentGuide('listRowSeparatorLeading', hasLeftIcon(row) ? PADDING_HORIZONTAL * 2 + ICON_SIZE : PADDING_HORIZONTAL),
+ onGeometryChange(({ width }) => setRowWidth(current => (current === width ? current : width))),
+ listRowInsets({ top: 0, leading: 0, bottom: 0, trailing: 0 }),
+ ...selectionTag(row)
+ ];
+
+ const renderHostedRow = (row: ReactElement) => (
+
+
+
+ {row}
+
+
+
+ );
+
+ const renderNativeRow = (row: ReactElement) => (
+
+ {row}
+
+ );
+
+ const renderRow = (row: ReactElement) => (isNativeRow(row) ? renderNativeRow(row) : renderHostedRow(row));
+
+ return (
+
+
+
+ {flattenListChildren(children)
+ .filter(element => !isListSeparator(element))
+ .map(element => (isSection(element) ? element : renderRow(element)))}
+
+
+
+ );
+};
+
+ListContainer.displayName = 'List.Container';
+
+export default ListContainer;
diff --git a/app/containers/List/ListContainer.tsx b/app/containers/List/ListContainer.tsx
index df3c14bdc1c..60fcf2995e2 100644
--- a/app/containers/List/ListContainer.tsx
+++ b/app/containers/List/ListContainer.tsx
@@ -13,8 +13,9 @@ const styles = StyleSheet.create({
interface IListContainer extends ScrollViewProps {
children: (ReactElement | null)[] | ReactElement | null;
testID?: string;
+ backgroundHidden?: boolean;
}
-const ListContainer = ({ children, ...props }: IListContainer) => {
+const ListContainer = ({ children, backgroundHidden: _backgroundHidden, ...props }: IListContainer) => {
const { bottom } = useSafeAreaInsets();
return (
diff --git a/app/containers/List/ListHeader.tsx b/app/containers/List/ListHeader.tsx
index f8ee321aed0..7e19f426d24 100644
--- a/app/containers/List/ListHeader.tsx
+++ b/app/containers/List/ListHeader.tsx
@@ -1,5 +1,6 @@
import { memo } from 'react';
-import { StyleSheet, Text, View } from 'react-native';
+import { StyleSheet, View } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import sharedStyles from '~/views/Styles';
import { themes } from '~/lib/constants/colors';
@@ -29,9 +30,12 @@ const ListHeader = memo(({ title, translateTitle = true, numberOfLines }: IListH
return (
-
+
{translateTitle ? I18n.t(title) : title}
-
+
);
});
diff --git a/app/containers/List/ListInfo.tsx b/app/containers/List/ListInfo.tsx
index c91ee9e4814..f715689a67c 100644
--- a/app/containers/List/ListInfo.tsx
+++ b/app/containers/List/ListInfo.tsx
@@ -1,5 +1,6 @@
import { memo } from 'react';
-import { StyleSheet, Text, View } from 'react-native';
+import { StyleSheet, View } from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import sharedStyles from '~/views/Styles';
import { themes } from '~/lib/constants/colors';
@@ -28,7 +29,7 @@ const ListInfo = memo(({ info, translateInfo = true }: IListInfo) => {
const { theme } = useTheme();
return (
- {translateInfo ? I18n.t(info) : info}
+ {translateInfo ? I18n.t(info) : info}
);
});
diff --git a/app/containers/List/ListItem.tsx b/app/containers/List/ListItem.tsx
index fb028afabd6..32798c7a443 100644
--- a/app/containers/List/ListItem.tsx
+++ b/app/containers/List/ListItem.tsx
@@ -4,12 +4,12 @@ import {
PixelRatio,
type StyleProp,
StyleSheet,
- Text,
type TextStyle,
View,
type AccessibilityRole,
type ViewStyle
} from 'react-native';
+import { PlainText } from '~/containers/PlainText';
import Touch from '../Touch';
import sharedStyles from '~/views/Styles';
@@ -17,6 +17,8 @@ import { useTheme } from '~/theme';
import I18n from '~/i18n';
import Icon from './ListIcon';
import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants';
+import { useNativeListMode } from './native/context';
+import NativeListItem from './native/Item';
import { CustomIcon } from '../CustomIcon';
import { useResponsiveLayout } from '~/lib/hooks/useResponsiveLayout/useResponsiveLayout';
import EventEmitter from '~/lib/methods/helpers/events';
@@ -75,9 +77,9 @@ const ListTitle = ({ title, color, styleTitle, translateTitle, numberOfLines }:
switch (typeof title) {
case 'string':
return (
-
+
{translateTitle && title ? I18n.t(title) : title}
-
+
);
case 'function':
return title();
@@ -196,9 +198,9 @@ const Content = memo(
) : null}
{subtitle ? (
-
+
{translateSubtitle ? I18n.t(subtitle) : subtitle}
-
+
) : null}
) : null}
@@ -242,7 +244,7 @@ const Button = memo(({ onPress, backgroundColor, underlayColor, style, ...props
onPress={handlePress}
style={[{ backgroundColor: backgroundColor || colors.surfaceRoom }, style]}
underlayColor={underlayColor}
- enabled={!props.disabled || !!props.disabledReason}>
+ disabled={props.disabled && !props.disabledReason}>
);
@@ -256,13 +258,19 @@ export interface IListItem extends Omit, Omit {
const { colors } = useTheme();
+ const nativeListMode = useNativeListMode();
+ const backgroundColor = props.backgroundColor || (nativeListMode ? 'transparent' : colors.surfaceRoom);
+
+ if (nativeListMode === 'native') {
+ return ;
+ }
if (props.onPress) {
const { onPress } = props;
- return ;
+ return ;
}
return (
-
+
);
diff --git a/app/containers/List/ListRadio.tsx b/app/containers/List/ListRadio.tsx
index fe014a45600..91eefff1124 100644
--- a/app/containers/List/ListRadio.tsx
+++ b/app/containers/List/ListRadio.tsx
@@ -4,6 +4,8 @@ import i18n from '~/i18n';
import ListItem, { type IListItem } from './ListItem';
import ListIcon from './ListIcon';
import { useTheme } from '~/theme';
+import Radio from '../Radio';
+import { useNativeListMode } from './native/context';
interface IListRadio extends IListItem {
value: any;
@@ -13,6 +15,7 @@ interface IListRadio extends IListItem {
const ListRadio = ({ value: _, isSelected, ...rest }: IListRadio) => {
const { colors } = useTheme();
+ const isNativeRow = useNativeListMode() === 'native';
const iconName = isSelected ? 'radio-checked' : 'radio-unchecked';
const iconColor = isSelected ? colors.badgeBackgroundLevel2 : colors.strokeMedium;
@@ -20,7 +23,7 @@ const ListRadio = ({ value: _, isSelected, ...rest }: IListRadio) => {
return (
}
+ right={() => (isNativeRow ? : )}
additionalAccessibilityLabel={isSelected ? i18n.t('Selected') : i18n.t('Unselected')}
accessibilityRole='radio'
/>
diff --git a/app/containers/List/ListSection.tsx b/app/containers/List/ListSection.tsx
index a326722bec3..4520e4252e5 100644
--- a/app/containers/List/ListSection.tsx
+++ b/app/containers/List/ListSection.tsx
@@ -1,7 +1,9 @@
-import { type ReactElement } from 'react';
+import { type ReactNode } from 'react';
import { StyleSheet, View } from 'react-native';
import Header from './ListHeader';
+import NativeListSection from './native/Section';
+import { useNativeListMode } from './native/context';
const styles = StyleSheet.create({
container: {
@@ -10,15 +12,25 @@ const styles = StyleSheet.create({
});
interface IListSection {
- children: (ReactElement | null)[] | ReactElement | null;
+ children: ReactNode;
title?: string;
translateTitle?: boolean;
}
const ListSection = ({ children, title, translateTitle }: IListSection) => {
+ const nativeListMode = useNativeListMode();
+
+ if (nativeListMode === 'native') {
+ return (
+
+ {children}
+
+ );
+ }
+
return (
- {title ? : null}
+ {title ? : null}
{children}
);
diff --git a/app/containers/List/ListSeparator.tsx b/app/containers/List/ListSeparator.tsx
index 442e99fdfd9..8ca44fb24ee 100644
--- a/app/containers/List/ListSeparator.tsx
+++ b/app/containers/List/ListSeparator.tsx
@@ -3,6 +3,7 @@ import { StyleSheet, View, type ViewStyle } from 'react-native';
import { themes } from '~/lib/constants/colors';
import { useTheme } from '~/theme';
+import { useNativeListMode } from './native/context';
const styles = StyleSheet.create({
separator: {
@@ -16,6 +17,11 @@ interface IListSeparator {
const ListSeparator = memo(({ style }: IListSeparator) => {
const { theme } = useTheme();
+ const isInNativeList = useNativeListMode();
+
+ if (isInNativeList) {
+ return null;
+ }
return ;
});
diff --git a/app/containers/List/__snapshots__/List.test.tsx.snap b/app/containers/List/__snapshots__/List.test.tsx.snap
index 09d7aab5b6d..693d4eac4c9 100644
--- a/app/containers/List/__snapshots__/List.test.tsx.snap
+++ b/app/containers/List/__snapshots__/List.test.tsx.snap
@@ -1,525 +1,640 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Story Snapshots: Alert should match snapshot 1`] = `
-
-
-
-
+
-
-
-
-
- Chats
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
- Chats
-
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: Header should match snapshot 1`] = `
+
+
+
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+ Chats
+
+
+
+
+
+
+
+
-
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+`;
+
+exports[`Story Snapshots: Icon should match snapshot 1`] = `
+
+
+
+
-
-
-
-
-
+
+
+
+
`;
-exports[`Story Snapshots: Header should match snapshot 1`] = `
-
-
-
-
- Chats
-
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
-
-
-
-`;
-
-exports[`Story Snapshots: Icon should match snapshot 1`] = `
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`Story Snapshots: ListItemWithRightContainerStyle should match snapshot 1`] = `
-
-
-
@@ -892,6 +847,9 @@ exports[`Story Snapshots: ListItemWithRightContainerStyle should match snapshot
}
>
-
+
`;
exports[`Story Snapshots: Pressable should match snapshot 1`] = `
-
-
-
-
+
-
-
-
-
-
-
-
- Press me
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- I'm disabled
-
-
-
-
-
-
-
-
-
-
+ },
+ ]
+ }
+ onGlobalEvent={[Function]}
+ text="I'm disabled"
+ />
+
+
+
+
+
+
+
+
`;
exports[`Story Snapshots: Radio should match snapshot 1`] = `
-
-
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- Option 1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- Option 2
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: SectionAndInfo should match snapshot 1`] = `
+
+
+
+
+
+
-
-
-
- Option 3
-
-
-
-
+
+
+
+
+
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
-
-
+
+
+
+
+
+
+
+
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`Story Snapshots: SectionAndInfo should match snapshot 1`] = `
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: Separator should match snapshot 1`] = `
+
-
+
+`;
+
+exports[`Story Snapshots: TitleAndSubtitle should match snapshot 1`] = `
+
+
-
-
+
-
-
-
-
-
-
- Section Item
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
+
+
-
-
-
- Section Item
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
- Section Item
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
- Section Item
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
-
-
- Section Item
-
-
-
-
-
-
-
-
-
-
-
- Section Item
-
-
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
-
-
-
-
-
-
-
- Section Item
-
-
-
-
-
-
-
-
-
-
-
- Section Item
-
-
-
-
-
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
-
-
-
-
-
-`;
-
-exports[`Story Snapshots: Separator should match snapshot 1`] = `
-
-
-
-
-
-`;
-
-exports[`Story Snapshots: TitleAndSubtitle should match snapshot 1`] = `
-
-
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
-
-
-
-
-
-
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
-
-
-
-
-
-
-`;
-
-exports[`Story Snapshots: WithBiggerFont should match snapshot 1`] = `
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
-`;
-
-exports[`Story Snapshots: WithBlackTheme should match snapshot 1`] = `
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
-`;
-
-exports[`Story Snapshots: WithCustomColors should match snapshot 1`] = `
-
-
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Press me!
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`Story Snapshots: WithDarkTheme should match snapshot 1`] = `
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
- Chats
-
-
-
-
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: WithBiggerFont should match snapshot 1`] = `
+
+
+
+
-
-
+
+
+
- Chats
-
-
-
-
-
-
-
-
-
+
-
-
+ {
+ "height": 46,
+ },
+ ]
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
-
- Chats
-
-
-
- All
-
-
-
-
-
+
-
-
+ {
+ "opacity": 0.3,
+ },
+ {
+ "height": 46,
+ },
+ ]
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
-
+
+
+
-
-
-
-
+
-
-
+ {
+ "height": 46,
+ },
+ ]
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
-
- Chats
-
-
-
- All
-
-
-
-
-
+
-
-
+ {
+ "opacity": 0.3,
+ },
+ {
+ "height": 46,
+ },
+ ]
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
- Chats
-
-
-
-
-
+
+
+
+
+
+
`;
-exports[`Story Snapshots: WithFlatList should match snapshot 1`] = `
+exports[`Story Snapshots: WithBlackTheme should match snapshot 1`] = `
-
-
-
-
-
-
+
-
-
+
+
+
-
-
- 0
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
-
-
- 1
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
-
-
- 2
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
-
-
- 3
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: WithCustomColors should match snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: WithDarkTheme should match snapshot 1`] = `
+
+
+
+
-
-
+
+
+
-
-
- 4
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
-
-
- 5
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
-
-
- 6
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
-
-
- 7
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: WithFlatList should match snapshot 1`] = `
+
+
+
-
-
-
-
-
- 8
-
-
-
-
-
- 9
+ 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`Story Snapshots: WithIcon should match snapshot 1`] = `
-
-
-
-
-
-
-
-
-
-
+
+
-
-
- Icon Left
-
-
-
+ "height": 0.5,
+ },
+ undefined,
+ {
+ "backgroundColor": "#CBCED1",
+ },
+ ]
+ }
+ />
-
-
-
-
- Icon Right
-
+
+
+ 1
+
+
+
+
+
+
-
-
-
+
+
+ 2
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+ "alignItems": "center",
+ "flexDirection": "row",
+ }
+ }
+ >
+
+ 3
+
+
+
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
-
+
+
+
-
-
-
+
+
+ 4
+
+
+
+
-
-
-
-
- Show Action Indicator
-
+
+
+ 5
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`Story Snapshots: WithSmallFont should match snapshot 1`] = `
-
-
-
-
+
+ 6
+
+
+
+
+
+
+
+
-
- Chats
-
+
+
+
+ 7
+
+
+
+
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
+ 8
+
-
-
+
+
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
-
-
-
-
-
-
+ 9
+
-
-
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: WithIcon should match snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
- Chats
-
-
-
-
-
-
- Chats
-
-
-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: WithSmallFont should match snapshot 1`] = `
+
+
+
+
+
-
+
+
+
-
-
+
-
-
-
-
-
-
-
-
-
- Chats
-
-
-
- All
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+ {
+ "opacity": 0.3,
+ },
+ {
+ "height": 46,
+ },
+ ]
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
-
+
+
+
-
-
+
-
-
+
-
-
+ {
+ "height": 46,
+ },
+ ]
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
-
- Chats
-
-
-
- All
-
-
-
-
-
+
-
-
+ {
+ "opacity": 0.3,
+ },
+ {
+ "height": 46,
+ },
+ ]
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
- Chats
-
-
-
-
-
+
+
+
+
+
+
`;
diff --git a/app/containers/List/__tests__/ListSection.test.tsx b/app/containers/List/__tests__/ListSection.test.tsx
new file mode 100644
index 00000000000..31d55205429
--- /dev/null
+++ b/app/containers/List/__tests__/ListSection.test.tsx
@@ -0,0 +1,43 @@
+import { Text } from 'react-native';
+import { render, screen } from '@testing-library/react-native';
+
+import ListSection from '../ListSection';
+import { NativeListContext } from '../native/context';
+import { asNativeListSection, isNativeListRow, isNativeListSection } from '../native/rowMarkers';
+
+jest.mock('../native/Section', () => {
+ const { Text: MockText } = jest.requireActual('react-native');
+ return ({ title }: { title?: string }) => {`native section ${title}`};
+});
+
+describe('ListSection', () => {
+ it('renders as a native section inside a native list', async () => {
+ await render(
+ row }}>
+
+ row
+
+
+ );
+ expect(screen.getByText('native section Calls')).toBeTruthy();
+ expect(screen.queryByText('row')).toBeNull();
+ });
+
+ it('renders its children outside a native list', async () => {
+ await render(
+
+ row
+
+ );
+ expect(screen.getByText('row')).toBeTruthy();
+ });
+});
+
+describe('asNativeListSection', () => {
+ it('marks a component as a whole section rather than a row', () => {
+ const CallRows = asNativeListSection(() => null);
+ expect(isNativeListSection(CallRows)).toBe(true);
+ expect(isNativeListRow(CallRows)).toBe(false);
+ expect(isNativeListSection(() => null)).toBe(false);
+ });
+});
diff --git a/app/containers/List/index.ts b/app/containers/List/index.ts
index 76002b160db..d893df9b54b 100644
--- a/app/containers/List/index.ts
+++ b/app/containers/List/index.ts
@@ -3,6 +3,7 @@ export { default as Item } from './ListItem';
export { default as Radio } from './ListRadio';
export { default as Section } from './ListSection';
export { default as Icon } from './ListIcon';
+export { default as Checkbox } from './ListCheckbox';
export { default as Separator } from './ListSeparator';
export { default as Header } from './ListHeader';
export { default as Info } from './ListInfo';
diff --git a/app/containers/List/listChildren.ts b/app/containers/List/listChildren.ts
new file mode 100644
index 00000000000..1419b952fed
--- /dev/null
+++ b/app/containers/List/listChildren.ts
@@ -0,0 +1,14 @@
+import { Children, cloneElement, Fragment, isValidElement, type ReactElement, type ReactNode } from 'react';
+
+import ListSeparator from './ListSeparator';
+
+export const flattenListChildren = (children: ReactNode, keyPrefix = ''): ReactElement[] =>
+ Children.toArray(children).flatMap(child => {
+ if (!isValidElement<{ children?: ReactNode }>(child)) {
+ return [];
+ }
+ const key = `${keyPrefix}${child.key}`;
+ return child.type === Fragment ? flattenListChildren(child.props.children, key) : [cloneElement(child, { key })];
+ });
+
+export const isListSeparator = (element: ReactElement) => element.type === ListSeparator;
diff --git a/app/containers/List/native/Accessory.ios.tsx b/app/containers/List/native/Accessory.ios.tsx
new file mode 100644
index 00000000000..9da0409592b
--- /dev/null
+++ b/app/containers/List/native/Accessory.ios.tsx
@@ -0,0 +1,58 @@
+import { Checkbox } from '@expo/ui';
+import { Image, RNHostView, Text, Toggle } from '@expo/ui/swift-ui';
+import { disabled, font, foregroundStyle, labelsHidden, lineLimit, tint } from '@expo/ui/swift-ui/modifiers';
+
+import { useTheme } from '~/theme';
+import NativeListIcon from './Icon.ios';
+import NativeListIndicator from './Indicator.ios';
+import NativeListStatus from './Status.ios';
+import { type TNativeListAccessory } from './describeAccessory';
+
+const NativeListAccessory = ({ accessory }: { accessory: TNativeListAccessory }) => {
+ const { colors } = useTheme();
+
+ switch (accessory.kind) {
+ case 'icon':
+ return ;
+ case 'indicator':
+ return ;
+ case 'check':
+ return (
+
+ );
+ case 'status':
+ return ;
+ case 'toggle':
+ return (
+
+ );
+ case 'checkbox':
+ return (
+
+ );
+ case 'text':
+ return (
+
+ {accessory.text}
+
+ );
+ case 'hosted':
+ return {accessory.element};
+ }
+};
+
+export default NativeListAccessory;
diff --git a/app/containers/List/native/Icon.ios.tsx b/app/containers/List/native/Icon.ios.tsx
new file mode 100644
index 00000000000..f1f6d8609b5
--- /dev/null
+++ b/app/containers/List/native/Icon.ios.tsx
@@ -0,0 +1,22 @@
+import { Text } from '@expo/ui/swift-ui';
+import { font, foregroundStyle } from '@expo/ui/swift-ui/modifiers';
+
+import { type TIconsName } from '~/containers/CustomIcon';
+import { mappedIcons } from '~/containers/CustomIcon/mappedIcons';
+import { ICON_SIZE } from '../constants';
+
+interface INativeListIcon {
+ name: TIconsName;
+ color: string;
+ size?: number;
+}
+
+const ICON_FONT_FAMILY = 'custom';
+
+const NativeListIcon = ({ name, color, size = ICON_SIZE }: INativeListIcon) => (
+
+ {String.fromCodePoint(mappedIcons[name])}
+
+);
+
+export default NativeListIcon;
diff --git a/app/containers/List/native/Indicator.ios.tsx b/app/containers/List/native/Indicator.ios.tsx
new file mode 100644
index 00000000000..b68690a07c6
--- /dev/null
+++ b/app/containers/List/native/Indicator.ios.tsx
@@ -0,0 +1,26 @@
+import { type ComponentProps } from 'react';
+import { Image } from '@expo/ui/swift-ui';
+import { font } from '@expo/ui/swift-ui/modifiers';
+
+import { useTheme } from '~/theme';
+
+export type TNativeListIndicator = 'disclosure' | 'external';
+
+const SYSTEM_NAMES: Record['systemName']> = {
+ disclosure: 'chevron.forward',
+ external: 'arrow.up.forward'
+};
+
+const NativeListIndicator = ({ indicator }: { indicator: TNativeListIndicator }) => {
+ const { colors } = useTheme();
+
+ return (
+
+ );
+};
+
+export default NativeListIndicator;
diff --git a/app/containers/List/native/Item.ios.tsx b/app/containers/List/native/Item.ios.tsx
new file mode 100644
index 00000000000..166333486ed
--- /dev/null
+++ b/app/containers/List/native/Item.ios.tsx
@@ -0,0 +1,99 @@
+import { type ReactNode } from 'react';
+import { Button, HStack, RNHostView, Spacer, Text, VStack } from '@expo/ui/swift-ui';
+import {
+ accessibilityLabel,
+ buttonStyle,
+ contentShape,
+ font,
+ foregroundStyle,
+ frame,
+ lineLimit,
+ listRowInsets,
+ opacity,
+ shapes
+} from '@expo/ui/swift-ui/modifiers';
+
+import { useTheme } from '~/theme';
+import { useResponsiveLayout } from '~/lib/hooks/useResponsiveLayout/useResponsiveLayout';
+import NativeListAccessory from './Accessory.ios';
+import NativeListIcon from './Icon.ios';
+import NativeListIndicator from './Indicator.ios';
+import { type IListItem } from '../ListItem';
+import { type INativeListItem } from './types';
+import { describeNativeListAccessory } from './describeAccessory';
+import { BASE_HEIGHT, PADDING_HORIZONTAL } from '../constants';
+import { nativeListItemAccessibilityLabel, nativeListItemSubtitle, nativeListItemTitle, pressNativeListItem } from './itemProps';
+
+const NativeListItemTitle = ({ item }: { item: IListItem }) => {
+ const { colors } = useTheme();
+ const title = nativeListItemTitle(item);
+
+ if (typeof item.title === 'function') {
+ const customTitle = item.title();
+ return customTitle ? {customTitle} : null;
+ }
+ return (
+
+
+ {title}
+
+ {item.alert ? : null}
+
+ );
+};
+
+const NativeListItemRow = ({ item, children }: { item: IListItem; children: ReactNode }) => {
+ const { fontScale } = useResponsiveLayout();
+ const rowModifiers = [
+ accessibilityLabel(nativeListItemAccessibilityLabel(item)),
+ frame({ minHeight: (item.heightContainer ?? BASE_HEIGHT) * fontScale }),
+ listRowInsets({ leading: PADDING_HORIZONTAL, trailing: PADDING_HORIZONTAL }),
+ ...(item.disabled ? [opacity(0.3)] : [])
+ ];
+
+ if (!item.onPress) {
+ return (
+
+ {children}
+
+ );
+ }
+ return (
+ pressNativeListItem(item)} testID={item.testID} modifiers={[buttonStyle('plain'), ...rowModifiers]}>
+
+ {children}
+
+
+ );
+};
+
+const NativeListItem = ({ item }: INativeListItem) => {
+ const { colors } = useTheme();
+ const subtitle = nativeListItemSubtitle(item);
+ const leading = describeNativeListAccessory(item.left?.());
+ const trailing = describeNativeListAccessory(item.right?.());
+
+ return (
+
+ {leading ? : null}
+
+
+ {subtitle ? (
+
+ {subtitle}
+
+ ) : null}
+
+
+ {trailing ? : null}
+ {item.showActionIndicator ? : null}
+
+ );
+};
+
+export default NativeListItem;
diff --git a/app/containers/List/native/Item.tsx b/app/containers/List/native/Item.tsx
new file mode 100644
index 00000000000..0a3a365637c
--- /dev/null
+++ b/app/containers/List/native/Item.tsx
@@ -0,0 +1,5 @@
+import { type INativeListItem } from './types';
+
+const NativeListItem = (_: INativeListItem) => null;
+
+export default NativeListItem;
diff --git a/app/containers/List/native/Picker.ios.tsx b/app/containers/List/native/Picker.ios.tsx
new file mode 100644
index 00000000000..e910913d871
--- /dev/null
+++ b/app/containers/List/native/Picker.ios.tsx
@@ -0,0 +1,36 @@
+import { Picker, Text } from '@expo/ui/swift-ui';
+import { font, foregroundStyle, frame, listRowInsets, pickerStyle, tag, tint } from '@expo/ui/swift-ui/modifiers';
+
+import { useTheme } from '~/theme';
+import { useResponsiveLayout } from '~/lib/hooks/useResponsiveLayout/useResponsiveLayout';
+import { BASE_HEIGHT, PADDING_HORIZONTAL } from '../constants';
+import { type INativeListPicker } from './types';
+
+const NativeListPicker = ({ title, testID, options, selection, onSelectionChange }: INativeListPicker) => {
+ const { colors } = useTheme();
+ const { fontScale } = useResponsiveLayout();
+
+ return (
+ {title}
+ }
+ modifiers={[
+ pickerStyle('menu'),
+ tint(colors.fontSecondaryInfo),
+ frame({ minHeight: BASE_HEIGHT * fontScale }),
+ listRowInsets({ leading: PADDING_HORIZONTAL, trailing: PADDING_HORIZONTAL })
+ ]}>
+ {options.map(option => (
+
+ {option.label}
+
+ ))}
+
+ );
+};
+
+export default NativeListPicker;
diff --git a/app/containers/List/native/Picker.tsx b/app/containers/List/native/Picker.tsx
new file mode 100644
index 00000000000..aaa4cc894c6
--- /dev/null
+++ b/app/containers/List/native/Picker.tsx
@@ -0,0 +1,5 @@
+import { type INativeListPicker } from './types';
+
+const NativeListPicker = (_: INativeListPicker) => null;
+
+export default NativeListPicker;
diff --git a/app/containers/List/native/Section.ios.tsx b/app/containers/List/native/Section.ios.tsx
new file mode 100644
index 00000000000..27ceb7f3d8b
--- /dev/null
+++ b/app/containers/List/native/Section.ios.tsx
@@ -0,0 +1,41 @@
+import { useContext, type ReactElement } from 'react';
+import { Section, Text } from '@expo/ui/swift-ui';
+
+import ListInfo from '../ListInfo';
+import { flattenListChildren, isListSeparator } from '../listChildren';
+import { NativeListContext } from './context';
+import { translateListText } from './itemProps';
+import { type INativeListSection } from './types';
+
+interface IInfoProps {
+ info: string;
+ translateInfo?: boolean;
+}
+
+const isInfo = (element: ReactElement): element is ReactElement => element.type === ListInfo;
+
+const NativeListSection = ({ children, title, translateTitle }: INativeListSection) => {
+ const { renderRow } = useContext(NativeListContext);
+ const elements = flattenListChildren(children).filter(element => !isListSeparator(element));
+ const infos = elements.filter(isInfo);
+ const rows = elements.filter(element => !isInfo(element));
+ const footer = infos.length ? (
+ <>
+ {infos.map(info => (
+ {translateListText(info.props.info, info.props.translateInfo)}
+ ))}
+ >
+ ) : undefined;
+
+ if (!rows.length && !infos.length) {
+ return null;
+ }
+
+ return (
+
+ {rows.map(renderRow)}
+
+ );
+};
+
+export default NativeListSection;
diff --git a/app/containers/List/native/Section.tsx b/app/containers/List/native/Section.tsx
new file mode 100644
index 00000000000..73581d1562f
--- /dev/null
+++ b/app/containers/List/native/Section.tsx
@@ -0,0 +1,5 @@
+import { type INativeListSection } from './types';
+
+const NativeListSection = (_: INativeListSection) => null;
+
+export default NativeListSection;
diff --git a/app/containers/List/native/Status.ios.tsx b/app/containers/List/native/Status.ios.tsx
new file mode 100644
index 00000000000..95f2646d389
--- /dev/null
+++ b/app/containers/List/native/Status.ios.tsx
@@ -0,0 +1,19 @@
+import { type TUserStatus } from '~/definitions';
+import { useUserStatusColor } from '~/lib/hooks/useUserStatusColor';
+import { useTheme } from '~/theme';
+import { hasIcon, type TIconsName } from '~/containers/CustomIcon';
+import NativeListIcon from './Icon.ios';
+
+const statusIconName = (status: TUserStatus): TIconsName => {
+ const name = `status-${status}`;
+ return hasIcon(name) ? (name as TIconsName) : 'status-offline';
+};
+
+const NativeListStatus = ({ status }: { status: TUserStatus }) => {
+ const { colors } = useTheme();
+ const statusColor = useUserStatusColor(status);
+
+ return ;
+};
+
+export default NativeListStatus;
diff --git a/app/containers/List/native/__tests__/describeAccessory.test.tsx b/app/containers/List/native/__tests__/describeAccessory.test.tsx
new file mode 100644
index 00000000000..3b8bbfe8d51
--- /dev/null
+++ b/app/containers/List/native/__tests__/describeAccessory.test.tsx
@@ -0,0 +1,86 @@
+import { Text, View } from 'react-native';
+
+import Avatar from '~/containers/Avatar';
+import { CustomIcon } from '~/containers/CustomIcon';
+import NewWindowIcon from '~/containers/NewWindowIcon';
+import Radio from '~/containers/Radio';
+import Status from '~/containers/Status/Status';
+import Switch from '~/containers/Switch';
+import ListCheckbox from '../../ListCheckbox';
+import ListIcon from '../../ListIcon';
+import { describeNativeListAccessory } from '../describeAccessory';
+
+describe('describeNativeListAccessory', () => {
+ it('has nothing to render for an empty slot', () => {
+ expect(describeNativeListAccessory(null)).toBeNull();
+ });
+
+ it('maps List.Icon and CustomIcon to icon font glyphs', () => {
+ expect(describeNativeListAccessory()).toEqual({ kind: 'icon', name: 'moon' });
+ expect(describeNativeListAccessory()).toEqual({
+ kind: 'icon',
+ name: 'warning',
+ color: 'red',
+ size: 20
+ });
+ });
+
+ it('ignores an empty icon color', () => {
+ expect(describeNativeListAccessory()).toEqual({
+ kind: 'icon',
+ name: 'checkbox-unchecked'
+ });
+ });
+
+ it('maps the external link icon to the external indicator', () => {
+ expect(describeNativeListAccessory()).toEqual({ kind: 'indicator', indicator: 'external' });
+ });
+
+ it('maps the chevron icon to the disclosure indicator', () => {
+ expect(describeNativeListAccessory()).toEqual({
+ kind: 'indicator',
+ indicator: 'disclosure'
+ });
+ });
+
+ it('shows a checkmark only for a checked radio', () => {
+ expect(describeNativeListAccessory()).toEqual({ kind: 'check' });
+ expect(describeNativeListAccessory()).toBeNull();
+ });
+
+ it('maps the user status', () => {
+ expect(describeNativeListAccessory()).toEqual({ kind: 'status', status: 'away' });
+ });
+
+ it('maps a Switch to a toggle keeping value, handler and disabled state', () => {
+ const onValueChange = jest.fn();
+ const accessory = describeNativeListAccessory();
+ expect(accessory).toEqual({ kind: 'toggle', isOn: true, onValueChange, disabled: true, testID: 'switch' });
+ });
+
+ it('maps List.Checkbox to a checkbox keeping value, handler and testID', () => {
+ const onValueChange = jest.fn();
+ expect(describeNativeListAccessory()).toEqual({
+ kind: 'checkbox',
+ value: true,
+ onValueChange,
+ testID: 'checkbox'
+ });
+ });
+
+ it('maps plain text, also when wrapped in a single View', () => {
+ expect(describeNativeListAccessory(Off)).toEqual({ kind: 'text', text: 'Off' });
+ expect(
+ describeNativeListAccessory(
+
+ Toasts
+
+ )
+ ).toEqual({ kind: 'text', text: 'Toasts' });
+ });
+
+ it('hosts anything else as React Native', () => {
+ const avatar = ;
+ expect(describeNativeListAccessory(avatar)).toEqual({ kind: 'hosted', element: avatar });
+ });
+});
diff --git a/app/containers/List/native/__tests__/itemProps.test.ts b/app/containers/List/native/__tests__/itemProps.test.ts
new file mode 100644
index 00000000000..9f6ff046daa
--- /dev/null
+++ b/app/containers/List/native/__tests__/itemProps.test.ts
@@ -0,0 +1,68 @@
+import EventEmitter from '~/lib/methods/helpers/events';
+import { LISTENER } from '~/containers/Toast';
+import { type IListItem } from '../../ListItem';
+import { nativeListItemAccessibilityLabel, nativeListItemTitle, pressNativeListItem } from '../itemProps';
+
+jest.mock('~/i18n', () => ({ t: (key: string) => `t:${key}` }));
+
+describe('nativeListItemTitle', () => {
+ it('translates string titles', () => {
+ expect(nativeListItemTitle({ title: 'Theme' })).toBe('t:Theme');
+ });
+
+ it('has no text for custom title components', () => {
+ expect(nativeListItemTitle({ title: () => null })).toBeUndefined();
+ });
+});
+
+describe('nativeListItemAccessibilityLabel', () => {
+ it('prefers the explicit label', () => {
+ expect(nativeListItemAccessibilityLabel({ title: 'Theme', accessibilityLabel: 'Custom' })).toBe('Custom');
+ });
+
+ it('joins translated title, subtitle and switch state', () => {
+ expect(
+ nativeListItemAccessibilityLabel({
+ title: 'Crash_report',
+ subtitle: 'Info',
+ translateSubtitle: false,
+ additionalAccessibilityLabel: true
+ })
+ ).toBe('t:Crash_report Info t:Enabled');
+ });
+
+ it('uses checked wording for checkboxes', () => {
+ expect(
+ nativeListItemAccessibilityLabel({
+ title: 'Name',
+ translateTitle: false,
+ additionalAccessibilityLabel: false,
+ additionalAccessibilityLabelCheck: true
+ })
+ ).toBe('Name t:Unchecked');
+ });
+});
+
+describe('pressNativeListItem', () => {
+ const onPress = jest.fn();
+ const item: IListItem = { title: 'Theme', onPress };
+
+ beforeEach(() => onPress.mockClear());
+
+ it('calls onPress with the title', () => {
+ pressNativeListItem(item);
+ expect(onPress).toHaveBeenCalledWith('Theme');
+ });
+
+ it('ignores presses on disabled rows', () => {
+ pressNativeListItem({ title: 'Theme', onPress, disabled: true });
+ expect(onPress).not.toHaveBeenCalled();
+ });
+
+ it('shows the disabled reason instead of pressing', () => {
+ const emit = jest.spyOn(EventEmitter, 'emit');
+ pressNativeListItem({ title: 'Theme', onPress, disabled: true, disabledReason: 'Not allowed' });
+ expect(emit).toHaveBeenCalledWith(LISTENER, { message: 'Not allowed' });
+ expect(onPress).not.toHaveBeenCalled();
+ });
+});
diff --git a/app/containers/List/native/context.ts b/app/containers/List/native/context.ts
new file mode 100644
index 00000000000..7cd85f92865
--- /dev/null
+++ b/app/containers/List/native/context.ts
@@ -0,0 +1,12 @@
+import { createContext, useContext, type ReactElement } from 'react';
+
+export type TNativeListMode = false | 'native' | 'hosted';
+
+interface INativeListContext {
+ mode: TNativeListMode;
+ renderRow: (row: ReactElement) => ReactElement;
+}
+
+export const NativeListContext = createContext({ mode: false, renderRow: row => row });
+
+export const useNativeListMode = () => useContext(NativeListContext).mode;
diff --git a/app/containers/List/native/describeAccessory.ts b/app/containers/List/native/describeAccessory.ts
new file mode 100644
index 00000000000..4836fa55740
--- /dev/null
+++ b/app/containers/List/native/describeAccessory.ts
@@ -0,0 +1,91 @@
+import { Children, isValidElement, type ReactElement, type ReactNode } from 'react';
+import { Text, View, type SwitchProps } from 'react-native';
+
+import { type TUserStatus } from '~/definitions';
+import { CustomIcon, type TIconsName } from '~/containers/CustomIcon';
+import NewWindowIcon from '~/containers/NewWindowIcon';
+import Radio from '~/containers/Radio';
+import Status from '~/containers/Status/Status';
+import Switch from '~/containers/Switch';
+import ListCheckbox, { type IListCheckbox } from '../ListCheckbox';
+import ListIcon from '../ListIcon';
+
+export type TNativeListAccessory =
+ | { kind: 'icon'; name: TIconsName; color?: string; size?: number }
+ | { kind: 'check' }
+ | { kind: 'indicator'; indicator: 'disclosure' | 'external' }
+ | { kind: 'status'; status: TUserStatus }
+ | { kind: 'toggle'; isOn: boolean; onValueChange?: (value: boolean) => void; disabled: boolean; testID?: string }
+ | { kind: 'checkbox'; value: boolean; onValueChange: (value: boolean) => void; testID?: string }
+ | { kind: 'text'; text: string }
+ | { kind: 'hosted'; element: ReactElement };
+
+interface IIconProps {
+ name: TIconsName;
+ color?: string;
+ size?: number;
+}
+
+const iconAccessory = (name: TIconsName, { color, size }: Partial): TNativeListAccessory => {
+ if (name === 'chevron-right') {
+ return { kind: 'indicator', indicator: 'disclosure' };
+ }
+ if (name === 'new-window') {
+ return { kind: 'indicator', indicator: 'external' };
+ }
+ return { kind: 'icon', name, color: color || undefined, size };
+};
+
+const textContent = (children: ReactNode) => {
+ const parts = Children.toArray(children);
+ return parts.every(part => typeof part === 'string' || typeof part === 'number') ? parts.join('') : null;
+};
+
+const onlyChild = (children: ReactNode) => {
+ const parts = Children.toArray(children);
+ return parts.length === 1 && isValidElement(parts[0]) ? parts[0] : null;
+};
+
+type TProps = Record;
+type TDescriber = (props: TProps) => TNativeListAccessory | null | undefined;
+
+const describeText: TDescriber = ({ children }) => {
+ const text = textContent(children);
+ return text === null ? undefined : { kind: 'text', text };
+};
+
+const describeToggle: TDescriber = props => {
+ const { value, onValueChange, disabled, testID } = props as SwitchProps;
+ return { kind: 'toggle', isOn: Boolean(value), onValueChange: onValueChange ?? undefined, disabled: Boolean(disabled), testID };
+};
+
+const describeCheckbox: TDescriber = props => {
+ const { value, onValueChange, testID } = props as IListCheckbox;
+ return { kind: 'checkbox', value, onValueChange, testID };
+};
+
+const describeWrapper: TDescriber = ({ children }) => {
+ const child = onlyChild(children);
+ const described = child ? describeNativeListAccessory(child) : null;
+ return described?.kind === 'hosted' ? undefined : described;
+};
+
+const describers = new Map([
+ [ListCheckbox, describeCheckbox],
+ [ListIcon, props => iconAccessory(props.name, props)],
+ [CustomIcon, props => iconAccessory(props.name, props)],
+ [NewWindowIcon, () => ({ kind: 'indicator', indicator: 'external' })],
+ [Radio, ({ check }) => (check ? { kind: 'check' } : null)],
+ [Status, ({ status }) => ({ kind: 'status', status: status ?? 'offline' })],
+ [Switch, describeToggle],
+ [Text, describeText],
+ [View, describeWrapper]
+]);
+
+export function describeNativeListAccessory(node: ReactNode): TNativeListAccessory | null {
+ if (!isValidElement(node)) {
+ return null;
+ }
+ const described = describers.get(node.type)?.(node.props);
+ return described === undefined ? { kind: 'hosted', element: node } : described;
+}
diff --git a/app/containers/List/native/itemProps.ts b/app/containers/List/native/itemProps.ts
new file mode 100644
index 00000000000..8e4ed4ae02a
--- /dev/null
+++ b/app/containers/List/native/itemProps.ts
@@ -0,0 +1,39 @@
+import I18n from '~/i18n';
+import EventEmitter from '~/lib/methods/helpers/events';
+import { LISTENER } from '~/containers/Toast';
+import { type IListItem } from '../ListItem';
+
+export const translateListText = (text: string, shouldTranslate = true) => (shouldTranslate ? I18n.t(text) : text);
+
+export const nativeListItemTitle = ({ title, translateTitle }: IListItem) =>
+ typeof title === 'string' ? translateListText(title, translateTitle) : undefined;
+
+export const nativeListItemSubtitle = ({ subtitle, translateSubtitle }: IListItem) =>
+ subtitle ? translateListText(subtitle, translateSubtitle) : undefined;
+
+const stateLabel = ({ additionalAccessibilityLabel, additionalAccessibilityLabelCheck }: IListItem) => {
+ if (typeof additionalAccessibilityLabel === 'string') {
+ return additionalAccessibilityLabel;
+ }
+ if (typeof additionalAccessibilityLabel !== 'boolean') {
+ return undefined;
+ }
+ if (additionalAccessibilityLabelCheck) {
+ return I18n.t(additionalAccessibilityLabel ? 'Checked' : 'Unchecked');
+ }
+ return I18n.t(additionalAccessibilityLabel ? 'Enabled' : 'Disabled');
+};
+
+export const nativeListItemAccessibilityLabel = (item: IListItem) =>
+ item.accessibilityLabel ??
+ [nativeListItemTitle(item), nativeListItemSubtitle(item), stateLabel(item)].filter(Boolean).join(' ');
+
+export const pressNativeListItem = ({ disabled, disabledReason, onPress, title }: IListItem) => {
+ if (disabled && disabledReason) {
+ EventEmitter.emit(LISTENER, { message: disabledReason });
+ return;
+ }
+ if (!disabled) {
+ onPress?.(title);
+ }
+};
diff --git a/app/containers/List/native/rowMarkers.ts b/app/containers/List/native/rowMarkers.ts
new file mode 100644
index 00000000000..14e3e2e595b
--- /dev/null
+++ b/app/containers/List/native/rowMarkers.ts
@@ -0,0 +1,16 @@
+import { type ComponentType } from 'react';
+
+const createComponentMarker = () => {
+ const markedTypes = new WeakSet