Compare commits

...

73 Commits

Author SHA1 Message Date
Elias Nahum
c7e29b2de8 Bump app build number to 456 (#7102) 2023-02-08 17:04:30 +02:00
Mattermost Build
57bd711836 Android fix (#7099) (#7100)
* Fix android notifications permission

* fix unsigned android build

(cherry picked from commit cb717aba0c)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-08 16:56:53 +02:00
Elias Nahum
b9b2991861 Fix android CustomPushNotificationHelper bad merge 2023-02-07 12:33:50 +02:00
Mattermost Build
7927d9ce78 Replace package and imports for Kotlin files (#7090) (#7091)
(cherry picked from commit f37a9fbabb)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-07 11:36:46 +02:00
Elias Nahum
f724e0abfa Bump app build number to 455 2023-02-07 10:47:32 +02:00
Elias Nahum
788be8478d Bump app version number to 2.0.1 2023-02-07 10:46:46 +02:00
Mattermost Build
bbc3d5ea82 Fix (#7082) (#7084)
(cherry picked from commit fab5665773)

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
2023-02-03 15:27:02 +05:30
Elias Nahum
2e87df6a8d Update RN and deps to fix ANR issues (#7078) 2023-02-02 14:44:31 +02:00
Elias Nahum
89be2be00e Fix the animation that occurs in login flow (#7054) 2023-02-02 12:16:27 +02:00
Elias Nahum
75c56a993f Support for Android Tablets & Foldable (#7025)
* Add Support for Android tablets & foldables

* add tablet and book posture

* Regenerate disposed observable on WindowInfoTracker
2023-02-02 12:15:43 +02:00
Elias Nahum
8d267f320d iPad: enable rotation in all directions (#7007)
* iPad: enable rotation in all directions

* feedback review
2023-02-02 12:14:27 +02:00
Elias Nahum
48b5b6099c Update Dependencies and bug fixes (#7000)
* update dependencies

* update dependencies

* feedback review

* update @mattermost/react-native-turbo-mailer
2023-02-02 12:13:09 +02:00
Elias Nahum
9b4c31bacf New UI for Emoji picker (#6933)
* BottomSheet migration to react-native-bottom-sheet

* Refactor Emoji picker to use bottom sheet

* Add skin selector

* Add Emoji Skin Tone tutorial

* add selected indicator to tone selector

* feedback review

* show tooltip after 750ms

* ux feedback review

* ux feedback review #2

* Hide emoji picker scroll indicator
2023-02-02 10:58:42 +02:00
Mattermost Build
e6bb2b826f Use timeout defaults for iOS Share Extension and Notification Service (#7051) (#7073)
* Use timeout defaults for iOS Share Extension and Notification Service

* more logs

* Add more logs, handle errors and safe parse the filename

(cherry picked from commit 5aaff10664)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-01 21:28:39 +02:00
Elias Nahum
2cec2f02f0 Fix lint after cherry-pick 2023-01-31 22:17:19 +02:00
Elias Nahum
cf2a29e219 Only fetchMissingDirectChannelsInfo when no display name is set (#7060) 2023-01-31 22:10:46 +02:00
Elias Nahum
26801c2516 Request permissions for Android push notifications and refactor code to use network client (#7059) 2023-01-31 21:46:16 +02:00
Daniel Espino García
e876942892 Fix add to default category code for dms and gms (#7057) (#7066) 2023-01-31 17:01:39 +01:00
Elias Nahum
210642f287 Fix the animation that occurs in login flow (#7055) 2023-01-30 11:58:26 +02:00
Avinash Lingaloo
e635d04505 Bump app build number to 454 (#7042)
Co-authored-by: Mattermost Build <build@mattermost.com>
2023-01-27 22:24:40 +02:00
Mattermost Build
245f89815e Fixes crashes and errors in iOS Share Extension and Notification Service (#7032) (#7047)
* Fix erros & crashes in iOS share extension

* Fix erros & crashes in iOS notification service

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
(cherry picked from commit ca14631487)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 22:20:19 +02:00
Elias Nahum
ec99c8bc0d disable top domain level verification (#7045) 2023-01-27 22:07:57 +02:00
Daniel Espino García
0a1c1a8bf7 Fix pdf upload and pdf download from search results (#6984) 2023-01-27 20:19:48 +02:00
Mattermost Build
ec2aeca0d0 catch exceptions in Android Database helper (#7027) (#7040)
(cherry picked from commit 7ed2e73a91)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 18:58:29 +02:00
Mattermost Build
cfb09ce7d7 Do not access record children directly to avoid crashes if the child is not present in the db (#7028) (#7036)
(cherry picked from commit 50b845452e)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 18:10:45 +02:00
Mattermost Build
ee7b4f05d5 Fix crash when dismissing notification on android (#7029) (#7035)
* Fix crash when dismissing notification on android

* ensure notification channels are created

(cherry picked from commit 9bae53b4ad)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 18:10:20 +02:00
Mattermost Build
069eaa9f52 Fix matchDeepLink when server is on a subpath (#7010) (#7018)
* Fixes matchDeepLink when server is in a subpath

* Fix matchDeepLink when serverUrl is in a subpath

(cherry picked from commit 983d0aab66)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-26 20:32:52 +02:00
Mattermost Build
4764a76c9f Remove mock locations from jailbrake detection (#7005) (#7017)
(cherry picked from commit 4aaf08b12a)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-26 20:32:36 +02:00
Mattermost Build
70119fc026 Bump app build number to 452 (#6974) (#6975)
(cherry picked from commit d417b95643)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-13 21:16:22 +02:00
Mattermost Build
0d9c6e0ad3 Fix the caret position when using the search phrase modifier (#6972) (#6973)
(cherry picked from commit 49fc180982)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-13 21:15:11 +02:00
Mattermost Build
f7d8ed9e1f Bump app build number to 451 (#6967) (#6968)
(cherry picked from commit 9411dbd669)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-12 13:07:36 +02:00
Mattermost Build
b8cc13d7fa Fix (#6960) (#6966)
(cherry picked from commit c8ded6ef3c)

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
2023-01-12 13:00:51 +02:00
Mattermost Build
317568b4c8 Fix some issues found by Sentry (#6962) (#6965)
(cherry picked from commit bb351c7376)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-12 11:11:14 +02:00
Mattermost Build
55f919dd27 Add String description in log arguments (#6961) (#6964)
(cherry picked from commit 247d8371d9)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-12 11:08:12 +02:00
Mattermost Build
da1b3dc71d Add back wrongly removed Ephemeral.theme assignment (#6959) (#6963)
(cherry picked from commit cab863d62f)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-12 11:03:57 +02:00
Elias Nahum
57a9ff31bf fix schema test 2023-01-11 22:23:03 +02:00
Elias Nahum
4f86a87bdc fix ci 2023-01-11 22:14:01 +02:00
Mattermost Build
9ab21b2f62 Bump build number to 450 (#6950) (#6955)
* Fix upgrade path

* Introduce Upgrade helper

* Reset server database schema version to 1

* Enable release builds on the CI

* Bump build number to 450

(cherry picked from commit 4199b13843)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-11 21:45:47 +02:00
Mattermost Build
1934945d72 Fix code syntax highlight on full screen view (#6944) (#6954)
(cherry picked from commit 8edf128d59)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2023-01-11 21:45:14 +02:00
Mattermost Build
5162e6b6e7 Fix connection banner showing when not needed (#6948) (#6953)
* Fix connection banner showing when not needed

* Fix some issues and some refactoring

(cherry picked from commit 6082a6a790)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2023-01-11 21:44:31 +02:00
Guillermo Vayá
9139a26967 Merge pull request #6951 from mattermost/weblate-6947
Weblate 6947
2023-01-11 13:14:49 +01:00
Kaya Zeren
56fbb3d842 Translated using Weblate (Turkish)
Currently translated at 93.6% (879 of 939 strings)

Translation: mattermost-languages-shipped/mattermost-mobile-v2
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/tr/
2023-01-11 12:53:17 +01:00
정성근
56349f865f Translated using Weblate (Korean)
Currently translated at 59.2% (556 of 939 strings)

Translation: mattermost-languages-shipped/mattermost-mobile-v2
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/ko/
2023-01-11 12:53:01 +01:00
Elias Nahum
fdf593bcec Fix navigation theming (#6946) 2023-01-11 09:54:07 +02:00
Elias Nahum
8e2e016a6c check for analytics enabled 2023-01-09 11:14:15 +02:00
Elias Nahum
4d9bc1fbed Bump app build number to 449 (#6940) 2023-01-07 18:44:27 +02:00
Elias Nahum
7351c7ccac fix Sentry import 2023-01-07 18:39:26 +02:00
Elias Nahum
107416109a Bump app build number to 448 (#6939) 2023-01-07 13:55:27 +02:00
Elias Nahum
c1abccf3ed update deps (#6938) 2023-01-07 13:43:33 +02:00
Elias Nahum
977bcd2b42 Fix sentry inner crash and have watermelon report in which table it cannot update records with pending changes 2023-01-07 12:49:48 +02:00
Elias Nahum
68ec90df90 Bump app build number to 447 (#6937) 2023-01-06 21:46:07 +02:00
Elias Nahum
c8d9758f18 CI fixes (#6936)
* CI fixes

* Fix fastlane script
2023-01-06 21:41:03 +02:00
Elias Nahum
8097ee300d Fix crashes found in NotificationService (#6935) 2023-01-06 21:35:40 +02:00
Elias Nahum
c2aaef2e14 Preserve new message line on WS reconnect (#6934) 2023-01-06 13:38:40 +04:00
Joseph Baylon
d465edefc3 Detox Maintenance: Skip tests that are unstable and cleanup known issues (#6931)
* Detox Reporting: Prepare for release and main

* Fix lint in switch

* Detox Maintenance: Skip unstable tests

* Skipped unstable and known issue tests

* Fix inline latex test

* Remove skipped from failures

* Remove unnecessary report failure

* Remove assert import

* Remove FAILURE_MESSAGE

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2023-01-05 14:43:09 -08:00
Elias Nahum
ca1f6df1c6 Generate video thumb from file url instead of public url (#6922) 2023-01-05 13:49:04 +02:00
Elias Nahum
001a6699fb Cache push notification profile image and add logs (#6932)
* Cache push notification profile image and add logs

* Fix indent
2023-01-05 12:31:52 +02:00
Elias Nahum
dbe565319d BottomSheet migration to react-native-bottom-sheet (#6907)
* BottomSheet migration to react-native-bottom-sheet

* Use correct scroll view for announcement bottom sheet

* ux review

* Fix post options bottom sheet snap point

* feedback review
2023-01-05 09:51:51 +02:00
Joseph Baylon
e9b8160f31 Detox Reporting: Prepare for release and main (#6930)
* Detox Reporting: Prepare for release and main

* Fix lint in switch

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2023-01-05 10:46:31 +08:00
Elias Nahum
3b289c306e Display own DM avatar in channel intro (#6924) 2023-01-04 15:28:16 +02:00
Elias Nahum
03dd4477da Fix onViewableItemsChanged 2023-01-04 14:42:43 +02:00
Joseph Baylon
2ba7bda0b9 Detox Report: Add commit hash to report (#6929) 2023-01-04 11:38:27 +08:00
Anurag Shivarathri
ed69eacbe7 Count fix (#6926) 2023-01-04 00:00:23 +02:00
Elias Nahum
5ff22953ab update dependencies (#6923) 2023-01-03 23:58:57 +02:00
Elias Nahum
0c4e554534 Use Promise.allSettle when fetching emojis (#6921) 2023-01-03 23:45:50 +02:00
Elias Nahum
411a7e22a2 Reduce the amount of queries to display the PostList (#6927) 2023-01-03 23:36:31 +02:00
Ji-Hyeon Gim
7df9801cdc Translated using Weblate (Korean)
Currently translated at 58.9% (554 of 939 strings)

Translation: mattermost-languages-shipped/mattermost-mobile-v2
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/ko/
2023-01-03 18:01:03 +02:00
정성근
bdf5ad87c3 Translated using Weblate (Korean)
Currently translated at 58.8% (553 of 939 strings)

Translation: mattermost-languages-shipped/mattermost-mobile-v2
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/ko/
2023-01-03 18:01:03 +02:00
정성근
3fe3d09b04 Translated using Weblate (Korean)
Currently translated at 58.8% (553 of 939 strings)

Translation: mattermost-languages-shipped/mattermost-mobile-v2
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/ko/
2023-01-03 18:01:03 +02:00
Tom De Moor
62a07e606f Translated using Weblate (Dutch)
Currently translated at 100.0% (939 of 939 strings)

Translation: mattermost-languages-shipped/mattermost-mobile-v2
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/nl/
2023-01-03 18:01:03 +02:00
MArtin Johnson
1a551c2104 Translated using Weblate (Swedish)
Currently translated at 100.0% (939 of 939 strings)

Translation: mattermost-languages-shipped/mattermost-mobile-v2
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/sv/
2023-01-03 18:01:03 +02:00
MArtin Johnson
43d151f0af Translated using Weblate (Swedish)
Currently translated at 98.8% (928 of 939 strings)

Translation: mattermost-languages-shipped/mattermost-mobile-v2
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/sv/
2023-01-03 18:01:03 +02:00
Csaba Tóth // BDSC Business Digitalisation Kft
fbfc126148 Translated using Weblate (Hungarian)
Currently translated at 41.0% (385 of 939 strings)

Translation: mattermost-languages-shipped/mattermost-mobile-v2
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/hu/
2023-01-03 18:01:03 +02:00
544 changed files with 10466 additions and 7920 deletions

View File

@@ -39,7 +39,7 @@ commands:
steps:
- add_ssh_keys:
fingerprints:
- "59:4d:99:5e:1c:6d:30:36:6d:60:76:88:ff:a7:ab:63"
- "03:1c:a7:07:35:bc:57:e4:1d:6c:e1:2c:4b:be:09:6d"
- run:
name: Clone the mobile private repo
command: git clone git@github.com:mattermost/mattermost-mobile-private.git ~/mattermost-mobile-private
@@ -309,13 +309,13 @@ jobs:
- save:
filename: "*.apk"
# build-android-release:
# executor: android
# steps:
# - build-android
# - persist
# - save:
# filename: "*.apk"
build-android-release:
executor: android
steps:
- build-android
- persist
- save:
filename: "*.apk"
build-android-pr:
executor: android
@@ -326,27 +326,27 @@ jobs:
- save:
filename: "*.apk"
# build-android-unsigned:
# executor: android
# steps:
# - checkout:
# path: ~/mattermost-mobile
# - npm-dependencies
# - assets
# - fastlane-dependencies:
# for: android
# - gradle-dependencies
# - run:
# name: Jetify Android libraries
# command: ./node_modules/.bin/jetify
# - run:
# working_directory: fastlane
# name: Run fastlane to build unsigned android
# no_output_timeout: 30m
# command: bundle exec fastlane android unsigned
# - persist
# - save:
# filename: "*.apk"
build-android-unsigned:
executor: android
steps:
- checkout:
path: ~/mattermost-mobile
- npm-dependencies
- assets
- fastlane-dependencies:
for: android
- gradle-dependencies
- run:
name: Jetify Android libraries
command: ./node_modules/.bin/jetify
- run:
working_directory: fastlane
name: Run fastlane to build unsigned android
no_output_timeout: 30m
command: bundle exec fastlane android unsigned
- persist
- save:
filename: "*.apk"
build-ios-beta:
executor:
@@ -358,13 +358,13 @@ jobs:
- save:
filename: "*.ipa"
# build-ios-release:
# executor: ios
# steps:
# - build-ios
# - persist
# - save:
# filename: "*.ipa"
build-ios-release:
executor: ios
steps:
- build-ios
- persist
- save:
filename: "*.ipa"
build-ios-pr:
executor: ios
@@ -375,63 +375,64 @@ jobs:
- save:
filename: "*.ipa"
# build-ios-unsigned:
# executor: ios
# steps:
# - checkout:
# path: ~/mattermost-mobile
# - npm-dependencies
# - pods-dependencies
# - assets
# - fastlane-dependencies:
# for: ios
# - run:
# working_directory: fastlane
# name: Run fastlane to build unsigned iOS
# no_output_timeout: 30m
# command: |
# HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
# bundle exec fastlane ios unsigned
# - persist_to_workspace:
# root: ~/
# paths:
# - mattermost-mobile/*.ipa
# - save:
# filename: "*.ipa"
build-ios-unsigned:
executor: ios
steps:
- checkout:
path: ~/mattermost-mobile
- npm-dependencies
- pods-dependencies
- assets
- fastlane-dependencies:
for: ios
- run:
working_directory: fastlane
name: Run fastlane to build unsigned iOS
no_output_timeout: 30m
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
bundle exec fastlane ios unsigned
- persist_to_workspace:
root: ~/
paths:
- mattermost-mobile/*.ipa
- save:
filename: "*.ipa"
# build-ios-simulator:
# executor: ios
# steps:
# - checkout:
# path: ~/mattermost-mobile
# - npm-dependencies
# - pods-dependencies
# - assets
# - fastlane-dependencies:
# for: ios
# - run:
# working_directory: fastlane
# name: Run fastlane to build unsigned x86_64 iOS app for iPhone simulator
# no_output_timeout: 30m
# command: |
# HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
# bundle exec fastlane ios simulator
# - persist_to_workspace:
# root: ~/
# paths:
# - mattermost-mobile/Mattermost-simulator-x86_64.app.zip
# - save:
# filename: "Mattermost-simulator-x86_64.app.zip"
build-ios-simulator:
executor: ios
steps:
- checkout:
path: ~/mattermost-mobile
- npm-dependencies
- pods-dependencies
- assets
- fastlane-dependencies:
for: ios
- run:
working_directory: fastlane
name: Run fastlane to build unsigned x86_64 iOS app for iPhone simulator
no_output_timeout: 30m
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
bundle exec fastlane ios simulator
- persist_to_workspace:
root: ~/
paths:
- mattermost-mobile/Mattermost-simulator-x86_64.app.zip
- save:
filename: "Mattermost-simulator-x86_64.app.zip"
# deploy-android-release:
# executor:
# name: android
# resource_class: medium
# steps:
# - deploy-to-store:
# task: "Deploy to Google Play"
# target: android
# file: "*.apk"
deploy-android-release:
executor:
name: android
resource_class: medium
steps:
- deploy-to-store:
task: "Deploy to Google Play"
target: android
file: "*.apk"
env: "SUPPLY_TRACK=beta"
deploy-android-beta:
executor:
@@ -444,13 +445,14 @@ jobs:
file: "*.apk"
env: "SUPPLY_TRACK=alpha"
# deploy-ios-release:
# executor: ios
# steps:
# - deploy-to-store:
# task: "Deploy to TestFlight"
# target: ios
# file: "*.ipa"
deploy-ios-release:
executor: ios
steps:
- deploy-to-store:
task: "Deploy to TestFlight"
target: ios
file: "*.ipa"
env: ""
deploy-ios-beta:
executor: ios
@@ -461,17 +463,17 @@ jobs:
file: "*.ipa"
env: ""
# github-release:
# executor:
# name: android
# resource_class: medium
# steps:
# - attach_workspace:
# at: ~/
# - run:
# name: Create GitHub release
# working_directory: fastlane
# command: bundle exec fastlane github
github-release:
executor:
name: android
resource_class: medium
steps:
- attach_workspace:
at: ~/
- run:
name: Create GitHub release
working_directory: fastlane
command: bundle exec fastlane github
workflows:
version: 2
@@ -483,26 +485,24 @@ workflows:
# requires:
# - test
# - build-android-release:
# context: mattermost-mobile-android-release
# requires:
# - test
# filters:
# branches:
# only:
# - /^build-\d+$/
# - /^build-android-\d+$/
# - /^build-android-release-\d+$/
# - deploy-android-release:
# context: mattermost-mobile-android-release
# requires:
# - build-android-release
# filters:
# branches:
# only:
# - /^build-\d+$/
# - /^build-android-\d+$/
# - /^build-android-release-\d+$/
- build-android-release:
context: mattermost-mobile-android-release
requires:
- test
filters:
branches:
only:
- /^build-release-\d+$/
- /^build-android-release-\d+$/
- deploy-android-release:
context: mattermost-mobile-android-release
requires:
- build-android-release
filters:
branches:
only:
- /^build-release-\d+$/
- /^build-android-release-\d+$/
- build-android-beta:
context: mattermost-mobile-android-beta
@@ -523,26 +523,24 @@ workflows:
- /^build-android-\d+$/
- /^build-android-beta-\d+$/
# - build-ios-release:
# context: mattermost-mobile-ios-release
# requires:
# - test
# filters:
# branches:
# only:
# - /^build-\d+$/
# - /^build-ios-\d+$/
# - /^build-ios-release-\d+$/
# - deploy-ios-release:
# context: mattermost-mobile-ios-release
# requires:
# - build-ios-release
# filters:
# branches:
# only:
# - /^build-\d+$/
# - /^build-ios-\d+$/
# - /^build-ios-release-\d+$/
- build-ios-release:
context: mattermost-mobile-ios-release
requires:
- test
filters:
branches:
only:
- /^build-release-\d+$/
- /^build-ios-release-\d+$/
- deploy-ios-release:
context: mattermost-mobile-ios-release
requires:
- build-ios-release
filters:
branches:
only:
- /^build-release-\d+$/
- /^build-ios-release-\d+$/
- build-ios-beta:
context: mattermost-mobile-ios-beta
@@ -578,43 +576,41 @@ workflows:
branches:
only: /^(build|ios)-pr-.*/
# - build-android-unsigned:
# context: mattermost-mobile-unsigned
# requires:
# - test
# filters:
# tags:
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
# branches:
# only: unsigned
# - build-ios-unsigned:
# context: mattermost-mobile-unsigned
# requires:
# - test
# filters:
# tags:
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
# branches:
# only: unsigned
# - build-ios-simulator:
# context: mattermost-mobile-unsigned
# requires:
# - test
# filters:
# branches:
# only:
# - /^build-\d+$/
# - /^build-ios-\d+$/
# - /^build-ios-beta-\d+$/
# - /^build-ios-sim-\d+$/
- build-android-unsigned:
context: mattermost-mobile-unsigned
requires:
- test
filters:
tags:
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
branches:
only: unsigned
- build-ios-unsigned:
context: mattermost-mobile-unsigned
requires:
- test
filters:
tags:
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
branches:
only: unsigned
- build-ios-simulator:
context: mattermost-mobile-unsigned
requires:
- test
filters:
branches:
only:
- /^build-\d+$/
- /^build-ios-sim-\d+$/
# - github-release:
# context: mattermost-mobile-unsigned
# requires:
# - build-android-unsigned
# - build-ios-unsigned
# filters:
# tags:
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
# branches:
# only: unsigned
- github-release:
context: mattermost-mobile-unsigned
requires:
- build-android-unsigned
- build-ios-unsigned
filters:
tags:
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
branches:
only: unsigned

11
.gitignore vendored
View File

@@ -46,6 +46,8 @@ local.properties
*.iml
*.hprof
.cxx/
*.keystore
!debug.keystore
android/app/bin
android/app/build
android/build
@@ -61,12 +63,6 @@ npm-debug.log
yarn-error.log
.yarninstall
# BUCK
buck-out/
\.buckd/
android/app/libs
*.keystore
# Vim
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
@@ -114,3 +110,6 @@ launch.json
# Notice.txt generation
!build/notice-file
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*

View File

@@ -2962,28 +2962,6 @@ IN THE SOFTWARE.
"""
---
## reanimated-bottom-sheet
This product contains a modified version of 'reanimated-bottom-sheet' by Michał Osadnik.
Highly configurable component imitating native bottom sheet behavior, with fully native 60 FPS animations!
* HOMEPAGE:
* https://github.com/osdnk/react-native-reanimated-bottom-sheet
* LICENSE: MIT
Copyright 2019 present Michał Osadnik
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## semver

View File

@@ -1,65 +0,0 @@
# To learn about Buck see [Docs](https://buckbuild.com/).
# To run your application with Buck:
# - install Buck
# - `npm start` - to start the packager
# - `cd android`
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
# - `buck install -r android/app` - compile, install and run application
#
lib_deps = []
for jarfile in glob(['libs/*.jar']):
name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')]
lib_deps.append(':' + name)
prebuilt_jar(
name = name,
binary_jar = jarfile,
)
for aarfile in glob(['libs/*.aar']):
name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')]
lib_deps.append(':' + name)
android_prebuilt_aar(
name = name,
aar = aarfile,
)
android_library(
name = "all-libs",
exported_deps = lib_deps,
)
android_library(
name = "app-code",
srcs = glob([
"src/main/java/**/*.java",
]),
deps = [
":all-libs",
":build_config",
":res",
],
)
android_build_config(
name = "build_config",
package = "com.mattermost.rnbeta",
)
android_resource(
name = "res",
package = "com.mattermost.rnbeta",
res = "src/main/res",
)
android_binary(
name = "app",
keystore = "//android/keystores:debug",
manifest = "src/main/AndroidManifest.xml",
package_type = "debug",
deps = [
":app-code",
],
)

View File

@@ -1,88 +1,56 @@
apply plugin: "com.android.application"
apply plugin: "com.facebook.react"
apply plugin: 'kotlin-android'
import com.android.build.OutputFile
import org.apache.tools.ant.taskdefs.condition.Os
/**
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
* and bundleReleaseJsAndAssets).
* These basically call `react-native bundle` with the correct arguments during the Android build
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
* bundle directly from the development server. Below you can see all the possible configurations
* and their defaults. If you decide to add a configuration block, make sure to add it before the
* `apply from: "../../node_modules/react-native/react.gradle"` line.
*
* project.ext.react = [
* // the name of the generated asset file containing your JS bundle
* bundleAssetName: "index.android.bundle",
*
* // the entry file for bundle generation. If none specified and
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
* // default. Can be overridden with ENTRY_FILE environment variable.
* entryFile: "index.android.js",
*
* // whether to bundle JS and assets in debug mode
* bundleInDebug: false,
*
* // whether to bundle JS and assets in release mode
* bundleInRelease: true,
*
* // whether to bundle JS and assets in another build variant (if configured).
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
* // The configuration property can be in the following formats
* // 'bundleIn${productFlavor}${buildType}'
* // 'bundleIn${buildType}'
* // bundleInFreeDebug: true,
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
*
* // whether to disable dev mode in custom build variants (by default only disabled in release)
* // for example: to disable dev mode in the staging build type (if configured)
* devDisabledInStaging: true,
* // The configuration property can be in the following formats
* // 'devDisabledIn${productFlavor}${buildType}'
* // 'devDisabledIn${buildType}'
*
* // the root of your project, i.e. where "package.json" lives
* root: "../../",
*
* // where to put the JS bundle asset in debug mode
* jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
*
* // where to put the JS bundle asset in release mode
* jsBundleDirRelease: "$buildDir/intermediates/assets/release",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in debug mode
* resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in release mode
* resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
*
* // by default the gradle tasks are skipped if none of the JS files or assets change; this means
* // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
* // date; if you have any other folders that you want to ignore for performance reasons (gradle
* // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
* // for example, you might want to remove it from here.
* inputExcludes: ["android/**", "ios/**"],
*
* // override which node gets called and with what additional arguments
* nodeExecutableAndArgs: ["node"],
*
* // supply additional arguments to the packager
* extraPackagerArgs: []
* ]
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
project.ext.react = [
entryFile: "index.ts",
bundleConfig: "metro.config.js",
bundleCommand: "bundle",
enableHermes: true,
]
react {
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '..'
// root = file("../")
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
// reactNativeDir = file("../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen
// codegenDir = file("../node_modules/react-native-codegen")
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
// cliFile = file("../node_modules/react-native/cli.js")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The command to run when bundling. By default is 'bundle'
// bundleCommand = "ram-bundle"
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
entryFile = file("../../index.ts")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
}
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
if (System.getenv("SENTRY_ENABLED") == "true") {
@@ -95,33 +63,35 @@ if (System.getenv("SENTRY_ENABLED") == "true") {
}
/**
* Set this to true to create two separate APKs instead of one:
* - An APK that only works on ARM devices
* - An APK that only works on x86 devices
* The advantage is the size of the APK is reduced by about 4MB.
* Upload all the APKs to the Play Store and people will download
* the correct one based on the CPU architecture of their device.
* Set this to true to create four separate APKs instead of one,
* one for each native architecture. This is useful if you don't
* use App Bundles (https://developer.android.com/guide/app-bundle/)
* and want to have separate APKs to upload to the Play Store
*/
def enableSeparateBuildPerCPUArchitecture = project.hasProperty('separateApk') ? project.property('separateApk').toBoolean() : false
/**
* Run Proguard to shrink the Java bytecode in release builds.
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/
def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc-intl:+'
/**
* Whether to enable the Hermes VM.
*
* This should be set on project.ext.react and that value will be read here. If it is not set
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
* and the benefits of using Hermes will therefore be sharply reduced.
*/
def enableHermes = project.ext.react.get("enableHermes", false);
/**
* Architectures to build native code for.
* Private function to get the list of Native Architectures you want to build.
* This reads the value from reactNativeArchitectures in your gradle.properties
* file and works together with the --active-arch-only flag of react-native run-android.
*/
def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
@@ -131,84 +101,21 @@ def reactNativeArchitectures() {
android {
ndkVersion rootProject.ext.ndkVersion
compileSdkVersion rootProject.ext.compileSdkVersion
namespace "com.mattermost.rnbeta"
lintOptions {
checkReleaseBuilds false
abortOnError false
}
packagingOptions {
pickFirst '**/libc++_shared.so'
}
defaultConfig {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 446
versionName "2.0.0"
versionCode 456
versionName "2.0.1"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
if (isNewArchitectureEnabled()) {
// We configure the CMake build only if you decide to opt-in for the New Architecture.
externalNativeBuild {
cmake {
arguments "-DPROJECT_BUILD_DIR=$buildDir",
"-DREACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
"-DREACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build",
"-DNODE_MODULES_DIR=$rootDir/../node_modules",
"-DANDROID_STL=c++_shared"
}
}
if (!enableSeparateBuildPerCPUArchitecture) {
ndk {
abiFilters (*reactNativeArchitectures())
}
}
}
}
if (isNewArchitectureEnabled()) {
// We configure the NDK build only if you decide to opt-in for the New Architecture.
externalNativeBuild {
cmake {
path "$projectDir/src/main/jni/CMakeLists.txt"
}
}
def reactAndroidProjectDir = project(':ReactAndroid').projectDir
def packageReactNdkDebugLibs = tasks.register("packageReactNdkDebugLibs", Copy) {
dependsOn(":ReactAndroid:packageReactNdkDebugLibsForBuck")
from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib")
into("$buildDir/react-ndk/exported")
}
def packageReactNdkReleaseLibs = tasks.register("packageReactNdkReleaseLibs", Copy) {
dependsOn(":ReactAndroid:packageReactNdkReleaseLibsForBuck")
from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib")
into("$buildDir/react-ndk/exported")
}
afterEvaluate {
// If you wish to add a custom TurboModule or component locally,
// you should uncomment this line.
// preBuild.dependsOn("generateCodegenArtifactsFromSchema")
preDebugBuild.dependsOn(packageReactNdkDebugLibs)
preReleaseBuild.dependsOn(packageReactNdkReleaseLibs)
// Due to a bug inside AGP, we have to explicitly set a dependency
// between configureCMakeDebug* tasks and the preBuild tasks.
// This can be removed once this is solved: https://issuetracker.google.com/issues/207403732
configureCMakeDebugRelease.dependsOn(preReleaseBuild)
configureCMakeDebugDebug.dependsOn(preDebugBuild)
reactNativeArchitectures().each { architecture ->
tasks.findByName("configureCMakeDebugDebug[${architecture}]")?.configure {
dependsOn("preDebugBuild")
}
tasks.findByName("configureCMakeDebugRelease[${architecture}]")?.configure {
dependsOn("preReleaseBuild")
}
}
}
}
signingConfigs {
@@ -281,70 +188,40 @@ repositories {
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
//noinspection GradleDynamicVersio
implementation "com.facebook.react:react-native:+" // From node_modules
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
exclude group:'com.squareup.okhttp3', module:'okhttp'
}
if (enableHermes) {
//noinspection GradleDynamicVersion
implementation("com.facebook.react:hermes-engine:+") { // From node_modules
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'androidx.window:window-rxjava3:1.0.0'
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation project(':reactnativenotifications')
implementation "com.google.firebase:firebase-messaging:$firebaseVersion"
// For animated GIF support
implementation 'com.facebook.fresco:fresco:2.6.0'
implementation 'com.facebook.fresco:animated-gif:2.6.0'
// For WebP support, including animated WebP
implementation 'com.facebook.fresco:animated-webp:2.6.0'
implementation 'com.facebook.fresco:webpsupport:2.6.0'
androidTestImplementation('com.wix:detox:+')
implementation project(':reactnativenotifications')
implementation project(':watermelondb-jsi')
}
configurations.all {
if (isNewArchitectureEnabled()) {
// If new architecture is enabled, we let you build RN from source
// Otherwise we fallback to a prebuilt .aar bundled in the NPM package.
// This will be applied to all the imported transtitive dependency.
resolutionStrategy.dependencySubstitution {
substitute(module("com.facebook.react:react-native"))
.using(project(":ReactAndroid"))
.because("On New Architecture we're building React Native from source")
substitute(module("com.facebook.react:hermes-engine"))
.using(project(":ReactAndroid:hermes-engine"))
.because("On New Architecture we're building Hermes from source")
}
}
resolutionStrategy {
force "com.facebook.soloader:soloader:0.10.1"
eachDependency { DependencyResolveDetails details ->
if (details.requested.name == 'play-services-base') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
@@ -359,13 +236,13 @@ configurations.all {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'okhttp') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.2'
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
}
if (details.requested.name == 'okhttp-tls') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.2'
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
}
if (details.requested.name == 'okhttp-urlconnection') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.2'
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
}
}
}
@@ -380,11 +257,3 @@ task copyDownloadableDepsToLibs(type: Copy) {
apply plugin: 'com.google.gms.google-services'
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
def isNewArchitectureEnabled() {
// To opt-in for the New Architecture, you can either:
// - Set `newArchEnabled` to true inside the `gradle.properties` file
// - Invoke gradle with `-newArchEnabled=true`
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
}

BIN
android/app/debug.keystore Normal file

Binary file not shown.

View File

@@ -4,7 +4,7 @@
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.rn;
package com.mattermost.flipper;
import android.content.Context;
import com.facebook.flipper.android.AndroidFlipperClient;
@@ -17,19 +17,22 @@ import com.facebook.flipper.plugins.inspector.DescriptorMapping;
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.react.ReactInstanceEventListener;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.network.NetworkingModule;
import okhttp3.OkHttpClient;
/**
* Class responsible of loading Flipper inside your React Native application. This is the debug
* flavor of it. Here you can add your own plugins and customize the Flipper setup.
*/
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
if (FlipperUtils.shouldEnableFlipper(context)) {
final FlipperClient client = AndroidFlipperClient.getInstance(context);
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
client.addPlugin(new ReactFlipperPlugin());
client.addPlugin(new DatabasesFlipperPlugin(context));
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
client.addPlugin(CrashReporterPlugin.getInstance());

View File

@@ -1,10 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mattermost.rnbeta">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
@@ -12,6 +12,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission android:name="android.permission.BLUETOOTH"

View File

@@ -0,0 +1,28 @@
package com.mattermost.helpers
import android.graphics.Bitmap
import android.util.LruCache
class BitmapCache {
private var memoryCache: LruCache<String, Bitmap>
init {
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8
memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
return bitmap.byteCount / 1024
}
}
}
fun getBitmapFromMemCache(key: String): Bitmap? {
return memoryCache.get(key)
}
fun addBitmapToMemoryCache(key: String, bitmap: Bitmap) {
if (getBitmapFromMemCache(key) == null) {
memoryCache.put(key, bitmap)
}
}
}

View File

@@ -7,7 +7,6 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
@@ -28,7 +27,6 @@ import androidx.core.app.Person;
import androidx.core.app.RemoteInput;
import androidx.core.graphics.drawable.IconCompat;
import com.facebook.react.bridge.ReactApplicationContext;
import com.mattermost.rnbeta.*;
import java.io.IOException;
@@ -53,11 +51,16 @@ public class CustomPushNotificationHelper {
private static NotificationChannel mHighImportanceChannel;
private static NotificationChannel mMinImportanceChannel;
private static void addMessagingStyleMessages(Context context, NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
private static final OkHttpClient client = new OkHttpClient();
private static final BitmapCache bitmapCache = new BitmapCache();
private static void addMessagingStyleMessages(NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
String message = bundle.getString("message", bundle.getString("body"));
String senderId = bundle.getString("sender_id");
String serverUrl = bundle.getString("server_url");
String type = bundle.getString("type");
String urlOverride = bundle.getString("override_icon_url");
if (senderId == null) {
senderId = "sender_id";
}
@@ -74,7 +77,7 @@ public class CustomPushNotificationHelper {
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
try {
Bitmap avatar = userAvatar(context, serverUrl, senderId, null);
Bitmap avatar = userAvatar(serverUrl, senderId, urlOverride);
if (avatar != null) {
sender.setIcon(IconCompat.createWithBitmap(avatar));
}
@@ -176,12 +179,12 @@ public class CustomPushNotificationHelper {
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
addNotificationExtras(notification, bundle);
setNotificationIcons(context, notification, bundle);
setNotificationMessagingStyle(context, notification, bundle);
setNotificationIcons(notification, bundle);
setNotificationMessagingStyle(notification, bundle);
setNotificationGroup(notification, groupId, createSummary);
setNotificationBadgeType(notification);
setNotificationChannel(notification, bundle);
setNotificationChannel(context, notification);
setNotificationDeleteIntent(context, notification, bundle, notificationId);
addNotificationReplyAction(context, notification, bundle, notificationId);
@@ -253,19 +256,12 @@ public class CustomPushNotificationHelper {
return title;
}
private static int getIconResourceId(Context context, String iconName) {
final Resources res = context.getResources();
String packageName = context.getPackageName();
String defType = "mipmap";
return res.getIdentifier(iconName, defType, packageName);
}
private static NotificationCompat.MessagingStyle getMessagingStyle(Context context, Bundle bundle) {
private static NotificationCompat.MessagingStyle getMessagingStyle(Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle;
final String senderId = "me";
final String serverUrl = bundle.getString("server_url");
final String type = bundle.getString("type");
String urlOverride = bundle.getString("override_icon_url");
Person.Builder sender = new Person.Builder()
.setKey(senderId)
@@ -273,7 +269,7 @@ public class CustomPushNotificationHelper {
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
try {
Bitmap avatar = userAvatar(context, serverUrl, "me", null);
Bitmap avatar = userAvatar(serverUrl, "me", urlOverride);
if (avatar != null) {
sender.setIcon(IconCompat.createWithBitmap(avatar));
}
@@ -286,7 +282,7 @@ public class CustomPushNotificationHelper {
String conversationTitle = getConversationTitle(bundle);
setMessagingStyleConversationTitle(messagingStyle, conversationTitle, bundle);
addMessagingStyleMessages(context, messagingStyle, conversationTitle, bundle);
addMessagingStyleMessages(messagingStyle, conversationTitle, bundle);
return messagingStyle;
}
@@ -313,25 +309,6 @@ public class CustomPushNotificationHelper {
return getConversationTitle(bundle);
}
public static int getSmallIconResourceId(Context context, String iconName) {
if (iconName == null) {
iconName = "ic_notification";
}
int resourceId = getIconResourceId(context, iconName);
if (resourceId == 0) {
iconName = "ic_launcher";
resourceId = getIconResourceId(context, iconName);
if (resourceId == 0) {
resourceId = android.R.drawable.ic_dialog_info;
}
}
return resourceId;
}
private static String removeSenderNameFromMessage(String message, String senderName) {
int index = message.indexOf(senderName);
if (index == 0) {
@@ -363,12 +340,15 @@ public class CustomPushNotificationHelper {
}
}
private static void setNotificationChannel(NotificationCompat.Builder notification, Bundle bundle) {
private static void setNotificationChannel(Context context, NotificationCompat.Builder notification) {
// If Android Oreo or above we need to register a channel
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
if (mHighImportanceChannel == null) {
createNotificationChannels(context);
}
NotificationChannel notificationChannel = mHighImportanceChannel;
notification.setChannelId(notificationChannel.getId());
}
@@ -384,8 +364,8 @@ public class CustomPushNotificationHelper {
notification.setDeleteIntent(deleteIntent);
}
private static void setNotificationMessagingStyle(Context context, NotificationCompat.Builder notification, Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(context, bundle);
private static void setNotificationMessagingStyle(NotificationCompat.Builder notification, Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(bundle);
notification.setStyle(messagingStyle);
}
@@ -398,20 +378,18 @@ public class CustomPushNotificationHelper {
}
}
private static void setNotificationIcons(Context context, NotificationCompat.Builder notification, Bundle bundle) {
String smallIcon = bundle.getString("smallIcon");
private static void setNotificationIcons(NotificationCompat.Builder notification, Bundle bundle) {
String channelName = getConversationTitle(bundle);
String senderName = bundle.getString("sender_name");
String serverUrl = bundle.getString("server_url");
String urlOverride = bundle.getString("override_icon_url");
int smallIconResId = getSmallIconResourceId(context, smallIcon);
notification.setSmallIcon(smallIconResId);
notification.setSmallIcon(R.mipmap.ic_notification);
if (serverUrl != null && channelName.equals(senderName)) {
try {
String senderId = bundle.getString("sender_id");
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
Bitmap avatar = userAvatar(serverUrl, senderId, urlOverride);
if (avatar != null) {
notification.setLargeIcon(avatar);
}
@@ -421,29 +399,31 @@ public class CustomPushNotificationHelper {
}
}
private static Bitmap userAvatar(Context context, final String serverUrl, final String userId, final String urlOverride) throws IOException {
private static Bitmap userAvatar(final String serverUrl, final String userId, final String urlOverride) throws IOException {
try {
final OkHttpClient client = new OkHttpClient();
Request request;
String url;
if (urlOverride != null) {
request = new Request.Builder().url(urlOverride).build();
Response response;
if (!TextUtils.isEmpty(urlOverride)) {
Request request = new Request.Builder().url(urlOverride).build();
Log.i("ReactNative", String.format("Fetch override profile image %s", urlOverride));
response = client.newCall(request).execute();
} else {
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
url = String.format("%s/api/v4/users/%s/image", serverUrl, userId);
Bitmap cached = bitmapCache.getBitmapFromMemCache(userId);
if (cached != null) {
Bitmap bitmap = cached.copy(cached.getConfig(), false);
return getCircleBitmap(bitmap);
}
String url = String.format("api/v4/users/%s/image", userId);
Log.i("ReactNative", String.format("Fetch profile image %s", url));
request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.url(url)
.build();
response = Network.getSync(serverUrl, url, null);
}
Response response = client.newCall(request).execute();
if (response.code() == 200) {
assert response.body() != null;
byte[] bytes = Objects.requireNonNull(response.body()).bytes();
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (TextUtils.isEmpty(urlOverride) && !TextUtils.isEmpty(userId)) {
bitmapCache.addBitmapToMemoryCache(userId, bitmap.copy(bitmap.getConfig(), false));
}
return getCircleBitmap(bitmap);
}

View File

@@ -20,13 +20,18 @@ class DatabaseHelper {
val onlyServerUrl: String?
get() {
val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''"
val cursor = defaultDatabase!!.rawQuery(query)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
try {
val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''"
val cursor = defaultDatabase!!.rawQuery(query)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
}
return null
}
@@ -38,14 +43,19 @@ class DatabaseHelper {
}
fun getServerUrlForIdentifier(identifier: String): String? {
val args: Array<Any?> = arrayOf(identifier)
val query = "SELECT url FROM Servers WHERE identifier=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
try {
val args: Array<Any?> = arrayOf(identifier)
val query = "SELECT url FROM Servers WHERE identifier=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
}
return null
}
@@ -63,19 +73,25 @@ class DatabaseHelper {
return resultMap
}
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
fun getDatabaseForServer(context: Context?, serverUrl: String): Database? {
val args: Array<Any?> = arrayOf(serverUrl)
val query = "SELECT db_path FROM Servers WHERE url=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val databasePath = cursor.getString(0)
cursor.close()
return Database(databasePath, context!!)
try {
val args: Array<Any?> = arrayOf(serverUrl)
val query = "SELECT db_path FROM Servers WHERE url=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val databasePath = cursor.getString(0)
cursor.close()
return Database(databasePath, context!!)
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
}
return null
}
@@ -148,18 +164,23 @@ class DatabaseHelper {
}
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
if (db != null) {
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
if (cursor1.count == 1) {
cursor1.moveToFirst()
val lastFetchedAt = cursor1.getDouble(0)
cursor1.close()
if (lastFetchedAt == 0.0) {
return queryLastPostCreateAt(db, channelId)
try {
if (db != null) {
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
if (cursor1.count == 1) {
cursor1.moveToFirst()
val lastFetchedAt = cursor1.getDouble(0)
cursor1.close()
if (lastFetchedAt == 0.0) {
return queryLastPostCreateAt(db, channelId)
}
return lastFetchedAt
}
return lastFetchedAt
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
}
return null
}

View File

@@ -12,6 +12,7 @@ import com.mattermost.networkclient.APIClientModule;
import com.mattermost.networkclient.enums.RetryTypes;
import okhttp3.HttpUrl;
import okhttp3.Response;
public class Network {
@@ -35,6 +36,16 @@ public class Network {
clientModule.post(baseUrl, endpoint, options, promise);
}
public static Response getSync(String baseUrl, String endpoint, ReadableMap options) {
createClientIfNeeded(baseUrl);
return clientModule.getSync(baseUrl, endpoint, options);
}
public static Response postSync(String baseUrl, String endpoint, ReadableMap options) {
createClientIfNeeded(baseUrl);
return clientModule.postSync(baseUrl, endpoint, options);
}
private static void createClientOptions() {
WritableMap headers = Arguments.createMap();
headers.putString("X-Requested-With", "XMLHttpRequest");

View File

@@ -145,9 +145,9 @@ public class NotificationHelper {
for (final StatusBarNotification status : statusNotifications) {
Bundle bundle = status.getNotification().extras;
if (isThreadNotification) {
hasMore = bundle.getString("root_id").equals(rootId);
hasMore = bundle.containsKey("root_id") && bundle.getString("root_id").equals(rootId);
} else {
hasMore = bundle.getString("channel_id").equals(channelId);
hasMore = bundle.containsKey("channel_id") && bundle.getString("channel_id").equals(channelId);
}
if (hasMore) break;
}

View File

@@ -1,116 +0,0 @@
package com.mattermost.newarchitecture;
import android.app.Application;
import androidx.annotation.NonNull;
import com.facebook.react.PackageList;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.ReactPackageTurboModuleManagerDelegate;
import com.facebook.react.bridge.JSIModulePackage;
import com.facebook.react.bridge.JSIModuleProvider;
import com.facebook.react.bridge.JSIModuleSpec;
import com.facebook.react.bridge.JSIModuleType;
import com.facebook.react.bridge.JavaScriptContextHolder;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.UIManager;
import com.facebook.react.fabric.ComponentFactory;
import com.facebook.react.fabric.CoreComponentsRegistry;
import com.facebook.react.fabric.FabricJSIModuleProvider;
import com.facebook.react.fabric.ReactNativeConfig;
import com.facebook.react.uimanager.ViewManagerRegistry;
import com.mattermost.rnbeta.BuildConfig;
import com.mattermost.newarchitecture.components.MainComponentsRegistry;
import com.mattermost.newarchitecture.modules.MainApplicationTurboModuleManagerDelegate;
import java.util.ArrayList;
import java.util.List;
/**
* A {@link ReactNativeHost} that helps you load everything needed for the New Architecture, both
* TurboModule delegates and the Fabric Renderer.
*
* <p>Please note that this class is used ONLY if you opt-in for the New Architecture (see the
* `newArchEnabled` property). Is ignored otherwise.
*/
public class MainApplicationReactNativeHost extends ReactNativeHost {
public MainApplicationReactNativeHost(Application application) {
super(application);
}
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
// TurboModules must also be loaded here providing a valid TurboReactPackage implementation:
// packages.add(new TurboReactPackage() { ... });
// If you have custom Fabric Components, their ViewManagers should also be loaded here
// inside a ReactPackage.
return packages;
}
@Override
protected String getJSMainModuleName() {
return "index";
}
@NonNull
@Override
protected ReactPackageTurboModuleManagerDelegate.Builder
getReactPackageTurboModuleManagerDelegateBuilder() {
// Here we provide the ReactPackageTurboModuleManagerDelegate Builder. This is necessary
// for the new architecture and to use TurboModules correctly.
return new MainApplicationTurboModuleManagerDelegate.Builder();
}
@Override
protected JSIModulePackage getJSIModulePackage() {
return new JSIModulePackage() {
@Override
public List<JSIModuleSpec> getJSIModules(
final ReactApplicationContext reactApplicationContext,
final JavaScriptContextHolder jsContext) {
final List<JSIModuleSpec> specs = new ArrayList<>();
// Here we provide a new JSIModuleSpec that will be responsible of providing the
// custom Fabric Components.
specs.add(
new JSIModuleSpec() {
@Override
public JSIModuleType getJSIModuleType() {
return JSIModuleType.UIManager;
}
@Override
public JSIModuleProvider<UIManager> getJSIModuleProvider() {
final ComponentFactory componentFactory = new ComponentFactory();
CoreComponentsRegistry.register(componentFactory);
// Here we register a Components Registry.
// The one that is generated with the template contains no components
// and just provides you the one from React Native core.
MainComponentsRegistry.register(componentFactory);
final ReactInstanceManager reactInstanceManager = getReactInstanceManager();
ViewManagerRegistry viewManagerRegistry =
new ViewManagerRegistry(
reactInstanceManager.getOrCreateViewManagers(reactApplicationContext));
return new FabricJSIModuleProvider(
reactApplicationContext,
componentFactory,
ReactNativeConfig.DEFAULT_CONFIG,
viewManagerRegistry);
}
});
return specs;
}
};
}
}

View File

@@ -1,36 +0,0 @@
package com.mattermost.newarchitecture.components;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.fabric.ComponentFactory;
import com.facebook.soloader.SoLoader;
/**
* Class responsible to load the custom Fabric Components. This class has native methods and needs a
* corresponding C++ implementation/header file to work correctly (already placed inside the jni/
* folder for you).
*
* <p>Please note that this class is used ONLY if you opt-in for the New Architecture (see the
* `newArchEnabled` property). Is ignored otherwise.
*/
@DoNotStrip
public class MainComponentsRegistry {
static {
SoLoader.loadLibrary("fabricjni");
}
@DoNotStrip private final HybridData mHybridData;
@DoNotStrip
private native HybridData initHybrid(ComponentFactory componentFactory);
@DoNotStrip
private MainComponentsRegistry(ComponentFactory componentFactory) {
mHybridData = initHybrid(componentFactory);
}
@DoNotStrip
public static MainComponentsRegistry register(ComponentFactory componentFactory) {
return new MainComponentsRegistry(componentFactory);
}
}

View File

@@ -1,48 +0,0 @@
package com.mattermost.newarchitecture.modules;
import com.facebook.jni.HybridData;
import com.facebook.react.ReactPackage;
import com.facebook.react.ReactPackageTurboModuleManagerDelegate;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.soloader.SoLoader;
import java.util.List;
/**
* Class responsible to load the TurboModules. This class has native methods and needs a
* corresponding C++ implementation/header file to work correctly (already placed inside the jni/
* folder for you).
*
* <p>Please note that this class is used ONLY if you opt-in for the New Architecture (see the
* `newArchEnabled` property). Is ignored otherwise.
*/
public class MainApplicationTurboModuleManagerDelegate
extends ReactPackageTurboModuleManagerDelegate {
private static volatile boolean sIsSoLibraryLoaded;
protected MainApplicationTurboModuleManagerDelegate(
ReactApplicationContext reactApplicationContext, List<ReactPackage> packages) {
super(reactApplicationContext, packages);
}
protected native HybridData initHybrid();
native boolean canCreateTurboModule(String moduleName);
public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder {
protected MainApplicationTurboModuleManagerDelegate build(
ReactApplicationContext context, List<ReactPackage> packages) {
return new MainApplicationTurboModuleManagerDelegate(context, packages);
}
}
@Override
protected synchronized void maybeLoadOtherSoLibraries() {
if (!sIsSoLibraryLoaded) {
// If you change the name of your application .so file in the Android.mk file,
// make sure you update the name here as well.
SoLoader.loadLibrary("rndiffapp_appmodules");
sIsSoLibraryLoaded = true;
}
}
}

View File

@@ -7,7 +7,6 @@ import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import java.util.Objects;
@@ -17,7 +16,6 @@ import com.mattermost.helpers.DatabaseHelper;
import com.mattermost.helpers.Network;
import com.mattermost.helpers.NotificationHelper;
import com.mattermost.helpers.PushNotificationDataHelper;
import com.mattermost.helpers.ResolvePromise;
import com.mattermost.share.ShareModule;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.notification.PushNotification;
@@ -31,7 +29,6 @@ public class CustomPushNotification extends PushNotification {
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
CustomPushNotificationHelper.createNotificationChannels(context);
dataHelper = new PushNotificationDataHelper(context);
try {
@@ -57,27 +54,31 @@ public class CustomPushNotification extends PushNotification {
boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
if (ackId != null && serverUrl != null) {
notificationReceiptDelivery(ackId, serverUrl, postId, type, isIdLoaded, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (isIdLoaded) {
Bundle response = (Bundle) value;
if (value != null) {
response.putString("server_url", serverUrl);
Bundle current = mNotificationProps.asBundle();
current.putAll(response);
mNotificationProps = createProps(current);
}
}
Bundle response = ReceiptDelivery.send(ackId, serverUrl, postId, type, isIdLoaded);
if (isIdLoaded && response != null) {
Bundle current = mNotificationProps.asBundle();
if (!current.containsKey("server_url")) {
response.putString("server_url", serverUrl);
}
@Override
public void reject(String code, String message) {
Log.e("ReactNative", code + ": " + message);
}
});
current.putAll(response);
mNotificationProps = createProps(current);
}
}
finishProcessingNotification(serverUrl, type, channelId, notificationId, isReactInit);
}
@Override
public void onOpened() {
if (mNotificationProps != null) {
digestNotification();
Bundle data = mNotificationProps.asBundle();
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
}
}
private void finishProcessingNotification(String serverUrl, String type, String channelId, int notificationId, Boolean isReactInit) {
switch (type) {
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
case CustomPushNotificationHelper.PUSH_TYPE_SESSION:
@@ -118,16 +119,6 @@ public class CustomPushNotification extends PushNotification {
}
}
@Override
public void onOpened() {
if (mNotificationProps != null) {
digestNotification();
Bundle data = mNotificationProps.asBundle();
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
}
}
private void buildNotification(Integer notificationId, boolean createSummary) {
final PendingIntent pendingIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, mNotificationProps);
final Notification notification = buildNotification(pendingIntent);
@@ -149,10 +140,6 @@ public class CustomPushNotification extends PushNotification {
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, true);
}
private void notificationReceiptDelivery(String ackId, String serverUrl, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
ReceiptDelivery.send(mContext, ackId, serverUrl, postId, type, isIdLoaded, promise);
}
private void notifyReceivedToJS() {
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
}

View File

@@ -0,0 +1,58 @@
package com.mattermost.rnbeta
import android.app.Activity
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import androidx.window.rxjava3.layout.windowLayoutInfoObservable
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
class FoldableObserver(private val activity: Activity) {
private var disposable: Disposable? = null
private lateinit var observable: Observable<WindowLayoutInfo>
public fun onCreate() {
observable = WindowInfoTracker.getOrCreate(activity)
.windowLayoutInfoObservable(activity)
}
public fun onStart() {
if (disposable?.isDisposed == true) {
onCreate()
}
disposable = observable.observeOn(AndroidSchedulers.mainThread())
.subscribe { layoutInfo ->
val splitViewModule = SplitViewModule.getInstance()
val foldingFeature = layoutInfo.displayFeatures
.filterIsInstance<FoldingFeature>()
.firstOrNull()
when {
foldingFeature?.state === FoldingFeature.State.FLAT ->
splitViewModule?.setDeviceFolded(false)
isTableTopPosture(foldingFeature) ->
splitViewModule?.setDeviceFolded(false)
isBookPosture(foldingFeature) ->
splitViewModule?.setDeviceFolded(false)
else -> {
splitViewModule?.setDeviceFolded(true)
}
}
}
}
public fun onStop() {
disposable?.dispose()
}
private fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}
private fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}
}

View File

@@ -6,37 +6,36 @@ import androidx.annotation.Nullable;
import android.view.KeyEvent;
import android.content.res.Configuration;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import com.reactnativenavigation.NavigationActivity;
import com.github.emilioicai.hwkeyboardevent.HWKeyboardEventModule;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactActivityDelegate;
public class MainActivity extends NavigationActivity {
private boolean HWKeyboardConnected = false;
private FoldableObserver foldableObserver = new FoldableObserver(this);
public static class MainActivityDelegate extends ReactActivityDelegate {
public MainActivityDelegate(NavigationActivity activity, String mainComponentName) {
super(activity, mainComponentName);
}
@Override
protected String getMainComponentName() {
return "Mattermost";
}
@Override
protected ReactRootView createRootView() {
ReactRootView reactRootView = new ReactRootView(getContext());
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED);
return reactRootView;
}
@Override
protected boolean isConcurrentRootEnabled() {
// If you opted-in for the New Architecture, we enable Concurrent Root (i.e. React 18).
// More on this on https://reactjs.org/blog/2022/03/29/react-v18.html
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
/**
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
* (aka React 18) with two boolean flags.
*/
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new DefaultReactActivityDelegate(
this,
getMainComponentName(),
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
// If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
);
}
@Override
@@ -44,6 +43,19 @@ public class MainActivity extends NavigationActivity {
super.onCreate(null);
setContentView(R.layout.launch_screen);
setHWKeyboardConnected();
foldableObserver.onCreate();
}
@Override
protected void onStart() {
super.onStart();
foldableObserver.onStart();
}
@Override
protected void onStop() {
super.onStop();
foldableObserver.onStop();
}
@Override

View File

@@ -1,10 +1,9 @@
package com.mattermost.rnbeta;
import com.facebook.react.bridge.JSIModuleSpec;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
@@ -23,11 +22,12 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import com.facebook.react.PackageList;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactNativeHost;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.TurboReactPackage;
import com.facebook.react.bridge.JSIModuleSpec;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.JSIModulePackage;
@@ -36,17 +36,16 @@ import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.modules.network.OkHttpClientProvider;
import com.facebook.soloader.SoLoader;
import com.mattermost.flipper.ReactNativeFlipper;
import com.mattermost.networkclient.RCTOkHttpClientFactory;
import com.mattermost.newarchitecture.MainApplicationReactNativeHost;
import com.nozbe.watermelondb.jsi.WatermelonDBJSIPackage;
public class MainApplication extends NavigationApplication implements INotificationsApplication {
public static MainApplication instance;
public Boolean sharedExtensionIsOpened = false;
private final ReactNativeHost mReactNativeHost =
new ReactNativeHost(this) {
new DefaultReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
@@ -71,6 +70,8 @@ public class MainApplication extends NavigationApplication implements INotificat
return ShareModule.getInstance(reactContext);
case "Notifications":
return NotificationsModule.getInstance(instance, reactContext);
case "SplitView":
return SplitViewModule.Companion.getInstance(reactContext);
default:
throw new IllegalArgumentException("Could not find module " + name);
}
@@ -83,6 +84,7 @@ public class MainApplication extends NavigationApplication implements INotificat
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
map.put("MattermostShare", new ReactModuleInfo("MattermostShare", "com.mattermost.share.ShareModule", false, false, true, false, false));
map.put("Notifications", new ReactModuleInfo("Notifications", "com.mattermost.rnbeta.NotificationsModule", false, false, false, false, false));
map.put("SplitView", new ReactModuleInfo("SplitView", "com.mattermost.rnbeta.SplitViewModule", false, false, false, false, false));
return map;
};
}
@@ -106,30 +108,26 @@ public class MainApplication extends NavigationApplication implements INotificat
protected String getJSMainModuleName() {
return "index";
}
@Override
protected boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
protected Boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
};
private final ReactNativeHost mNewArchitectureNativeHost =
new MainApplicationReactNativeHost(this);
@Override
public ReactNativeHost getReactNativeHost() {
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
return mNewArchitectureNativeHost;
} else {
return mReactNativeHost;
}
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
instance = this;
// If you opted-in for the New Architecture, we enable the TurboModule system
ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
Context context = getApplicationContext();
// Delete any previous temp files created by the app
@@ -141,6 +139,13 @@ public class MainApplication extends NavigationApplication implements INotificat
// with a cookie jar defined in APIClientModule and an interceptor to intercept all
// requests that originate from React Native's OKHttpClient
OkHttpClientProvider.setOkHttpClientFactory(new RCTOkHttpClientFactory());
SoLoader.init(this, /* native exopackage */ false);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load();
}
ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
}
@Override
@@ -153,26 +158,4 @@ public class MainApplication extends NavigationApplication implements INotificat
new JsIOHelper()
);
}
/**
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
*/
private static void initializeFlipper(
Context context, ReactInstanceManager reactInstanceManager) {
if (BuildConfig.DEBUG) {
try {
/*
We use reflection here to pick up the class that initializes Flipper,
since Flipper library is not available in release mode
*/
Class<?> aClass = Class.forName("com.rn.ReactNativeFlipper");
aClass
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
.invoke(null, context, reactInstanceManager);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

View File

@@ -6,6 +6,7 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
@@ -20,8 +21,12 @@ import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.mattermost.helpers.Credentials;
import com.reactlibrary.createthumbnail.CreateThumbnailModule;
import com.mattermost.helpers.RealPathUtil;
import java.io.File;
@@ -29,6 +34,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.nio.channels.FileChannel;
public class MattermostManagedModule extends ReactContextBaseJavaModule {
@@ -127,19 +133,6 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
promise.resolve(map);
}
@ReactMethod
public void isRunningInSplitView(final Promise promise) {
WritableMap result = Arguments.createMap();
Activity current = getCurrentActivity();
if (current != null) {
result.putBoolean("isSplitView", current.isInMultiWindowMode());
} else {
result.putBoolean("isSplitView", false);
}
promise.resolve(result);
}
@ReactMethod
public void saveFile(String path, final Promise promise) {
Uri contentUri;
@@ -206,6 +199,30 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
}
}
@ReactMethod
public void createThumbnail(ReadableMap options, Promise promise) {
try {
WritableMap optionsMap = Arguments.createMap();
optionsMap.merge(options);
String url = options.hasKey("url") ? options.getString("url") : "";
URL videoUrl = new URL(url);
String serverUrl = videoUrl.getProtocol() + "://" + videoUrl.getHost() + ":" + videoUrl.getPort();
String token = Credentials.getCredentialsForServerSync(this.reactContext, serverUrl);
if (!TextUtils.isEmpty(token)) {
WritableMap headers = Arguments.createMap();
if (optionsMap.hasKey("headers")) {
headers.merge(optionsMap.getMap("headers"));
}
headers.putString("Authorization", "Bearer " + token);
optionsMap.putMap("headers", headers);
}
CreateThumbnailModule thumb = new CreateThumbnailModule(this.reactContext);
thumb.create(optionsMap.copy(), promise);
} catch (Exception e) {
promise.reject("CreateThumbnail_ERROR", e);
}
}
private static class SaveDataTask extends GuardedResultAsyncTask<Object> {
private final WeakReference<Context> weakContext;
private final String fromFile;

View File

@@ -8,28 +8,16 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.Person;
import android.util.Log;
import java.io.IOException;
import java.util.Objects;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONObject;
import org.json.JSONException;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.mattermost.helpers.*;
import com.facebook.react.bridge.ReactApplicationContext;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
@@ -53,12 +41,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
final int notificationId = intent.getIntExtra(CustomPushNotificationHelper.NOTIFICATION_ID, -1);
final String serverUrl = bundle.getString("server_url");
if (serverUrl != null) {
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
if (token != null) {
replyToMessage(serverUrl, token, notificationId, message);
}
replyToMessage(serverUrl, notificationId, message);
} else {
onReplyFailed(notificationId);
}
@@ -67,7 +50,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
}
}
protected void replyToMessage(final String serverUrl, final String token, final int notificationId, final CharSequence message) {
protected void replyToMessage(final String serverUrl, final int notificationId, final CharSequence message) {
final String channelId = bundle.getString("channel_id");
final String postId = bundle.getString("post_id");
String rootId = bundle.getString("root_id");
@@ -75,63 +58,53 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
rootId = postId;
}
if (token == null || serverUrl == null) {
if (serverUrl == null) {
onReplyFailed(notificationId);
return;
}
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
String json = buildReplyPost(channelId, rootId, message.toString());
RequestBody body = RequestBody.create(json, JSON);
WritableMap headers = Arguments.createMap();
headers.putString("Content-Type", "application/json");
WritableMap body = Arguments.createMap();
body.putString("channel_id", channelId);
body.putString("message", message.toString());
body.putString("root_id", rootId);
WritableMap options = Arguments.createMap();
options.putMap("headers", headers);
options.putMap("body", body);
String postsEndpoint = "/api/v4/posts?set_online=false";
String url = String.format("%s%s", serverUrl.replaceAll("/$", ""), postsEndpoint);
Log.i("ReactNative", String.format("Reply URL=%s", url));
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.url(url)
.post(body)
.build();
client.newCall(request).enqueue(new okhttp3.Callback() {
Network.post(serverUrl, postsEndpoint, options, new ResolvePromise() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.i("ReactNative", String.format("Reply FAILED exception %s", e.getMessage()));
public void resolve(@Nullable Object value) {
if (value != null) {
onReplySuccess(notificationId, message);
Log.i("ReactNative", "Reply SUCCESS");
} else {
Log.i("ReactNative", "Reply FAILED resolved without value");
onReplyFailed(notificationId);
}
}
@Override
public void reject(Throwable reason) {
Log.i("ReactNative", String.format("Reply FAILED exception %s", reason.getMessage()));
onReplyFailed(notificationId);
}
@Override
public void onResponse(@NonNull Call call, @NonNull final Response response) throws IOException {
if (response.isSuccessful()) {
onReplySuccess(notificationId, message);
Log.i("ReactNative", "Reply SUCCESS");
} else {
Log.i("ReactNative",
String.format("Reply FAILED status %s BODY %s",
response.code(),
Objects.requireNonNull(response.body()).string()
)
);
onReplyFailed(notificationId);
}
public void reject(String code, String message) {
Log.i("ReactNative",
String.format("Reply FAILED status %s BODY %s", code, message)
);
onReplyFailed(notificationId);
}
});
}
protected String buildReplyPost(String channelId, String rootId, String message) {
try {
JSONObject json = new JSONObject();
json.put("channel_id", channelId);
json.put("message", message);
json.put("root_id", rootId);
return json.toString();
} catch(JSONException e) {
return "{}";
}
}
protected void onReplyFailed(int notificationId) {
recreateNotification(notificationId, "Message failed to send.");
}

View File

@@ -1,135 +1,60 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import java.lang.System;
import java.util.Objects;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.HttpUrl;
import org.json.JSONObject;
import org.json.JSONException;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.mattermost.helpers.*;
import okhttp3.Response;
public class ReceiptDelivery {
private static final int[] FIBONACCI_BACKOFF = new int[] { 0, 1, 2, 3, 5, 8 };
private static final String[] ackKeys = new String[]{"post_id", "root_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"};
public static void send(Context context, final String ackId, final String serverUrl, final String postId, final String type, final boolean isIdLoaded, ResolvePromise promise) {
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
public static Bundle send(final String ackId, final String serverUrl, final String postId, final String type, final boolean isIdLoaded) {
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with ID-LOADED=%s", ackId, type, serverUrl, isIdLoaded));
execute(serverUrl, postId, token, ackId, type, isIdLoaded, promise);
}
WritableMap options = Arguments.createMap();
WritableMap headers = Arguments.createMap();
WritableMap body = Arguments.createMap();
headers.putString("Content-Type", "application/json");
options.putMap("headers", headers);
body.putString("id", ackId);
body.putDouble("received_at", System.currentTimeMillis());
body.putString("platform", "android");
body.putString("type", type);
body.putString("post_id", postId);
body.putBoolean("is_id_loaded", isIdLoaded);
options.putMap("body", body);
protected static void execute(String serverUrl, String postId, String token, String ackId, String type, boolean isIdLoaded, ResolvePromise promise) {
if (token == null) {
promise.reject("Receipt delivery failure", "Invalid token");
return;
}
if (serverUrl == null) {
promise.reject("Receipt delivery failure", "Invalid server URL");
}
JSONObject json;
long receivedAt = System.currentTimeMillis();
try {
json = new JSONObject();
json.put("id", ackId);
json.put("received_at", receivedAt);
json.put("platform", "android");
json.put("type", type);
json.put("post_id", postId);
json.put("is_id_loaded", isIdLoaded);
} catch (JSONException e) {
Log.e("ReactNative", "Receipt delivery failed to build json payload");
promise.reject("Receipt delivery failure", e.toString());
return;
}
final HttpUrl url;
if (serverUrl != null) {
url = HttpUrl.parse(
String.format("%s/api/v4/notifications/ack", serverUrl.replaceAll("/$", "")));
if (url != null) {
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(json.toString(), JSON);
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.url(url)
.post(body)
.build();
makeServerRequest(client, request, isIdLoaded, 0, promise);
}
}
}
private static void makeServerRequest(OkHttpClient client, Request request, Boolean isIdLoaded, int reRequestCount, ResolvePromise promise) {
try {
Response response = client.newCall(request).execute();
try (Response response = Network.postSync(serverUrl, "api/v4/notifications/ack", options)) {
String responseBody = Objects.requireNonNull(response.body()).string();
if (response.code() != 200) {
switch (response.code()) {
case 302:
promise.reject("Receipt delivery failure", "StatusFound");
return;
case 400:
promise.reject("Receipt delivery failure", "StatusBadRequest");
return;
case 401:
promise.reject("Receipt delivery failure", "Unauthorized");
case 403:
promise.reject("Receipt delivery failure", "Forbidden");
return;
case 500:
promise.reject("Receipt delivery failure", "StatusInternalServerError");
return;
case 501:
promise.reject("Receipt delivery failure", "StatusNotImplemented");
return;
}
throw new Exception(responseBody);
}
JSONObject jsonResponse = new JSONObject(responseBody);
return parseAckResponse(jsonResponse);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static Bundle parseAckResponse(JSONObject jsonResponse) {
try {
Bundle bundle = new Bundle();
String[] keys = new String[]{"post_id", "root_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"};
for (String key : keys) {
for (String key : ackKeys) {
if (jsonResponse.has(key)) {
bundle.putString(key, jsonResponse.getString(key));
}
}
promise.resolve(bundle);
return bundle;
} catch (Exception e) {
Log.e("ReactNative", "Receipt delivery failed to send");
if (isIdLoaded) {
try {
reRequestCount++;
if (reRequestCount < FIBONACCI_BACKOFF.length) {
Log.i("ReactNative", "Retry attempt " + reRequestCount + " with backoff delay: " + FIBONACCI_BACKOFF[reRequestCount] + " seconds");
Thread.sleep(FIBONACCI_BACKOFF[reRequestCount] * 1000);
makeServerRequest(client, request, true, reRequestCount, promise);
}
} catch(InterruptedException ie) {
ie.printStackTrace();
}
}
promise.reject("Receipt delivery failure", e.toString());
e.printStackTrace();
return null;
}
}
}

View File

@@ -0,0 +1,73 @@
package com.mattermost.rnbeta
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
import com.learnium.RNDeviceInfo.resolver.DeviceTypeResolver
class SplitViewModule(private var reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private var isDeviceFolded: Boolean = false
private var listenerCount = 0
companion object {
private var instance: SplitViewModule? = null
fun getInstance(reactContext: ReactApplicationContext): SplitViewModule {
if (instance == null) {
instance = SplitViewModule(reactContext)
} else {
instance!!.reactContext = reactContext
}
return instance!!
}
fun getInstance(): SplitViewModule? {
return instance
}
}
override fun getName() = "SplitView"
fun sendEvent(eventName: String,
params: WritableMap?) {
reactContext
.getJSModule(RCTDeviceEventEmitter::class.java)
.emit(eventName, params)
}
private fun getSplitViewResults(folded: Boolean) : WritableMap? {
if (currentActivity != null) {
val deviceResolver = DeviceTypeResolver(this.reactContext)
val map = Arguments.createMap()
map.putBoolean("isSplitView", currentActivity!!.isInMultiWindowMode || folded)
map.putBoolean("isTablet", deviceResolver.isTablet)
return map
}
return null
}
fun setDeviceFolded(folded: Boolean) {
val map = getSplitViewResults(folded)
if (listenerCount > 0 && isDeviceFolded != folded) {
sendEvent("SplitViewChanged", map)
}
isDeviceFolded = folded
}
@ReactMethod
fun isRunningInSplitView(promise: Promise) {
promise.resolve(getSplitViewResults(isDeviceFolded))
}
@ReactMethod
fun addListener(eventName: String) {
listenerCount += 1
}
@ReactMethod
fun removeListeners(count: Int) {
listenerCount -= count
}
}

View File

@@ -90,22 +90,26 @@ public class ShareUtils {
}
private static Bitmap getBitmapAtTime(Context context, String filePath, int time) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
if (URLUtil.isFileUrl(filePath)) {
String decodedPath;
try {
decodedPath = URLDecoder.decode(filePath, "UTF-8");
} catch (UnsupportedEncodingException e) {
decodedPath = filePath;
try {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
if (URLUtil.isFileUrl(filePath)) {
String decodedPath;
try {
decodedPath = URLDecoder.decode(filePath, "UTF-8");
} catch (UnsupportedEncodingException e) {
decodedPath = filePath;
}
retriever.setDataSource(decodedPath.replace("file://", ""));
} else if (filePath.contains("content://")) {
retriever.setDataSource(context, Uri.parse(filePath));
}
retriever.setDataSource(decodedPath.replace("file://", ""));
} else if (filePath.contains("content://")) {
retriever.setDataSource(context, Uri.parse(filePath));
Bitmap image = retriever.getFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
retriever.release();
return image;
} catch (Exception e) {
throw new IllegalStateException("File doesn't exist or not supported");
}
Bitmap image = retriever.getFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
retriever.release();
return image;
}
}

View File

@@ -1,7 +0,0 @@
cmake_minimum_required(VERSION 3.13)
# Define the library name here.
project(rndiffapp_appmodules)
# This file includes all the necessary to let you build your application with the New Architecture.
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)

View File

@@ -1,32 +0,0 @@
#include "MainApplicationModuleProvider.h"
#include <rncli.h>
#include <rncore.h>
namespace facebook {
namespace react {
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
const std::string &moduleName,
const JavaTurboModule::InitParams &params) {
// Here you can provide your own module provider for TurboModules coming from
// either your application or from external libraries. The approach to follow
// is similar to the following (for a library called `samplelibrary`:
//
// auto module = samplelibrary_ModuleProvider(moduleName, params);
// if (module != nullptr) {
// return module;
// }
// return rncore_ModuleProvider(moduleName, params);
// Module providers autolinked by RN CLI
auto rncli_module = rncli_ModuleProvider(moduleName, params);
if (rncli_module != nullptr) {
return rncli_module;
}
return rncore_ModuleProvider(moduleName, params);
}
} // namespace react
} // namespace facebook

View File

@@ -1,16 +0,0 @@
#pragma once
#include <memory>
#include <string>
#include <ReactCommon/JavaTurboModule.h>
namespace facebook {
namespace react {
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
const std::string &moduleName,
const JavaTurboModule::InitParams &params);
} // namespace react
} // namespace facebook

View File

@@ -1,45 +0,0 @@
#include "MainApplicationTurboModuleManagerDelegate.h"
#include "MainApplicationModuleProvider.h"
namespace facebook {
namespace react {
jni::local_ref<MainApplicationTurboModuleManagerDelegate::jhybriddata>
MainApplicationTurboModuleManagerDelegate::initHybrid(
jni::alias_ref<jhybridobject>) {
return makeCxxInstance();
}
void MainApplicationTurboModuleManagerDelegate::registerNatives() {
registerHybrid({
makeNativeMethod(
"initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid),
makeNativeMethod(
"canCreateTurboModule",
MainApplicationTurboModuleManagerDelegate::canCreateTurboModule),
});
}
std::shared_ptr<TurboModule>
MainApplicationTurboModuleManagerDelegate::getTurboModule(
const std::string &name,
const std::shared_ptr<CallInvoker> &jsInvoker) {
// Not implemented yet: provide pure-C++ NativeModules here.
return nullptr;
}
std::shared_ptr<TurboModule>
MainApplicationTurboModuleManagerDelegate::getTurboModule(
const std::string &name,
const JavaTurboModule::InitParams &params) {
return MainApplicationModuleProvider(name, params);
}
bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule(
const std::string &name) {
return getTurboModule(name, nullptr) != nullptr ||
getTurboModule(name, {.moduleName = name}) != nullptr;
}
} // namespace react
} // namespace facebook

View File

@@ -1,38 +0,0 @@
#include <memory>
#include <string>
#include <ReactCommon/TurboModuleManagerDelegate.h>
#include <fbjni/fbjni.h>
namespace facebook {
namespace react {
class MainApplicationTurboModuleManagerDelegate
: public jni::HybridClass<
MainApplicationTurboModuleManagerDelegate,
TurboModuleManagerDelegate> {
public:
// Adapt it to the package you used for your Java class.
static constexpr auto kJavaDescriptor =
"Lcom/rndiffapp/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;";
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject>);
static void registerNatives();
std::shared_ptr<TurboModule> getTurboModule(
const std::string &name,
const std::shared_ptr<CallInvoker> &jsInvoker) override;
std::shared_ptr<TurboModule> getTurboModule(
const std::string &name,
const JavaTurboModule::InitParams &params) override;
/**
* Test-only method. Allows user to verify whether a TurboModule can be
* created by instances of this class.
*/
bool canCreateTurboModule(const std::string &name);
};
} // namespace react
} // namespace facebook

View File

@@ -1,65 +0,0 @@
#include "MainComponentsRegistry.h"
#include <CoreComponentsRegistry.h>
#include <fbjni/fbjni.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/components/rncore/ComponentDescriptors.h>
#include <rncli.h>
namespace facebook {
namespace react {
MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {}
std::shared_ptr<ComponentDescriptorProviderRegistry const>
MainComponentsRegistry::sharedProviderRegistry() {
auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry();
// Autolinked providers registered by RN CLI
rncli_registerProviders(providerRegistry);
// Custom Fabric Components go here. You can register custom
// components coming from your App or from 3rd party libraries here.
//
// providerRegistry->add(concreteComponentDescriptorProvider<
// AocViewerComponentDescriptor>());
return providerRegistry;
}
jni::local_ref<MainComponentsRegistry::jhybriddata>
MainComponentsRegistry::initHybrid(
jni::alias_ref<jclass>,
ComponentFactory *delegate) {
auto instance = makeCxxInstance(delegate);
auto buildRegistryFunction =
[](EventDispatcher::Weak const &eventDispatcher,
ContextContainer::Shared const &contextContainer)
-> ComponentDescriptorRegistry::Shared {
auto registry = MainComponentsRegistry::sharedProviderRegistry()
->createComponentDescriptorRegistry(
{eventDispatcher, contextContainer});
auto mutableRegistry =
std::const_pointer_cast<ComponentDescriptorRegistry>(registry);
mutableRegistry->setFallbackComponentDescriptor(
std::make_shared<UnimplementedNativeViewComponentDescriptor>(
ComponentDescriptorParameters{
eventDispatcher, contextContainer, nullptr}));
return registry;
};
delegate->buildRegistryFunction = buildRegistryFunction;
return instance;
}
void MainComponentsRegistry::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid),
});
}
} // namespace react
} // namespace facebook

View File

@@ -1,32 +0,0 @@
#pragma once
#include <ComponentFactory.h>
#include <fbjni/fbjni.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/componentregistry/ComponentDescriptorRegistry.h>
namespace facebook {
namespace react {
class MainComponentsRegistry
: public facebook::jni::HybridClass<MainComponentsRegistry> {
public:
// Adapt it to the package you used for your Java class.
constexpr static auto kJavaDescriptor =
"Lcom/mattermost/newarchitecture/components/MainComponentsRegistry;";
static void registerNatives();
MainComponentsRegistry(ComponentFactory *delegate);
private:
static std::shared_ptr<ComponentDescriptorProviderRegistry const>
sharedProviderRegistry();
static jni::local_ref<jhybriddata> initHybrid(
jni::alias_ref<jclass>,
ComponentFactory *delegate);
};
} // namespace react
} // namespace facebook

View File

@@ -1,11 +0,0 @@
#include <fbjni/fbjni.h>
#include "MainApplicationTurboModuleManagerDelegate.h"
#include "MainComponentsRegistry.h"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
return facebook::jni::initialize(vm, [] {
facebook::react::MainApplicationTurboModuleManagerDelegate::
registerNatives();
facebook::react::MainComponentsRegistry::registerNatives();
});
}

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.mattermost.flipper;
import android.content.Context;
import com.facebook.react.ReactInstanceManager;
/**
* Class responsible of loading Flipper inside your React Native application. This is the release
* flavor of it so it's empty as we don't want to load Flipper.
*/
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
// Do nothing as we don't want to initialize Flipper on Release.
}
}

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.mattermost.flipper;
import android.content.Context;
import com.facebook.react.ReactInstanceManager;
/**
* Class responsible of loading Flipper inside your React Native application. This is the release
* flavor of it so it's empty as we don't want to load Flipper.
*/
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
// Do nothing as we don't want to initialize Flipper on Release.
}
}

View File

@@ -2,22 +2,18 @@
buildscript {
ext {
buildToolsVersion = "31.0.0"
buildToolsVersion = "33.0.0"
minSdkVersion = 24
compileSdkVersion = 31
targetSdkVersion = 31
supportLibVersion = "31.0.0"
compileSdkVersion = 33
targetSdkVersion = 33
supportLibVersion = "33.0.0"
kotlinVersion = "1.5.30"
kotlin_version = "1.5.30"
firebaseVersion = "21.0.0"
RNNKotlinVersion = kotlinVersion
if (System.properties['os.arch'] == "aarch64") {
// For M1 Users we need to use the NDK 24 which added support for aarch64
ndkVersion = "24.0.8215888"
} else {
// Otherwise we default to the side-by-side NDK version from AGP.
ndkVersion = "21.4.7075529"
}
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
}
repositories {
mavenCentral()
@@ -25,9 +21,8 @@ buildscript {
google()
}
dependencies {
classpath("com.android.tools.build:gradle:7.2.1")
classpath("com.android.tools.build:gradle:7.3.1")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("de.undercouch:gradle-download-task:5.0.1")
classpath('com.google.gms:google-services:4.3.14')
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
@@ -38,50 +33,9 @@ buildscript {
allprojects {
repositories {
exclusiveContent {
// We get React Native's Android binaries exclusively through npm,
// from a local Maven repo inside node_modules/react-native/.
// (The use of exclusiveContent prevents looking elsewhere like Maven Central
// and potentially getting a wrong version.)
filter {
includeGroup "com.facebook.react"
}
forRepository {
maven {
url "$rootDir/../node_modules/react-native/android"
}
}
}
google()
mavenCentral()
mavenLocal()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url("$rootDir/../node_modules/react-native/android")
// Replace AAR from original RN with AAR from react-native-v8
// url("$rootDir/../node_modules/react-native-v8/dist")
}
maven {
// Local Maven repo containing AARs with JSC library built for Android
url("$rootDir/../node_modules/jsc-android/dist")
// prebuilt libv8android.so
// url("$rootDir/../node_modules/v8-android/dist")
}
maven {
url "https://www.jitpack.io"
}
maven {
url "$rootDir/../node_modules/detox/Detox-android"
}
mavenCentral {
// We don't want to fetch react-native from Maven Central as there are
// older versions over there.
content {
excludeGroup "com.facebook.react"
}
}
}
}

View File

@@ -40,4 +40,8 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
newArchEnabled=false
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true

View File

@@ -1,13 +1,5 @@
rootProject.name = 'Mattermost'
include ':app'
includeBuild('../node_modules/react-native-gradle-plugin')
if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") {
include(":ReactAndroid")
project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid')
include(":ReactAndroid:hermes-engine")
project(":ReactAndroid:hermes-engine").projectDir = file('../node_modules/react-native/ReactAndroid/hermes-engine')
}
include ':reactnativenotifications'
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/lib/android/app')
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
@@ -15,3 +7,4 @@ include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
include ':watermelondb-jsi'
project(':watermelondb-jsi').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android-jsi')
includeBuild('../node_modules/react-native-gradle-plugin')

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Tutorial} from '@constants';
import {GLOBAL_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {logError} from '@utils/log';
@@ -22,16 +23,20 @@ export const storeDeviceToken = async (token: string, prepareRecordsOnly = false
return storeGlobal(GLOBAL_IDENTIFIERS.DEVICE_TOKEN, token, prepareRecordsOnly);
};
export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => {
return storeGlobal(GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, 'true', prepareRecordsOnly);
};
export const storeOnboardingViewedValue = async (value = true) => {
return storeGlobal(GLOBAL_IDENTIFIERS.ONBOARDING, value, false);
};
export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => {
return storeGlobal(Tutorial.MULTI_SERVER, 'true', prepareRecordsOnly);
};
export const storeProfileLongPressTutorial = async (prepareRecordsOnly = false) => {
return storeGlobal(GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, 'true', prepareRecordsOnly);
return storeGlobal(Tutorial.PROFILE_LONG_PRESS, 'true', prepareRecordsOnly);
};
export const storeSkinEmojiSelectorTutorial = async (prepareRecordsOnly = false) => {
return storeGlobal(Tutorial.EMOJI_SKIN_SELECTOR, 'true', prepareRecordsOnly);
};
export const storeDontAskForReview = async (prepareRecordsOnly = false) => {

View File

@@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Model} from '@nozbe/watermelondb';
import {CHANNELS_CATEGORY, DMS_CATEGORY} from '@constants/categories';
import DatabaseManager from '@database/manager';
import {prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById, prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
@@ -81,7 +79,6 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
return {error: 'no current user id'};
}
const models: Model[] = [];
const categoriesWithChannels: CategoryWithChannels[] = [];
if (isDMorGM(channel)) {
@@ -101,11 +98,10 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
cwc.channel_ids.unshift(channel.id);
categoriesWithChannels.push(cwc);
}
const ccModels = await prepareCategoryChannels(operator, categoriesWithChannels);
models.push(...ccModels);
}
const models = await prepareCategoryChannels(operator, categoriesWithChannels);
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models);
}

View File

@@ -1,13 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database} from '@nozbe/watermelondb';
import {DeviceEventEmitter} from 'react-native';
import {Navigation} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import ServerDataOperator from '@database/operator/server_data_operator';
import {getMyChannel} from '@queries/servers/channel';
import {getCommonSystemValues, getTeamHistory} from '@queries/servers/system';
import {getTeamChannelHistory} from '@queries/servers/team';
@@ -15,6 +13,9 @@ import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@scr
import {switchToChannel} from './channel';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Database} from '@nozbe/watermelondb';
let mockIsTablet: jest.Mock;
const now = new Date('2020-01-01').getTime();

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Model} from '@nozbe/watermelondb';
import {DeviceEventEmitter} from 'react-native';
import {General, Navigation as NavigationConstants, Preferences, Screens} from '@constants';
@@ -24,6 +23,7 @@ import {isTablet} from '@utils/helpers';
import {logError, logInfo} from '@utils/log';
import {displayGroupMessageName, displayUsername, getUserIdFromChannelName} from '@utils/user';
import type {Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
import type UserModel from '@typings/database/models/servers/user';
@@ -76,7 +76,7 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
}
models = (await Promise.all(modelPromises)).flat();
const {member: viewedAt} = await markChannelAsViewed(serverUrl, channelId, true);
const {member: viewedAt} = await markChannelAsViewed(serverUrl, channelId, false, true);
if (viewedAt) {
models.push(viewedAt);
}
@@ -160,7 +160,7 @@ export async function selectAllMyChannelIds(serverUrl: string) {
}
}
export async function markChannelAsViewed(serverUrl: string, channelId: string, prepareRecordsOnly = false) {
export async function markChannelAsViewed(serverUrl: string, channelId: string, onlyCounts = false, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const member = await getMyChannel(database, channelId);
@@ -172,8 +172,10 @@ export async function markChannelAsViewed(serverUrl: string, channelId: string,
m.isUnread = false;
m.mentionsCount = 0;
m.manuallyUnread = false;
m.viewedAt = member.lastViewedAt;
m.lastViewedAt = Date.now();
if (!onlyCounts) {
m.viewedAt = member.lastViewedAt;
m.lastViewedAt = Date.now();
}
});
PushNotifications.removeChannelNotifications(serverUrl, channelId);
if (!prepareRecordsOnly) {

View File

@@ -1,16 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IntlShape} from 'react-intl';
import {sendEphemeralPost} from '@actions/local/post';
import ClientError from '@client/rest/error';
import {AppCallResponseTypes} from '@constants/apps';
import NetworkManager from '@managers/network_manager';
import {cleanForm, createCallRequest, makeCallErrorResponse} from '@utils/apps';
import type {Client} from '@client/rest';
import type ClientError from '@client/rest/error';
import type PostModel from '@typings/database/models/servers/post';
import type {IntlShape} from 'react-intl';
export async function handleBindingClick<Res=unknown>(serverUrl: string, binding: AppBinding, context: AppContext, intl: IntlShape): Promise<{data?: AppCallResponse<Res>; error?: AppCallResponse<Res>}> {
// Fetch form

View File

@@ -2,8 +2,6 @@
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import {Model} from '@nozbe/watermelondb';
import {IntlShape} from 'react-intl';
import {DeviceEventEmitter} from 'react-native';
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
@@ -17,7 +15,7 @@ import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import AppsManager from '@managers/apps_manager';
import NetworkManager from '@managers/network_manager';
import {getActiveServer} from '@queries/app/servers';
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId} from '@queries/servers/channel';
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId, queryChannelsById} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getCommonSystemValues, getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getLicense, setCurrentChannelId, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams, removeChannelFromTeamHistory} from '@queries/servers/team';
@@ -40,7 +38,9 @@ import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from '
import {fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
import type {Client} from '@client/rest';
import type {Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
import type {IntlShape} from 'react-intl';
export type MyChannelsRequest = {
categories?: CategoryWithChannels[];
@@ -438,38 +438,37 @@ export async function fetchMyChannel(serverUrl: string, teamId: string, channelI
}
export async function fetchMissingDirectChannelsInfo(serverUrl: string, directChannels: Channel[], locale?: string, teammateDisplayNameSetting?: string, currentUserId?: string, fetchOnly = false) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
const displayNameByChannel: Record<string, string> = {};
const users: UserProfile[] = [];
const updatedChannels = new Set<Channel>();
const dms: Channel[] = [];
const dmIds: string[] = [];
const dmWithoutDisplayName = new Set<string>();
const gms: Channel[] = [];
for (const c of directChannels) {
if (c.type === General.DM_CHANNEL) {
dms.push(c);
dmIds.push(c.id);
if (!c.display_name) {
dmWithoutDisplayName.add(c.id);
}
continue;
}
gms.push(c);
}
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const displayNameByChannel: Record<string, string> = {};
const users: UserProfile[] = [];
const updatedChannels = new Set<Channel>();
const dms: Channel[] = [];
const dmIds: string[] = [];
const dmWithoutDisplayName = new Set<string>();
const gms: Channel[] = [];
const channelIds = new Set(directChannels.map((c) => c.id));
const storedChannels = await queryChannelsById(database, Array.from(channelIds)).fetch();
const storedChannelsMap = new Map(storedChannels.map((c) => [c.id, c]));
for (const c of directChannels) {
if (c.type === General.DM_CHANNEL) {
dms.push(c);
dmIds.push(c.id);
if (!c.display_name && !storedChannelsMap.get(c.id)?.displayName) {
dmWithoutDisplayName.add(c.id);
}
continue;
}
gms.push(c);
}
const currentUser = await getCurrentUser(database);
// let's filter those channels that we already have the users
const membersCount = await getMembersCountByChannelsId(database, dmIds);
const profileChannelsToFetch = dmIds.filter((id) => membersCount[id] <= 1 || dmWithoutDisplayName.has(id));
const profileChannelsToFetch = dmIds.filter((id) => membersCount[id] <= 1 && dmWithoutDisplayName.has(id));
const results = await Promise.all([
profileChannelsToFetch.length ? fetchProfilesPerChannels(serverUrl, profileChannelsToFetch, currentUserId, false) : Promise.resolve({data: undefined}),
fetchProfilesInGroupChannels(serverUrl, gms.map((c) => c.id), false),

View File

@@ -1,11 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IntlShape} from 'react-intl';
import {Alert} from 'react-native';
import {doAppSubmit, postEphemeralCallResponseForCommandArgs} from '@actions/remote/apps';
import {Client} from '@client/rest';
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
import {AppCallResponseTypes} from '@constants/apps';
import DatabaseManager from '@database/manager';
@@ -18,6 +16,9 @@ import {showAppForm} from '@screens/navigation';
import {handleDeepLink, matchDeepLink} from '@utils/deep_link';
import {tryOpenURL} from '@utils/url';
import type {Client} from '@client/rest';
import type {IntlShape} from 'react-intl';
export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string): Promise<{data?: CommandResponse; error?: string | {message: string}}> => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {

View File

@@ -2,13 +2,14 @@
// See LICENSE.txt for license information.
import {forceLogoutIfNecessary} from '@actions/remote/session';
import {Client} from '@client/rest';
import {Emoji, General} from '@constants';
import DatabaseManager from '@database/manager';
import {debounce} from '@helpers/api/general';
import NetworkManager from '@managers/network_manager';
import {queryCustomEmojisByName} from '@queries/servers/custom_emoji';
import type {Client} from '@client/rest';
export const fetchCustomEmojis = async (serverUrl: string, page = 0, perPage = General.PAGE_SIZE_DEFAULT, sort = Emoji.SORT_BY_NAME) => {
let client: Client;
try {
@@ -87,10 +88,17 @@ const debouncedFetchEmojiByNames = debounce(async (serverUrl: string) => {
promises.push(client.getCustomEmojiByName(name));
}
const emojis = await Promise.all(promises);
try {
await operator.handleCustomEmojis({emojis, prepareRecordsOnly: false});
const emojisResult = await Promise.allSettled(promises);
const emojis = emojisResult.reduce<CustomEmoji[]>((result, e) => {
if (e.status === 'fulfilled') {
result.push(e.value);
}
return result;
}, []);
if (emojis.length) {
await operator.handleCustomEmojis({emojis, prepareRecordsOnly: false});
}
return {error: undefined};
} catch (error) {
return {error};

View File

@@ -8,6 +8,7 @@ import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnecte
import {getCurrentUser} from '@queries/servers/user';
import {setTeamLoading} from '@store/team_load_store';
import {deleteV1Data} from '@utils/file';
import {isTablet} from '@utils/helpers';
import {logInfo} from '@utils/log';
import {handleEntryAfterLoadNavigation, registerDeviceToken, syncOtherServers, verifyPushProxy} from './common';
@@ -29,7 +30,7 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
// clear lastUnreadChannelId
const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''});
if (removeLastUnreadChannelId) {
operator.batchRecords(removeLastUnreadChannelId);
await operator.batchRecords(removeLastUnreadChannelId);
}
const {database} = operator;
@@ -47,7 +48,12 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData} = entryData;
if (isUpgrade && meData?.user) {
const me = await prepareCommonSystemValues(operator, {currentUserId: meData.user.id});
const isTabletDevice = await isTablet();
const me = await prepareCommonSystemValues(operator, {
currentUserId: meData.user.id,
currentTeamId: initialTeamId,
currentChannelId: isTabletDevice ? initialChannelId : undefined,
});
if (me?.length) {
await operator.batchRecords(me);
}
@@ -84,8 +90,8 @@ export async function upgradeEntry(serverUrl: string) {
const error = configAndLicense.error || entryData.error;
if (!error) {
DatabaseManager.updateServerIdentifier(serverUrl, configAndLicense.config!.DiagnosticId);
DatabaseManager.setActiveServerDatabase(serverUrl);
await DatabaseManager.updateServerIdentifier(serverUrl, configAndLicense.config!.DiagnosticId);
await DatabaseManager.setActiveServerDatabase(serverUrl);
deleteV1Data();
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database, Model} from '@nozbe/watermelondb';
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, MyChannelsRequest} from '@actions/remote/channel';
import {fetchGroupsForMember} from '@actions/remote/groups';
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
@@ -37,6 +35,7 @@ import {logDebug} from '@utils/log';
import {processIsCRTEnabled} from '@utils/thread';
import type ClientError from '@client/rest/error';
import type {Database, Model} from '@nozbe/watermelondb';
export type AppEntryData = {
initialTeamId: string;

View File

@@ -1,10 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database} from '@nozbe/watermelondb';
import {storeConfigAndLicense} from '@actions/local/systems';
import {MyChannelsRequest} from '@actions/remote/channel';
import {fetchGroupsForMember} from '@actions/remote/groups';
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
import {MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
@@ -25,7 +22,9 @@ import {processIsCRTEnabled} from '@utils/thread';
import {teamsToRemove, FETCH_UNREADS_TIMEOUT, entryRest, EntryResponse, entryInitialChannelId, restDeferredAppEntryActions, getRemoveTeamIds} from './common';
import type {MyChannelsRequest} from '@actions/remote/channel';
import type ClientError from '@client/rest/error';
import type {Database} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
export async function deferredAppEntryGraphQLActions(

View File

@@ -1,20 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ClientResponse, ClientResponseError} from '@mattermost/react-native-network-client';
import {Client} from '@client/rest';
import ClientError from '@client/rest/error';
import {DOWNLOAD_TIMEOUT} from '@constants/network';
import NetworkManager from '@managers/network_manager';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
import type ClientError from '@client/rest/error';
import type {ClientResponse, ClientResponseError} from '@mattermost/react-native-network-client';
export const downloadFile = (serverUrl: string, fileId: string, desitnation: string) => { // Let it throw and handle it accordingly
const client = NetworkManager.getClient(serverUrl);
return client.apiClient.download(client.getFileRoute(fileId), desitnation.replace('file://', ''), {timeoutInterval: DOWNLOAD_TIMEOUT});
};
export const downloadProfileImage = (serverUrl: string, userId: string, lastPictureUpdate: number, destination: string) => { // Let it throw and handle it accordingly
const client = NetworkManager.getClient(serverUrl);
return client.apiClient.download(client.getProfilePictureUrl(userId, lastPictureUpdate), destination.replace('file://', ''), {timeoutInterval: DOWNLOAD_TIMEOUT});
};
export const uploadFile = (
serverUrl: string,
file: FileInfo,

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Client} from '@client/rest';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {getChannelById} from '@queries/servers/channel';
@@ -9,6 +8,8 @@ import {getTeamById} from '@queries/servers/team';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
export const fetchGroup = async (serverUrl: string, id: string, fetchOnly = false) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);

View File

@@ -436,7 +436,7 @@ export async function fetchPostsBefore(serverUrl: string, channelId: string, pos
await operator.batchRecords(models);
} catch (error) {
logError('FETCH AUTHORS ERROR', error);
logError('FETCH POSTS BEFORE ERROR', error);
}
}
@@ -544,9 +544,15 @@ export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOn
}
if (promises.length) {
const result = await Promise.all(promises);
const authors = result.flat();
const authorsResult = await Promise.allSettled(promises);
const result = authorsResult.reduce<UserProfile[][]>((acc, item) => {
if (item.status === 'fulfilled') {
acc.push(item.value);
}
return acc;
}, []);
const authors = result.flat();
if (!fetchOnly && authors.length) {
await operator.handleUsers({
users: authors,

View File

@@ -178,3 +178,19 @@ export const setDirectChannelVisible = async (serverUrl: string, channelId: stri
return {error};
}
};
export const savePreferredSkinTone = async (serverUrl: string, skinCode: string) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const userId = await getCurrentUserId(database);
const pref: PreferenceType = {
user_id: userId,
category: Preferences.CATEGORY_EMOJI,
name: Preferences.EMOJI_SKINTONE,
value: skinCode,
};
return savePreference(serverUrl, [pref]);
} catch (error) {
return {error};
}
};

View File

@@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Model} from '@nozbe/watermelondb';
import {addRecentReaction} from '@actions/local/reactions';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
@@ -14,6 +12,7 @@ import {getEmojiFirstAlias} from '@utils/emoji/helpers';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
import type {Model} from '@nozbe/watermelondb';
import type PostModel from '@typings/database/models/servers/post';
export async function addReaction(serverUrl: string, postId: string, emojiName: string) {

View File

@@ -1,11 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Model} from '@nozbe/watermelondb';
import {DeviceEventEmitter} from 'react-native';
import {removeUserFromTeam as localRemoveUserFromTeam} from '@actions/local/team';
import {Client} from '@client/rest';
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
import {Events} from '@constants';
import DatabaseManager from '@database/manager';
@@ -27,7 +25,9 @@ import {fetchPostsForChannel, fetchPostsForUnreadChannels} from './post';
import {fetchRolesIfNeeded} from './role';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
import type ClientError from '@client/rest/error';
import type {Model} from '@nozbe/watermelondb';
export type MyTeamsRequest = {
teams?: Team[];

View File

@@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Model from '@nozbe/watermelondb/Model';
import {markTeamThreadsAsRead, markThreadAsViewed, processReceivedThreads, switchToThread, updateTeamThreadsSync, updateThread} from '@actions/local/thread';
import {fetchPostThread} from '@actions/remote/post';
import {General} from '@constants';
@@ -19,6 +17,7 @@ import {getThreadsListEdges} from '@utils/thread';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
import type Model from '@nozbe/watermelondb/Model';
type FetchThreadsOptions = {
before?: string;
@@ -361,7 +360,7 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
const allNewThreads = await fetchThreads(
serverUrl,
teamId,
{deleted: true, since: syncData.latest},
{deleted: true, since: syncData.latest + 1},
);
if (allNewThreads.error) {
return {error: allNewThreads.error};

View File

@@ -3,7 +3,6 @@
/* eslint-disable max-lines */
import {Model} from '@nozbe/watermelondb';
import {chunk} from 'lodash';
import {updateChannelsDisplayName} from '@actions/local/channel';
@@ -26,6 +25,7 @@ import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
import type ClientError from '@client/rest/error';
import type {Model} from '@nozbe/watermelondb';
import type UserModel from '@typings/database/models/servers/user';
export type MyUserRequest = {

View File

@@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Model} from '@nozbe/watermelondb';
import {addChannelToDefaultCategory} from '@actions/local/category';
import {
markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt,
@@ -22,6 +20,8 @@ import {getCurrentUser, getTeammateNameDisplay, getUserById} from '@queries/serv
import EphemeralStore from '@store/ephemeral_store';
import {logDebug} from '@utils/log';
import type {Model} from '@nozbe/watermelondb';
// Received when current user created a channel in a different client
export async function handleChannelCreatedEvent(serverUrl: string, msg: any) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
@@ -128,7 +128,7 @@ export async function handleChannelViewedEvent(serverUrl: string, msg: any) {
const currentChannelId = await getCurrentChannelId(database);
if (activeServerUrl !== serverUrl || (currentChannelId !== channelId && !EphemeralStore.isSwitchingToChannel(channelId))) {
await markChannelAsViewed(serverUrl, channelId, false);
await markChannelAsViewed(serverUrl, channelId);
}
} catch {
// do nothing

View File

@@ -444,7 +444,7 @@ async function fetchPostDataIfNeeded(serverUrl: string) {
await fetchPostsForChannel(serverUrl, currentChannelId);
markChannelAsRead(serverUrl, currentChannelId);
if (!EphemeralStore.wasNotificationTapped()) {
markChannelAsViewed(serverUrl, currentChannelId);
markChannelAsViewed(serverUrl, currentChannelId, true);
}
EphemeralStore.setNotificationTapped(false);
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Model} from '@nozbe/watermelondb';
import {DeviceEventEmitter} from 'react-native';
import {storeMyChannelsForTeam, markChannelAsUnread, markChannelAsViewed, updateLastPostAt} from '@actions/local/channel';
@@ -20,12 +19,11 @@ import NavigationStore from '@store/navigation_store';
import {isTablet} from '@utils/helpers';
import {isFromWebhook, isSystemMessage, shouldIgnorePost} from '@utils/post';
import type {Model} from '@nozbe/watermelondb';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
function preparedMyChannelHack(myChannel: MyChannelModel) {
// @ts-expect-error hack accessing _preparedState
if (!myChannel._preparedState) {
// @ts-expect-error hack setting _preparedState
myChannel._preparedState = null;
}
}
@@ -143,7 +141,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
markChannelAsRead(serverUrl, post.channel_id);
} else if (markAsViewed) {
preparedMyChannelHack(myChannel);
const {member: viewedAt} = await markChannelAsViewed(serverUrl, post.channel_id, true);
const {member: viewedAt} = await markChannelAsViewed(serverUrl, post.channel_id, false, true);
if (viewedAt) {
models.push(viewedAt);
}
@@ -164,8 +162,13 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
}
}
let actionType: string = ActionType.POSTS.RECEIVED_NEW;
if (isCRTEnabled && post.root_id) {
actionType = ActionType.POSTS.RECEIVED_IN_THREAD;
}
const postModels = await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_NEW,
actionType,
order: [post.id],
posts: [post],
prepareRecordsOnly: true,
@@ -203,8 +206,14 @@ export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage)
models.push(...authorsModels);
}
let actionType: string = ActionType.POSTS.RECEIVED_NEW;
const isCRTEnabled = await getIsCRTEnabled(operator.database);
if (isCRTEnabled && post.root_id) {
actionType = ActionType.POSTS.RECEIVED_IN_THREAD;
}
const postModels = await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_NEW,
actionType,
order: [post.id],
posts: [post],
prepareRecordsOnly: true,

View File

@@ -1,15 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Model} from '@nozbe/watermelondb';
import {removeUserFromTeam} from '@actions/local/team';
import {fetchMyChannelsForTeam} from '@actions/remote/channel';
import {fetchRoles} from '@actions/remote/role';
import {fetchMyTeam, handleKickFromTeam, updateCanJoinTeams} from '@actions/remote/team';
import {updateUsersNoLongerVisible} from '@actions/remote/user';
import DatabaseManager from '@database/manager';
import ServerDataOperator from '@database/operator/server_data_operator';
import NetworkManager from '@managers/network_manager';
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
@@ -19,6 +16,9 @@ import EphemeralStore from '@store/ephemeral_store';
import {setTeamLoading} from '@store/team_load_store';
import {logDebug} from '@utils/log';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Model} from '@nozbe/watermelondb';
export async function handleTeamArchived(serverUrl: string, msg: WebSocketMessage) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);

View File

@@ -2,12 +2,22 @@
// See LICENSE.txt for license information.
import {markTeamThreadsAsRead, processReceivedThreads, updateThread} from '@actions/local/thread';
import {getCurrentTeamId} from '@app/queries/servers/system';
import DatabaseManager from '@database/manager';
import EphemeralStore from '@store/ephemeral_store';
export async function handleThreadUpdatedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
try {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return;
}
const thread: Thread = JSON.parse(msg.data.thread);
const teamId = msg.broadcast.team_id;
let teamId = msg.broadcast.team_id;
if (!teamId) {
teamId = await getCurrentTeamId(database);
}
// Mark it as following
thread.is_following = true;

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Model} from '@nozbe/watermelondb';
import {DeviceEventEmitter} from 'react-native';
import {updateChannelsDisplayName} from '@actions/local/channel';
@@ -16,6 +15,8 @@ import {getConfig, getLicense} from '@queries/servers/system';
import {getCurrentUser} from '@queries/servers/user';
import {displayUsername} from '@utils/user';
import type {Model} from '@nozbe/watermelondb';
export async function handleUserUpdatedEvent(serverUrl: string, msg: WebSocketMessage) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {

View File

@@ -1,11 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Client} from '@client/rest';
import {MEMBERS_PER_PAGE} from '@constants/graphql';
import NetworkManager from '@managers/network_manager';
import QueryNames from './constants';
import type {Client} from '@client/rest';
const doGQLQuery = async (serverUrl: string, query: string, variables: {[name: string]: any}, operationName: string) => {
let client: Client;
try {

View File

@@ -1,10 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ClientResponse, ClientResponseError, ProgressPromise, UploadRequestOptions} from '@mattermost/react-native-network-client';
import {toMilliseconds} from '@utils/datetime';
import type {ClientResponse, ClientResponseError, ProgressPromise, UploadRequestOptions} from '@mattermost/react-native-network-client';
export interface ClientFilesMix {
getFileUrl: (fileId: string, timestamp: number) => string;
getFileThumbnailUrl: (fileId: string, timestamp: number) => string;

View File

@@ -33,6 +33,7 @@ export default class WebSocketClient {
private firstConnectCallback?: () => void;
private missedEventsCallback?: () => void;
private reconnectCallback?: () => void;
private reliableReconnectCallback?: () => void;
private errorCallback?: Function;
private closeCallback?: (connectFailCount: number, lastDisconnect: number) => void;
private connectingCallback?: () => void;
@@ -148,8 +149,11 @@ export default class WebSocketClient {
logInfo('websocket re-established connection to', this.url);
if (!reliableWebSockets && this.reconnectCallback) {
this.reconnectCallback();
} else if (reliableWebSockets && this.serverSequence && this.missedEventsCallback) {
this.missedEventsCallback();
} else if (reliableWebSockets) {
this.reliableReconnectCallback?.();
if (this.serverSequence && this.missedEventsCallback) {
this.missedEventsCallback();
}
}
} else if (this.firstConnectCallback) {
logInfo('websocket connected to', this.url);
@@ -295,6 +299,10 @@ export default class WebSocketClient {
this.reconnectCallback = callback;
}
public setReliableReconnectCallback(callback: () => void) {
this.reliableReconnectCallback = callback;
}
public setErrorCallback(callback: Function) {
this.errorCallback = callback;
}

View File

@@ -9,6 +9,7 @@ import {
View,
} from 'react-native';
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {dismissAnnouncement} from '@actions/local/systems';
import CompassIcon from '@components/compass_icon';
@@ -17,6 +18,7 @@ import {ANNOUNCEMENT_BAR_HEIGHT} from '@constants/view';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {bottomSheet} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@@ -82,6 +84,7 @@ const AnnouncementBanner = ({
const intl = useIntl();
const serverUrl = useServerUrl();
const height = useSharedValue(0);
const {bottom} = useSafeAreaInsets();
const theme = useTheme();
const [visible, setVisible] = useState(false);
const style = getStyle(theme);
@@ -100,19 +103,20 @@ const AnnouncementBanner = ({
defaultMessage: 'Announcement',
});
let snapPoint = SNAP_POINT_WITHOUT_DISMISS;
if (allowDismissal) {
snapPoint += DISMISS_BUTTON_HEIGHT;
}
const snapPoint = bottomSheetSnapPoint(
1,
SNAP_POINT_WITHOUT_DISMISS + (allowDismissal ? DISMISS_BUTTON_HEIGHT : 0),
bottom,
);
bottomSheet({
closeButtonId: CLOSE_BUTTON_ID,
title,
renderContent,
snapPoints: [snapPoint, 10],
snapPoints: [1, snapPoint],
theme,
});
}, [theme.sidebarHeaderTextColor, intl.locale, renderContent, allowDismissal]);
}, [theme.sidebarHeaderTextColor, intl.locale, renderContent, allowDismissal, bottom]);
const handleDismiss = useCallback(() => {
dismissAnnouncement(serverUrl, bannerText);

View File

@@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BottomSheetScrollView} from '@gorhom/bottom-sheet';
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {Text, View} from 'react-native';
import {ScrollView, Text, View} from 'react-native';
import Button from 'react-native-button';
import {ScrollView} from 'react-native-gesture-handler';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {dismissAnnouncement} from '@actions/local/systems';
@@ -84,6 +84,8 @@ const ExpandedAnnouncementBanner = ({
return [style.container, {marginBottom: insets.bottom + 10}];
}, [style, insets.bottom]);
const Scroll = useMemo(() => (isTablet ? ScrollView : BottomSheetScrollView), [isTablet]);
return (
<View style={containerStyle}>
{!isTablet && (
@@ -94,7 +96,7 @@ const ExpandedAnnouncementBanner = ({
})}
</Text>
)}
<ScrollView
<Scroll
style={style.scrollContainer}
>
<Markdown
@@ -106,7 +108,7 @@ const ExpandedAnnouncementBanner = ({
theme={theme}
location={Screens.BOTTOM_SHEET}
/>
</ScrollView>
</Scroll>
<Button
containerStyle={buttonStyles.okay.button}
onPress={close}

View File

@@ -31,8 +31,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
elevation: 3,
},
shadow: {
backgroundColor: theme.centerChannelBg,
shadowColor: '#000',
shadowOpacity: 0.12,
shadowOpacity: 1,
shadowRadius: 6,
shadowOffset: {
width: 0,

View File

@@ -1,9 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database} from '@nozbe/watermelondb';
import {IntlShape} from 'react-intl';
import {doAppFetchForm, doAppLookup} from '@actions/remote/apps';
import {fetchChannelById, fetchChannelByName, searchChannels} from '@actions/remote/channel';
import {fetchUsersByIds, fetchUsersByUsernames, searchUsers} from '@actions/remote/user';
@@ -17,8 +14,10 @@ import {createCallRequest, filterEmptyOptions} from '@utils/apps';
import {getChannelSuggestions, getUserSuggestions, inTextMentionSuggestions} from './mentions';
import type {Database} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
import type UserModel from '@typings/database/models/servers/user';
import type {IntlShape} from 'react-intl';
/* eslint-disable max-lines */

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MessageDescriptor} from '@formatjs/intl/src/types';
import React from 'react';
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native';
@@ -9,6 +8,8 @@ import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import type {MessageDescriptor} from '@formatjs/intl/src/types';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {

View File

@@ -3,12 +3,13 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {StyleProp, ViewStyle} from 'react-native';
import OptionBox from '@components/option_box';
import {Screens} from '@constants';
import {dismissBottomSheet, goToScreen, showModal} from '@screens/navigation';
import type {StyleProp, ViewStyle} from 'react-native';
type Props = {
channelId: string;
containerStyle?: StyleProp<ViewStyle>;

View File

@@ -24,12 +24,12 @@ type Props = {
testID?: string;
}
const OPTIONS_HEIGHT = 62;
export const CHANNEL_ACTIONS_OPTIONS_HEIGHT = 62;
const styles = StyleSheet.create({
wrapper: {
flexDirection: 'row',
height: OPTIONS_HEIGHT,
height: CHANNEL_ACTIONS_OPTIONS_HEIGHT,
},
separator: {
width: 8,

View File

@@ -3,13 +3,14 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {StyleProp, ViewStyle} from 'react-native';
import {toggleFavoriteChannel} from '@actions/remote/category';
import OptionBox from '@components/option_box';
import {useServerUrl} from '@context/server';
import {dismissBottomSheet} from '@screens/navigation';
import type {StyleProp, ViewStyle} from 'react-native';
type Props = {
channelId: string;
containerStyle?: StyleProp<ViewStyle>;

View File

@@ -3,7 +3,6 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {StyleProp, ViewStyle} from 'react-native';
import CompassIcon from '@components/compass_icon';
import OptionBox from '@components/option_box';
@@ -12,6 +11,8 @@ import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {dismissBottomSheet, showModal} from '@screens/navigation';
import type {StyleProp, ViewStyle} from 'react-native';
type Props = {
channelId: string;
containerStyle?: StyleProp<ViewStyle>;

View File

@@ -3,13 +3,14 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {StyleProp, ViewStyle} from 'react-native';
import {toggleMuteChannel} from '@actions/remote/channel';
import OptionBox from '@components/option_box';
import {useServerUrl} from '@context/server';
import {dismissBottomSheet} from '@screens/navigation';
import type {StyleProp, ViewStyle} from 'react-native';
type Props = {
channelId: string;
containerStyle?: StyleProp<ViewStyle>;

View File

@@ -3,12 +3,13 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {StyleProp, ViewStyle} from 'react-native';
import OptionBox from '@components/option_box';
import {Screens} from '@constants';
import {dismissBottomSheet, goToScreen, showModal} from '@screens/navigation';
import type {StyleProp, ViewStyle} from 'react-native';
type Props = {
channelId: string;
containerStyle?: StyleProp<ViewStyle>;

View File

@@ -2,6 +2,23 @@
exports[`components/channel_list/categories/body/channel_item should match snapshot 1`] = `
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
@@ -115,6 +132,23 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
exports[`components/channel_list/categories/body/channel_item should match snapshot when it has a call 1`] = `
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
@@ -257,6 +291,23 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
exports[`components/channel_list/categories/body/channel_item should match snapshot when it has a draft 1`] = `
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}

View File

@@ -10,7 +10,7 @@ import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {observeChannelsWithCalls} from '@calls/state';
import {General} from '@constants';
import {withServerUrl} from '@context/server';
import {observeChannelSettings, observeMyChannel} from '@queries/servers/channel';
import {observeChannelSettings, observeMyChannel, queryChannelMembers} from '@queries/servers/channel';
import {queryDraft} from '@queries/servers/drafts';
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
import {observeTeam} from '@queries/servers/team';
@@ -67,7 +67,7 @@ const enhance = withObservables(['channel', 'showTeamName'], ({
let membersCount = of$(0);
if (channel.type === General.GM_CHANNEL) {
membersCount = channel.members.observeCount(false);
membersCount = queryChannelMembers(database, channel.id).observeCount(false);
}
const isUnread = myChannel.pipe(

View File

@@ -9,6 +9,23 @@ exports[`components/channel_list_row should be selected 1`] = `
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
@@ -103,6 +120,23 @@ exports[`components/channel_list_row should match snapshot with delete_at filled
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
@@ -175,6 +209,23 @@ exports[`components/channel_list_row should match snapshot with open channel ico
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
@@ -247,6 +298,23 @@ exports[`components/channel_list_row should match snapshot with private channel
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
@@ -319,6 +387,23 @@ exports[`components/channel_list_row should match snapshot with purpose filled i
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
@@ -406,6 +491,23 @@ exports[`components/channel_list_row should match snapshot with shared filled in
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Database from '@nozbe/watermelondb/Database';
import React from 'react';
import {renderWithEverything} from '@test/intl-test-helper';
@@ -9,6 +8,8 @@ import TestHelper from '@test/test_helper';
import ChannelListRow from '.';
import type Database from '@nozbe/watermelondb/Database';
describe('components/channel_list_row', () => {
let database: Database;
const channel: Channel = {

View File

@@ -5,7 +5,6 @@ import Clipboard from '@react-native-clipboard/clipboard';
import React, {useCallback} from 'react';
import {BaseOption} from '@components/common_post_options';
import {Screens} from '@constants';
import {SNACK_BAR_TYPE} from '@constants/snack_bar';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
@@ -13,10 +12,11 @@ import {dismissBottomSheet} from '@screens/navigation';
import {showSnackBar} from '@utils/snack_bar';
import type PostModel from '@typings/database/models/servers/post';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
bottomSheetId: typeof Screens[keyof typeof Screens];
sourceScreen: typeof Screens[keyof typeof Screens];
bottomSheetId: AvailableScreens;
sourceScreen: AvailableScreens;
post: PostModel;
teamName: string;
}

View File

@@ -6,6 +6,7 @@ import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeChannel} from '@queries/servers/channel';
import {observeCurrentTeamId} from '@queries/servers/system';
import {observeTeam} from '@queries/servers/team';
@@ -17,7 +18,7 @@ import type TeamModel from '@typings/database/models/servers/team';
const enhanced = withObservables(['post'], ({post, database}: WithDatabaseArgs & { post: PostModel }) => {
const currentTeamId = observeCurrentTeamId(database);
const channel = post.channel.observe();
const channel = observeChannel(database, post.id);
const teamName = combineLatest([channel, currentTeamId]).pipe(
switchMap(([c, tid]) => {

View File

@@ -5,15 +5,15 @@ import React, {useCallback} from 'react';
import {updateThreadFollowing} from '@actions/remote/thread';
import {BaseOption} from '@components/common_post_options';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import type ThreadModel from '@typings/database/models/servers/thread';
import type {AvailableScreens} from '@typings/screens/navigation';
type FollowThreadOptionProps = {
bottomSheetId: typeof Screens[keyof typeof Screens];
bottomSheetId: AvailableScreens;
thread: ThreadModel;
teamId?: string;
};

View File

@@ -1,18 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {observeTeamIdByThread} from '@queries/servers/thread';
import FollowThreadOption from './follow_thread_option';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ThreadModel from '@typings/database/models/servers/thread';
const enhanced = withObservables(['thread'], ({thread}: { thread: ThreadModel }) => {
const enhanced = withObservables(['thread'], ({thread, database}: {thread: ThreadModel} & WithDatabaseArgs) => {
return {
teamId: observeTeamIdByThread(thread),
teamId: observeTeamIdByThread(database, thread),
};
});
export default enhanced(FollowThreadOption);
export default withDatabase(enhanced(FollowThreadOption));

View File

@@ -5,16 +5,16 @@ import React, {useCallback} from 'react';
import {fetchAndSwitchToThread} from '@actions/remote/thread';
import {BaseOption} from '@components/common_post_options';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import type PostModel from '@typings/database/models/servers/post';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
post: PostModel;
bottomSheetId: typeof Screens[keyof typeof Screens];
bottomSheetId: AvailableScreens;
}
const ReplyOption = ({post, bottomSheetId}: Props) => {
const serverUrl = useServerUrl();

View File

@@ -5,13 +5,14 @@ import React, {useCallback} from 'react';
import {deleteSavedPost, savePostPreference} from '@actions/remote/preference';
import {BaseOption} from '@components/common_post_options';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import type {AvailableScreens} from '@typings/screens/navigation';
type CopyTextProps = {
bottomSheetId: typeof Screens[keyof typeof Screens];
bottomSheetId: AvailableScreens;
isSaved: boolean;
postId: string;
}

View File

@@ -106,6 +106,7 @@ const ConnectionBanner = ({
}
return () => {
clearTimeoutRef(openTimeout);
clearTimeoutRef(closeTimeout);
};
}, []);
@@ -125,7 +126,7 @@ const ConnectionBanner = ({
}
}, [isConnected]);
useEffect(() => {
useDidUpdate(() => {
if (appState === 'active') {
if (!isConnected && !visible) {
if (!openTimeout.current) {
@@ -138,10 +139,11 @@ const ConnectionBanner = ({
}
}
} else {
setVisible(false);
clearTimeoutRef(openTimeout);
clearTimeoutRef(closeTimeout);
}
}, [appState]);
}, [appState === 'active']);
useEffect(() => {
height.value = withTiming(visible ? ANNOUNCEMENT_BAR_HEIGHT : 0, {
@@ -149,12 +151,6 @@ const ConnectionBanner = ({
});
}, [visible]);
useEffect(() => {
return () => {
clearTimeoutRef(closeTimeout);
};
});
const bannerStyle = useAnimatedStyle(() => ({
height: height.value,
}));

View File

@@ -2,6 +2,23 @@
exports[`components/custom_status/clear_button should match snapshot 1`] = `
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Database from '@nozbe/watermelondb/Database';
import React from 'react';
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
@@ -9,6 +8,8 @@ import {CustomStatusDurationEnum} from '@constants/custom_status';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import type Database from '@nozbe/watermelondb/Database';
describe('components/custom_status/custom_status_emoji', () => {
let database: Database | undefined;
beforeAll(async () => {

View File

@@ -57,7 +57,7 @@ const Emoji = (props: EmojiProps) => {
} catch {
// do nothing
}
} else if (name && !isUnicodeEmoji(name)) {
} else if (name && (name.length > 1 || !isUnicodeEmoji(name))) {
fetchCustomEmojiInBatch(serverUrl, name);
}
}

View File

@@ -3,7 +3,7 @@
import React, {useMemo} from 'react';
import {EmojiComponent, EmojiProps} from '@typings/components/emoji';
import type {EmojiComponent, EmojiProps} from '@typings/components/emoji';
let emojiComponent: EmojiComponent;

View File

@@ -1,76 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {View} from 'react-native';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import SectionIcon from './icon';
export const SCROLLVIEW_NATIVE_ID = 'emojiSelector';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
bottom: 10,
height: 35,
position: 'absolute',
width: '100%',
},
background: {
backgroundColor: theme.centerChannelBg,
},
pane: {
flexDirection: 'row',
borderRadius: 10,
paddingHorizontal: 10,
width: '100%',
borderColor: changeOpacity(theme.centerChannelColor, 0.3),
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderWidth: 1,
justifyContent: 'space-between',
},
}));
export type SectionIconType = {
key: string;
icon: string;
}
type Props = {
currentIndex: number;
sections: SectionIconType[];
scrollToIndex: (index: number) => void;
}
const EmojiSectionBar = ({currentIndex, sections, scrollToIndex}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (
<KeyboardTrackingView
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
normalList={true}
style={styles.container}
testID='emoji_picker.emoji_sections.section_bar'
>
<View style={styles.background}>
<View style={styles.pane}>
{sections.map((section, index) => (
<SectionIcon
currentIndex={currentIndex}
key={section.key}
icon={section.icon}
index={index}
scrollToIndex={scrollToIndex}
theme={theme}
/>
))}
</View>
</View>
</KeyboardTrackingView>
);
};
export default EmojiSectionBar;

View File

@@ -1,35 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {StyleProp, ViewStyle} from 'react-native';
import Emoji from '@components/emoji';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {preventDoubleTap} from '@utils/tap';
type Props = {
name: string;
onEmojiPress: (emoji: string) => void;
size?: number;
style: StyleProp<ViewStyle>;
}
const TouchableEmoji = ({name, onEmojiPress, size = 30, style}: Props) => {
const onPress = useCallback(preventDoubleTap(() => onEmojiPress(name)), []);
return (
<TouchableWithFeedback
onPress={onPress}
style={style}
type={'opacity'}
>
<Emoji
emojiName={name}
size={size}
/>
</TouchableWithFeedback>
);
};
export default React.memo(TouchableEmoji);

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ClientResponse, ProgressPromise} from '@mattermost/react-native-network-client';
import React, {forwardRef, useImperativeHandle, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Platform, StatusBar, StatusBarStyle, StyleSheet, TouchableOpacity, View} from 'react-native';
@@ -21,6 +20,7 @@ import {emptyFunction} from '@utils/general';
import FileIcon from './file_icon';
import type {Client} from '@client/rest';
import type {ClientResponse, ProgressPromise} from '@mattermost/react-native-network-client';
export type DocumentFileRef = {
handlePreviewPress: () => void;

View File

@@ -104,7 +104,8 @@ const ImageFile = ({
const props: ProgressiveImageProps = {};
if (file.localPath) {
props.defaultSource = {uri: file.localPath};
const prefix = file.localPath.startsWith('file://') ? '' : 'file://';
props.defaultSource = {uri: prefix + file.localPath};
} else if (file.id) {
if (file.mini_preview && file.mime_type) {
props.thumbnailUri = `data:${file.mime_type};base64,${file.mini_preview}`;

View File

@@ -6,6 +6,7 @@ import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$, from as from$} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
import {queryFilesForPost} from '@queries/servers/file';
import {observeConfigBooleanValue, observeLicense} from '@queries/servers/system';
import {fileExists} from '@utils/file';
@@ -47,7 +48,7 @@ const enhance = withObservables(['post'], ({database, post}: EnhanceProps) => {
map(([download, compliance]) => compliance || download),
);
const filesInfo = post.files.observeWithColumns(['local_path']).pipe(
const filesInfo = queryFilesForPost(database, post.id).observeWithColumns(['local_path']).pipe(
switchMap((fs) => from$(filesLocalPathValidation(fs, post.userId))),
);

View File

@@ -2,11 +2,10 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {StyleSheet, useWindowDimensions, View} from 'react-native';
import {createThumbnail} from 'react-native-create-thumbnail';
import {StyleSheet, useWindowDimensions, View, NativeModules} from 'react-native';
import {updateLocalFile} from '@actions/local/file';
import {buildFilePreviewUrl, fetchPublicLink} from '@actions/remote/file';
import {buildFilePreviewUrl, buildFileUrl} from '@actions/remote/file';
import CompassIcon from '@components/compass_icon';
import ProgressiveImage from '@components/progressive_image';
import {useServerUrl} from '@context/server';
@@ -18,6 +17,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import FileIcon from './file_icon';
import type {ResizeMode} from 'react-native-fast-image';
const {createThumbnail} = NativeModules.MattermostManaged;
type Props = {
index: number;
@@ -84,11 +84,9 @@ const VideoFile = ({
try {
const exists = data.mini_preview ? await fileExists(data.mini_preview) : false;
if (!data.mini_preview || !exists) {
// We use the public link to avoid having to pass the token through a third party
// library
const publicUri = await fetchPublicLink(serverUrl, data.id!);
if (('link') in publicUri) {
const {path: uri, height, width} = await createThumbnail({url: data.localPath || publicUri.link, timeStamp: 2000});
const videoUrl = buildFileUrl(serverUrl, data.id!);
if (videoUrl) {
const {path: uri, height, width} = await createThumbnail({url: data.localPath || videoUrl, timeStamp: 2000});
data.mini_preview = uri;
data.height = height;
data.width = width;

View File

@@ -73,6 +73,23 @@ exports[`Loading Error should match snapshot 1`] = `
Error description
</Text>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}

View File

@@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import {useManagedConfig} from '@mattermost/react-native-emm';
import {Database} from '@nozbe/watermelondb';
import Clipboard from '@react-native-clipboard/clipboard';
import React, {useCallback, useEffect, useMemo} from 'react';
import {useIntl} from 'react-intl';
@@ -21,6 +20,7 @@ import {bottomSheet, dismissBottomSheet, openAsBottomSheet} from '@screens/navig
import {bottomSheetSnapPoint} from '@utils/helpers';
import {displayUsername, getUsersByUsername} from '@utils/user';
import type {Database} from '@nozbe/watermelondb';
import type GroupModelType from '@typings/database/models/servers/group';
import type GroupMembershipModel from '@typings/database/models/servers/group_membership';
import type UserModelType from '@typings/database/models/servers/user';
@@ -34,7 +34,7 @@ type AtMentionProps = {
location: string;
mentionKeys?: Array<{key: string }>;
mentionName: string;
mentionStyle: TextStyle;
mentionStyle: StyleProp<TextStyle>;
onPostPress?: (e: GestureResponderEvent) => void;
teammateNameDisplay: string;
textStyle?: StyleProp<TextStyle>;
@@ -69,7 +69,7 @@ const AtMention = ({
const intl = useIntl();
const managedConfig = useManagedConfig<ManagedConfig>();
const theme = useTheme();
const insets = useSafeAreaInsets();
const {bottom} = useSafeAreaInsets();
const serverUrl = useServerUrl();
const user = useMemo(() => {
@@ -92,6 +92,7 @@ const AtMention = ({
// @ts-expect-error: The model constructor is hidden within WDB type definition
return new UserModel(database.get(USER), {username: ''});
}, [users, mentionName]);
const userMentionKeys = useMemo(() => {
if (mentionKeys) {
return mentionKeys;
@@ -195,14 +196,14 @@ const AtMention = ({
bottomSheet({
closeButtonId: 'close-at-mention',
renderContent,
snapPoints: [bottomSheetSnapPoint(2, ITEM_HEIGHT, insets.bottom), 10],
snapPoints: [1, bottomSheetSnapPoint(2, ITEM_HEIGHT, bottom)],
title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}),
theme,
});
}
}, [managedConfig, intl, theme, insets]);
}, [managedConfig, intl, theme, bottom]);
const mentionTextStyle = [];
const mentionTextStyle: StyleProp<TextStyle> = [];
let backgroundColor;
let canPress = false;

Some files were not shown because too many files have changed in this diff Show More