Compare commits

...

45 Commits

Author SHA1 Message Date
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
477 changed files with 8665 additions and 6767 deletions

View File

@@ -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

@@ -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 448
versionName "2.0.0"
versionCode 455
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"/>

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

@@ -133,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;

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

@@ -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';

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 {

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

@@ -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;

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;
}
}
@@ -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

@@ -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

@@ -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

@@ -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

@@ -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>;
@@ -203,7 +203,7 @@ const AtMention = ({
}
}, [managedConfig, intl, theme, bottom]);
const mentionTextStyle = [];
const mentionTextStyle: StyleProp<TextStyle> = [];
let backgroundColor;
let canPress = false;

View File

@@ -5,7 +5,7 @@ import {useManagedConfig} from '@mattermost/react-native-emm';
import {Parser, Node} from 'commonmark';
import Renderer from 'commonmark-react-renderer';
import React, {ReactElement, useMemo, useRef} from 'react';
import {Dimensions, GestureResponderEvent, Platform, StyleProp, Text, TextStyle, View, ViewStyle} from 'react-native';
import {Dimensions, GestureResponderEvent, Platform, StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle} from 'react-native';
import CompassIcon from '@components/compass_icon';
import Emoji from '@components/emoji';
@@ -143,13 +143,15 @@ const Markdown = ({
if (disableAtMentions) {
return renderText({context, literal: `@${mentionName}`});
}
const computedStyles = StyleSheet.flatten(computeTextStyle(textStyles, baseTextStyle, context));
const {fontFamily, fontSize, fontWeight} = computedStyles;
return (
<AtMention
channelId={channelId}
disableAtChannelMentionHighlight={disableAtChannelMentionHighlight}
mentionStyle={textStyles.mention}
textStyle={[computeTextStyle(textStyles, baseTextStyle, context), style.atMentionOpacity]}
mentionStyle={[textStyles.mention, {fontSize, fontWeight, fontFamily}]}
textStyle={[computedStyles, style.atMentionOpacity]}
isSearchResult={isSearchResult}
location={location}
mentionName={mentionName}

View File

@@ -85,7 +85,7 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc
const screen = Screens.CODE;
const passProps = {
code: content,
language,
language: getHighlightLanguageFromNameOrAlias(language),
textStyle,
};

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {PasteInputRef} from '@mattermost/react-native-paste-input';
import React, {useCallback, useRef} from 'react';
import {LayoutChangeEvent, Platform, ScrollView, View} from 'react-native';
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
@@ -16,6 +15,8 @@ import SendAction from '../send_action';
import Typing from '../typing';
import Uploads from '../uploads';
import type {PasteInputRef} from '@mattermost/react-native-paste-input';
type Props = {
testID?: string;
channelId: string;

View File

@@ -7,7 +7,7 @@ import React from 'react';
import {of as of$} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {observeChannel} from '@queries/servers/channel';
import {observeChannel, observeChannelInfo} from '@queries/servers/channel';
import {observeConfigBooleanValue, observeConfigIntValue} from '@queries/servers/system';
import PostInput from './post_input';
@@ -31,7 +31,7 @@ const enhanced = withObservables(['channelId'], ({database, channelId}: WithData
);
const membersInChannel = channel.pipe(
switchMap((c) => (c ? c.info.observe() : of$({memberCount: 0}))),
switchMap((c) => (c ? observeChannelInfo(database, c.id) : of$({memberCount: 0}))),
switchMap((i: ChannelInfoModel) => of$(i.memberCount)),
distinctUntilChanged(),
);

View File

@@ -7,7 +7,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {IntlShape, useIntl} from 'react-intl';
import {
Alert, AppState, AppStateStatus, DeviceEventEmitter, EmitterSubscription, Keyboard,
KeyboardTypeOptions, NativeSyntheticEvent, Platform, TextInputSelectionChangeEventData,
NativeSyntheticEvent, Platform, TextInputSelectionChangeEventData,
} from 'react-native';
import HWKeyboardEvent from 'react-native-hw-keyboard-event';
@@ -20,9 +20,10 @@ import {useIsTablet} from '@hooks/device';
import {t} from '@i18n';
import NavigationStore from '@store/navigation_store';
import {extractFileInfo} from '@utils/file';
import {switchKeyboardForCodeBlocks} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme, getKeyboardAppearanceFromTheme} from '@utils/theme';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
testID?: string;
channelDisplayName?: string;
@@ -125,7 +126,6 @@ export default function PostInput({
const lastNativeValue = useRef('');
const previousAppState = useRef(AppState.currentState);
const [keyboardType, setKeyboardType] = useState<KeyboardTypeOptions>('default');
const [longMessageAlertShown, setLongMessageAlertShown] = useState(false);
const disableCopyAndPaste = managedConfig.copyAndPasteProtection === 'true';
@@ -180,11 +180,6 @@ export default function PostInput({
const handlePostDraftSelectionChanged = useCallback((event: NativeSyntheticEvent<TextInputSelectionChangeEventData> | null, fromHandleTextChange = false) => {
const cp = fromHandleTextChange ? cursorPosition : event!.nativeEvent.selection.end;
if (Platform.OS === 'ios') {
const newKeyboardType = switchKeyboardForCodeBlocks(value, cp);
setKeyboardType(newKeyboardType);
}
updateCursorPosition(cp);
}, [updateCursorPosition, cursorPosition]);
@@ -194,11 +189,6 @@ export default function PostInput({
checkMessageLength(newValue);
// Workaround to avoid iOS emdash autocorrect in Code Blocks
if (Platform.OS === 'ios') {
handlePostDraftSelectionChanged(null, true);
}
if (
newValue &&
lastTypingEventSent.current + timeBetweenUserTypingUpdatesMilliseconds < Date.now() &&
@@ -211,7 +201,6 @@ export default function PostInput({
}, [
updateValue,
checkMessageLength,
handlePostDraftSelectionChanged,
timeBetweenUserTypingUpdatesMilliseconds,
channelId,
rootId,
@@ -228,7 +217,7 @@ export default function PostInput({
const handleHardwareEnterPress = useCallback((keyEvent: {pressedKey: string}) => {
const topScreen = NavigationStore.getVisibleScreen();
let sourceScreen = Screens.CHANNEL;
let sourceScreen: AvailableScreens = Screens.CHANNEL;
if (rootId) {
sourceScreen = Screens.THREAD;
} else if (isTablet) {
@@ -307,24 +296,24 @@ export default function PostInput({
return (
<PasteableTextInput
allowFontScaling={true}
testID={testID}
ref={inputRef}
disableCopyPaste={disableCopyAndPaste}
style={pasteInputStyle}
disableFullscreenUI={true}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
multiline={true}
onBlur={onBlur}
onChangeText={handleTextChange}
onFocus={onFocus}
onPaste={onPaste}
onSelectionChange={handlePostDraftSelectionChanged}
placeholder={intl.formatMessage(getPlaceHolder(rootId), {channelDisplayName})}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
multiline={true}
onBlur={onBlur}
onFocus={onFocus}
blurOnSubmit={false}
ref={inputRef}
smartPunctuation='disable'
submitBehavior='newline'
style={pasteInputStyle}
testID={testID}
underlineColorAndroid='transparent'
keyboardType={keyboardType}
onPaste={onPaste}
disableFullscreenUI={true}
textContentType='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
);
}

View File

@@ -4,7 +4,6 @@
import React from 'react';
import {useIntl} from 'react-intl';
import {View} from 'react-native';
import {CameraOptions} from 'react-native-image-picker';
import FormattedText from '@components/formatted_text';
import SlideUpPanelItem from '@components/slide_up_panel_item';
@@ -14,6 +13,8 @@ import {dismissBottomSheet} from '@screens/navigation';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import type {CameraOptions} from 'react-native-image-picker';
type Props = {
onPress: (options: CameraOptions) => void;
}

View File

@@ -4,7 +4,6 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Alert, StyleSheet} from 'react-native';
import {CameraOptions} from 'react-native-image-picker';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import CompassIcon from '@components/compass_icon';
@@ -22,6 +21,7 @@ import {changeOpacity} from '@utils/theme';
import CameraType from './camera_type';
import type {QuickActionAttachmentProps} from '@typings/components/post_draft_quick_action';
import type {CameraOptions} from 'react-native-image-picker';
const style = StyleSheet.create({
icon: {

View File

@@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
import {General, Permissions} from '@constants';
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
import {observeChannel, observeCurrentChannel} from '@queries/servers/channel';
import {observeChannel, observeChannelInfo, observeCurrentChannel} from '@queries/servers/channel';
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
import {observePermissionForChannel} from '@queries/servers/role';
import {observeConfigBooleanValue, observeConfigIntValue, observeCurrentUserId} from '@queries/servers/system';
@@ -56,7 +56,7 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
}),
);
const channelInfo = channel.pipe(switchMap((c) => (c ? c.info.observe() : of$(undefined))));
const channelInfo = channel.pipe(switchMap((c) => (c ? observeChannelInfo(database, c.id) : of$(undefined))));
const membersCount = channelInfo.pipe(
switchMap((i) => (i ? of$(i.memberCount) : of$(0))),
);

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