Compare commits
39 Commits
release-2.
...
modularize
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ade9cb49d9 | ||
|
|
3643adf391 | ||
|
|
778b512951 | ||
|
|
4d827d1e44 | ||
|
|
92d37995c5 | ||
|
|
b86ec123c0 | ||
|
|
6e5c443c26 | ||
|
|
b22c80f244 | ||
|
|
ed15730d53 | ||
|
|
fc55dcd808 | ||
|
|
46bdc8fb26 | ||
|
|
5b71c88e89 | ||
|
|
0adc4a1eed | ||
|
|
dfa015aca8 | ||
|
|
eaf783ad72 | ||
|
|
eb76543229 | ||
|
|
5d0fb1444c | ||
|
|
fd21cbc2f1 | ||
|
|
95985b1940 | ||
|
|
ff7d3c9580 | ||
|
|
f1e5e6f58b | ||
|
|
c4d557f0f0 | ||
|
|
1d5fae16c5 | ||
|
|
afa446da59 | ||
|
|
0f0880bffc | ||
|
|
fdeac9001d | ||
|
|
ef2537be07 | ||
|
|
1ecfac0823 | ||
|
|
5fdb1f05da | ||
|
|
2acbc13a6d | ||
|
|
68da5abfc1 | ||
|
|
09c40aaa74 | ||
|
|
1328902814 | ||
|
|
391bd85d9d | ||
|
|
c212f014ec | ||
|
|
1fbd1f43fe | ||
|
|
9ce8144a50 | ||
|
|
fae02306c8 | ||
|
|
461f49cf6c |
@@ -39,7 +39,7 @@ commands:
|
||||
steps:
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "03:1c:a7:07:35:bc:57:e4:1d:6c:e1:2c:4b:be:09:6d"
|
||||
- "59:4d:99:5e:1c:6d:30:36:6d:60:76:88:ff:a7:ab:63"
|
||||
- run:
|
||||
name: Clone the mobile private repo
|
||||
command: git clone git@github.com:mattermost/mattermost-mobile-private.git ~/mattermost-mobile-private
|
||||
@@ -111,10 +111,7 @@ commands:
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
- run:
|
||||
name: Getting JavaScript dependencies
|
||||
command: |
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
node node_modules/\@sentry/cli/scripts/install.js
|
||||
node node_modules/react-native-webrtc/tools/downloadWebRTC.js
|
||||
command: NODE_ENV=development npm ci --ignore-scripts
|
||||
- save_cache:
|
||||
name: Save npm cache
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
@@ -310,13 +307,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
|
||||
@@ -327,27 +324,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:
|
||||
@@ -359,13 +356,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
|
||||
@@ -376,64 +373,63 @@ 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"
|
||||
env: "SUPPLY_TRACK=beta"
|
||||
# deploy-android-release:
|
||||
# executor:
|
||||
# name: android
|
||||
# resource_class: medium
|
||||
# steps:
|
||||
# - deploy-to-store:
|
||||
# task: "Deploy to Google Play"
|
||||
# target: android
|
||||
# file: "*.apk"
|
||||
|
||||
deploy-android-beta:
|
||||
executor:
|
||||
@@ -446,14 +442,13 @@ jobs:
|
||||
file: "*.apk"
|
||||
env: "SUPPLY_TRACK=alpha"
|
||||
|
||||
deploy-ios-release:
|
||||
executor: ios
|
||||
steps:
|
||||
- deploy-to-store:
|
||||
task: "Deploy to TestFlight"
|
||||
target: ios
|
||||
file: "*.ipa"
|
||||
env: ""
|
||||
# deploy-ios-release:
|
||||
# executor: ios
|
||||
# steps:
|
||||
# - deploy-to-store:
|
||||
# task: "Deploy to TestFlight"
|
||||
# target: ios
|
||||
# file: "*.ipa"
|
||||
|
||||
deploy-ios-beta:
|
||||
executor: ios
|
||||
@@ -464,17 +459,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
|
||||
@@ -486,24 +481,26 @@ workflows:
|
||||
# requires:
|
||||
# - test
|
||||
|
||||
- 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-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-beta:
|
||||
context: mattermost-mobile-android-beta
|
||||
@@ -524,24 +521,26 @@ workflows:
|
||||
- /^build-android-\d+$/
|
||||
- /^build-android-beta-\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-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-beta:
|
||||
context: mattermost-mobile-ios-beta
|
||||
@@ -577,41 +576,43 @@ 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-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
|
||||
# - 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+$/
|
||||
|
||||
# - github-release:
|
||||
# context: mattermost-mobile-unsigned
|
||||
# requires:
|
||||
# - build-android-unsigned
|
||||
# - build-ios-unsigned
|
||||
# filters:
|
||||
# tags:
|
||||
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
# branches:
|
||||
# only: unsigned
|
||||
|
||||
18
.gitignore
vendored
@@ -7,9 +7,6 @@ mattermost.keystore
|
||||
tmp/
|
||||
.env
|
||||
env.d.ts
|
||||
*.apk
|
||||
*.aab
|
||||
*.ipa
|
||||
|
||||
*/**/compass-icons.ttf
|
||||
|
||||
@@ -33,6 +30,8 @@ xcuserdata
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.apk
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
ios/Pods
|
||||
@@ -47,8 +46,6 @@ local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
android/app/bin
|
||||
android/app/build
|
||||
android/build
|
||||
@@ -64,6 +61,12 @@ 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]
|
||||
@@ -99,7 +102,7 @@ coverage
|
||||
mattermost-license.txt
|
||||
*.mattermost-license
|
||||
detox/artifacts
|
||||
detox/detox_pixel_*
|
||||
detox/detox_pixel_4_xl_api_30
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
@@ -111,6 +114,3 @@ launch.json
|
||||
|
||||
# Notice.txt generation
|
||||
!build/notice-file
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
"error": "visit rvm install https://rvm.io/rvm/install",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "bundler",
|
||||
"semver": "2.1.4",
|
||||
"error": "install watchman `gem install bundler --version 2.1.4`",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "pod",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Submit feature requests to https://mattermost.com/suggestions/. File non-security related bugs here in the following format:
|
||||
Submit feature requests to http://www.mattermost.org/feature-requests/. File non-security related bugs here in the following format:
|
||||
|
||||
#### Summary
|
||||
Issue in one concise sentence.
|
||||
|
||||
344
NOTICE.txt
@@ -91,39 +91,6 @@ A spec-compliant polyfill for Intl.RelativeTimeFormat fully tested by the offici
|
||||
|
||||
---
|
||||
|
||||
## @gorhom/bottom-sheet
|
||||
|
||||
This product contains '@gorhom/bottom-sheet' by Mo Gorhom.
|
||||
|
||||
A performant interactive bottom sheet with fully configurable options
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/gorhom/react-native-bottom-sheet
|
||||
|
||||
* LICENSE: MIT License
|
||||
|
||||
Copyright (c) 2020 Mo Gorhom
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @mattermost/compass-icons
|
||||
|
||||
This product contains '@mattermost/compass-icons' by Mattermost.
|
||||
@@ -233,41 +200,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @mattermost/react-native-turbo-mailer
|
||||
|
||||
This product contains '@mattermost/react-native-turbo-mailer' by Avinash Lingaloo.
|
||||
|
||||
An adaptation of react-native-mail that supports Turbo Module
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/mattermost/react-native-turbo-mailer#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Mattermost
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @msgpack/msgpack
|
||||
@@ -361,17 +293,38 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @react-native-camera-roll/camera-roll
|
||||
## @react-native-cameraroll/react-native-cameraroll
|
||||
|
||||
This product contains '@react-native-camera-roll/camera-roll' by Bartol Karuza.
|
||||
This product contains 'react-native-cameraroll' by Bartol Karuza.
|
||||
|
||||
React Native Camera Roll for iOS & Android
|
||||
React-native native module that provides access to the local camera roll or photo library
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-cameraroll/react-native-cameraroll#readme
|
||||
* https://github.com/react-native-cameraroll/react-native-cameraroll
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
@@ -561,21 +514,6 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @react-navigation/stack
|
||||
|
||||
This product contains '@react-navigation/stack'.
|
||||
|
||||
Stack navigator component for iOS and Android with animated transitions and gestures
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://reactnavigation.org/docs/stack-navigator/
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @rudderstack/rudder-sdk-react-native
|
||||
@@ -1192,40 +1130,6 @@ Lightweight fuzzy-search
|
||||
limitations under the License.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## html-entities
|
||||
|
||||
This product contains 'html-entities' by Marat Dulin.
|
||||
|
||||
Fastest HTML entities encode/decode library.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/mdevils/html-entities#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
Copyright (c) 2021 Dulin Marat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## jail-monkey
|
||||
@@ -2128,42 +2032,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-in-app-review
|
||||
|
||||
This product contains 'react-native-in-app-review' by Mina Samir Shafik.
|
||||
|
||||
react native in app review, to rate on Play store, App Store, Generally, the in-app review flow (see figure 1 for play store, figure 2 for ios) can be triggered at any time throughout the user journey of your app. During the flow, the user has the ability
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/MinaSamir11/react-native-in-app-review#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Mina Samir
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-incall-manager
|
||||
@@ -2444,6 +2312,42 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-shadow-2
|
||||
|
||||
This product contains a modified version of 'react-native-shadow-2' by Henrique Bruno Fantauzzi de Almeida.
|
||||
|
||||
Cross-platform shadow for React Native. Supports Android, iOS, Web and Expo.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/SrBrahma/react-native-shadow-2
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Henrique Bruno Fantauzzi de Almeida
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-notifications
|
||||
@@ -2645,42 +2549,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-shadow-2
|
||||
|
||||
This product contains a modified version of 'react-native-shadow-2' by Henrique Bruno Fantauzzi de Almeida.
|
||||
|
||||
Cross-platform shadow for React Native. Supports Android, iOS, Web and Expo.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/SrBrahma/react-native-shadow-2
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Henrique Bruno Fantauzzi de Almeida
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-share
|
||||
@@ -2753,6 +2621,42 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-user-agent
|
||||
|
||||
This product contains 'react-native-user-agent' by Bebnev Anton.
|
||||
|
||||
Library that helps you to get mobile application user agent and web view user agent strings.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/bebnev/react-native-user-agent
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Anton Bebnev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-vector-icons
|
||||
@@ -2824,38 +2728,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-walkthrough-tooltip
|
||||
|
||||
This product contains 'react-native-walkthrough-tooltip' by Jason Gaare.
|
||||
|
||||
React Native Walkthrough Tooltip is a fullscreen modal that highlights whichever element it wraps.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/jasongaare/react-native-walkthrough-tooltip
|
||||
|
||||
* LICENSE: MIT License
|
||||
|
||||
Copyright (c) 2018 Jason Gaare
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
@@ -3027,6 +2899,28 @@ IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
|
||||
---
|
||||
|
||||
## reanimated-bottom-sheet
|
||||
|
||||
This product contains a modified version of 'reanimated-bottom-sheet' by Michał Osadnik.
|
||||
|
||||
Highly configurable component imitating native bottom sheet behavior, with fully native 60 FPS animations!
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/osdnk/react-native-reanimated-bottom-sheet
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
Copyright 2019 – present Michał Osadnik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## semver
|
||||
|
||||
18
README.md
@@ -1,10 +1,12 @@
|
||||
# Mattermost Mobile v2
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (7.1.0+)
|
||||
- **Supported iOS versions:** 12.1+
|
||||
This is a work in progress branch for the next major version of the Mattermost mobile app. Once the work is completed and ready to share, this brach will be set as the default branch in this repository.
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (5.25)
|
||||
- **Supported iOS versions:** 11+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 21 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 14 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
|
||||
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
|
||||
|
||||
@@ -49,9 +51,15 @@ You can leave the Beta testing program at any time:
|
||||
|
||||
App data is wiped from the device when a user logs out of the app. If the user is logged in when the account is deactivated, then within one minute the system logs the user out, and as a result all app data is wiped from the device.
|
||||
|
||||
### I need the code for the v1 version
|
||||
### Can I connect to multiple Mattermost servers using the mobile apps?
|
||||
|
||||
You can still access it! We have moved the code from master to the [v1 branch](https://github.com/mattermost/mattermost-mobile/tree/v1). Be aware that we will not be providing any more v1 versions or updates in the public stores.
|
||||
At the moment, we only support connecting to one server at a time. If you need to connect to multiple servers, please [upvote the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/10975938) so we can track demand for it.
|
||||
|
||||
As a work around, you can install both the released "Mattermost" app and sign up to be a [tester](#testing) for the "Mattermost Beta" app so you can connect to two servers at once.
|
||||
|
||||
### Will there be second generation apps available for tablets?
|
||||
|
||||
We plan to add support for tablets in the future, but the timeline depends on how many people have a need for it. If you're looking for a tablet version, please help us out by [upvoting the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/20082079)!
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
|
||||
65
android/app/BUCK
Normal file
@@ -0,0 +1,65 @@
|
||||
# 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",
|
||||
],
|
||||
)
|
||||
@@ -1,54 +1,88 @@
|
||||
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
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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: []
|
||||
* ]
|
||||
*/
|
||||
|
||||
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"]
|
||||
}
|
||||
project.ext.react = [
|
||||
entryFile: "index.ts",
|
||||
bundleConfig: "metro.config.js",
|
||||
bundleCommand: "bundle",
|
||||
enableHermes: true,
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
|
||||
if (System.getenv("SENTRY_ENABLED") == "true") {
|
||||
@@ -61,35 +95,33 @@ if (System.getenv("SENTRY_ENABLED") == "true") {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
def enableSeparateBuildPerCPUArchitecture = project.hasProperty('separateApk') ? project.property('separateApk').toBoolean() : false
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
* Run Proguard to shrink the Java bytecode in release builds.
|
||||
*/
|
||||
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:+'
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
def reactNativeArchitectures() {
|
||||
def value = project.getProperties().get("reactNativeArchitectures")
|
||||
@@ -99,21 +131,84 @@ 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 459
|
||||
versionName "2.1.0"
|
||||
versionCode 432
|
||||
versionName "2.0.0"
|
||||
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 {
|
||||
@@ -145,7 +240,6 @@ android {
|
||||
release {
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
|
||||
if (useReleaseKey) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
@@ -171,10 +265,10 @@ android {
|
||||
// For each separate APK per architecture, set a unique version code as described here:
|
||||
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
|
||||
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86_64": 4]
|
||||
def abi = output.filters[0]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
versionCodes.get(abi.identifier) * 2000000 + defaultConfig.versionCode
|
||||
versionCodes.get(abi) * 2000000 + defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,62 +281,91 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
//noinspection GradleDynamicVersio
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
||||
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-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||
exclude group:'com.facebook.flipper'
|
||||
}
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
|
||||
if (enableHermes) {
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation("com.facebook.react:hermes-engine:+") { // From node_modules
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
|
||||
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.6.1'
|
||||
implementation 'com.google.android.material:material:1.8.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
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')
|
||||
|
||||
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: '18.1.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-tasks') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.0.2'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-stats') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '17.0.3'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-basement') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.1.0'
|
||||
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.10.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.2'
|
||||
}
|
||||
if (details.requested.name == 'okhttp-tls') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.2'
|
||||
}
|
||||
if (details.requested.name == 'okhttp-urlconnection') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.2'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,3 +380,11 @@ 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"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.wix.detox.Detox;
|
||||
import com.wix.detox.config.DetoxConfig;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -20,11 +19,10 @@ public class DetoxTest {
|
||||
|
||||
@Test
|
||||
public void runDetoxTests() {
|
||||
DetoxConfig detoxConfig = new DetoxConfig();
|
||||
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
|
||||
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
|
||||
detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60);
|
||||
Detox.DetoxIdlePolicyConfig idlePolicyConfig = new Detox.DetoxIdlePolicyConfig();
|
||||
idlePolicyConfig.masterTimeoutSec = 60;
|
||||
idlePolicyConfig.idleResourceTimeoutSec = 30;
|
||||
|
||||
Detox.runTests(mActivityRule, detoxConfig);
|
||||
Detox.runTests(mActivityRule, idlePolicyConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.mattermost.flipper;
|
||||
package com.rn;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
@@ -17,29 +17,30 @@ 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 com.mattermost.networkclient.RCTOkHttpClientFactory;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import okhttp3.OkHttpClient;
|
||||
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());
|
||||
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
|
||||
RCTOkHttpClientFactory.Companion.setFlipperPlugin(networkFlipperPlugin);
|
||||
NetworkingModule.setCustomClientBuilder(
|
||||
builder -> builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)));
|
||||
new NetworkingModule.CustomClientBuilder() {
|
||||
@Override
|
||||
public void apply(OkHttpClient.Builder builder) {
|
||||
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
|
||||
}
|
||||
});
|
||||
client.addPlugin(networkFlipperPlugin);
|
||||
client.start();
|
||||
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
|
||||
@@ -52,7 +53,12 @@ public class ReactNativeFlipper {
|
||||
public void onReactContextInitialized(ReactContext reactContext) {
|
||||
reactInstanceManager.removeReactInstanceEventListener(this);
|
||||
reactContext.runOnNativeModulesQueueThread(
|
||||
() -> client.addPlugin(new FrescoFlipperPlugin()));
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -1,10 +1,10 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.mattermost.rnbeta">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
@@ -12,7 +12,6 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"
|
||||
@@ -84,23 +83,5 @@
|
||||
android:resizeableActivity="true"
|
||||
android:exported="true"
|
||||
/>
|
||||
<activity
|
||||
android:name="com.mattermost.share.ShareActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme"
|
||||
android:taskAffinity="com.mattermost.share"
|
||||
android:exported="true"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<!-- for sharing-->
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.mattermost.helpers
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.util.LruCache
|
||||
|
||||
class BitmapCache {
|
||||
private var memoryCache: LruCache<String, Bitmap>
|
||||
private var keysCache: LruCache<String, String>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
keysCache = LruCache<String, String>(50)
|
||||
}
|
||||
|
||||
fun bitmap(userId: String, updatedAt: Double, serverUrl: String): Bitmap? {
|
||||
val key = "$serverUrl-$userId-$updatedAt"
|
||||
return memoryCache.get(key)
|
||||
}
|
||||
|
||||
fun insertBitmap(bitmap: Bitmap?, userId: String, updatedAt: Double, serverUrl: String) {
|
||||
if (bitmap == null) {
|
||||
removeBitmap(userId, serverUrl)
|
||||
}
|
||||
val key = "$serverUrl-$userId-$updatedAt"
|
||||
val cachedKey = "$serverUrl-$userId"
|
||||
keysCache.put(cachedKey, key)
|
||||
memoryCache.put(key, bitmap)
|
||||
}
|
||||
|
||||
fun removeBitmap(userId: String, serverUrl: String) {
|
||||
val cachedKey = "$serverUrl-$userId"
|
||||
val key = keysCache.get(cachedKey)
|
||||
if (key != null) {
|
||||
memoryCache.remove(key)
|
||||
keysCache.remove(cachedKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllBitmaps() {
|
||||
memoryCache.evictAll()
|
||||
keysCache.evictAll()
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public class Credentials {
|
||||
String service = map.getString("service");
|
||||
assert service != null;
|
||||
if (service.isEmpty()) {
|
||||
String[] credentials = token[0].split(", *");
|
||||
String[] credentials = token[0].split(",[ ]*");
|
||||
if (credentials.length == 2) {
|
||||
token[0] = credentials[0];
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -21,15 +22,14 @@ import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
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 com.nozbe.watermelondb.Database;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
@@ -39,9 +39,6 @@ import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
import static com.mattermost.helpers.database_extension.GeneralKt.getDatabaseForServer;
|
||||
import static com.mattermost.helpers.database_extension.UserKt.getLastPictureUpdate;
|
||||
|
||||
public class CustomPushNotificationHelper {
|
||||
public static final String CHANNEL_HIGH_IMPORTANCE_ID = "channel_01";
|
||||
public static final String CHANNEL_MIN_IMPORTANCE_ID = "channel_02";
|
||||
@@ -56,16 +53,11 @@ public class CustomPushNotificationHelper {
|
||||
private static NotificationChannel mHighImportanceChannel;
|
||||
private static NotificationChannel mMinImportanceChannel;
|
||||
|
||||
private static final OkHttpClient client = new OkHttpClient();
|
||||
|
||||
private static final BitmapCache bitmapCache = new BitmapCache();
|
||||
|
||||
private static void addMessagingStyleMessages(Context context, 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";
|
||||
}
|
||||
@@ -82,10 +74,7 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, senderId))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -128,7 +117,6 @@ public class CustomPushNotificationHelper {
|
||||
notification.addExtras(userInfoBundle);
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
private static void addNotificationReplyAction(Context context, NotificationCompat.Builder notification, Bundle bundle, int notificationId) {
|
||||
String postId = bundle.getString("post_id");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
@@ -190,7 +178,7 @@ public class CustomPushNotificationHelper {
|
||||
setNotificationGroup(notification, groupId, createSummary);
|
||||
setNotificationBadgeType(notification);
|
||||
|
||||
setNotificationChannel(context, notification);
|
||||
setNotificationChannel(notification, bundle);
|
||||
setNotificationDeleteIntent(context, notification, bundle, notificationId);
|
||||
addNotificationReplyAction(context, notification, bundle, notificationId);
|
||||
|
||||
@@ -262,12 +250,19 @@ 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) {
|
||||
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)
|
||||
@@ -275,10 +270,7 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, serverUrl, "me", urlOverride);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, "me"))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -315,6 +307,25 @@ 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) {
|
||||
@@ -346,15 +357,12 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static void setNotificationChannel(Context context, NotificationCompat.Builder notification) {
|
||||
private static void setNotificationChannel(NotificationCompat.Builder notification, Bundle bundle) {
|
||||
// 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());
|
||||
}
|
||||
@@ -385,72 +393,44 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
|
||||
private static void setNotificationIcons(Context context, NotificationCompat.Builder notification, Bundle bundle) {
|
||||
String smallIcon = bundle.getString("smallIcon");
|
||||
String channelName = getConversationTitle(bundle);
|
||||
String senderName = bundle.getString("sender_name");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
String urlOverride = bundle.getString("override_icon_url");
|
||||
|
||||
notification.setSmallIcon(R.mipmap.ic_notification);
|
||||
int smallIconResId = getSmallIconResourceId(context, smallIcon);
|
||||
notification.setSmallIcon(smallIconResId);
|
||||
|
||||
if (serverUrl != null && channelName.equals(senderName)) {
|
||||
try {
|
||||
String senderId = bundle.getString("sender_id");
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
|
||||
if (avatar != null) {
|
||||
notification.setLargeIcon(avatar);
|
||||
}
|
||||
notification.setLargeIcon(userAvatar(context, serverUrl, senderId));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap userAvatar(final Context context, @NonNull final String serverUrl, final String userId, final String urlOverride) throws IOException {
|
||||
try {
|
||||
Response response;
|
||||
Double lastUpdateAt = 0.0;
|
||||
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 {
|
||||
DatabaseHelper dbHelper = DatabaseHelper.Companion.getInstance();
|
||||
if (dbHelper != null) {
|
||||
Database db = getDatabaseForServer(dbHelper, context, serverUrl);
|
||||
if (db != null) {
|
||||
lastUpdateAt = getLastPictureUpdate(db, userId);
|
||||
if (lastUpdateAt == null) {
|
||||
lastUpdateAt = 0.0;
|
||||
}
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
Bitmap cached = bitmapCache.bitmap(userId, lastUpdateAt, serverUrl);
|
||||
if (cached != null) {
|
||||
Bitmap bitmap = cached.copy(cached.getConfig(), false);
|
||||
return getCircleBitmap(bitmap);
|
||||
}
|
||||
private static Bitmap userAvatar(Context context, final String serverUrl, final String userId) throws IOException {
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
|
||||
|
||||
bitmapCache.removeBitmap(userId, serverUrl);
|
||||
String url = String.format("api/v4/users/%s/image", userId);
|
||||
Log.i("ReactNative", String.format("Fetch profile image %s", url));
|
||||
response = Network.getSync(serverUrl, url, null);
|
||||
}
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
final String url = String.format("%s/api/v4/users/%s/image", serverUrl, userId);
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", String.format("Bearer %s", token))
|
||||
.url(url)
|
||||
.build();
|
||||
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 (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.insertBitmap(bitmap.copy(bitmap.getConfig(), false), userId, lastUpdateAt, serverUrl);
|
||||
}
|
||||
return getCircleBitmap(bitmap);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
Log.i("ReactNative", String.format("Fetch profile %s", url));
|
||||
return getCircleBitmap(bitmap);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,30 +2,31 @@ package com.mattermost.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
import java.lang.Exception
|
||||
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.lang.Exception
|
||||
import java.util.*
|
||||
|
||||
class DatabaseHelper {
|
||||
var defaultDatabase: Database? = null
|
||||
private var defaultDatabase: Database? = null
|
||||
|
||||
val onlyServerUrl: String?
|
||||
get() {
|
||||
try {
|
||||
val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''"
|
||||
defaultDatabase!!.rawQuery(query).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
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
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -36,13 +37,624 @@ 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
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun find(db: Database, tableName: String, id: String?): ReadableMap? {
|
||||
val args: Array<Any?> = arrayOf(id)
|
||||
try {
|
||||
db.rawQuery("select * from $tableName where id == ? limit 1", args).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return null
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
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!!)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryIds(db: Database, tableName: String, ids: Array<String>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(ids).map { "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("select distinct id from $tableName where id IN ($args)", ids as Array<Any?>).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex("id")
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun queryByColumn(db: Database, tableName: String, columnName: String, values: Array<Any?>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(values).map { "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("select distinct $columnName from $tableName where $columnName IN ($args)", values).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex(columnName)
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun queryCurrentUserId(db: Database): String? {
|
||||
val result = find(db, "System", "currentUserId")!!
|
||||
return result.getString("value")
|
||||
}
|
||||
|
||||
private fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
|
||||
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
|
||||
if (cursor1.count == 1) {
|
||||
cursor1.moveToFirst()
|
||||
val earliest = cursor1.getDouble(0)
|
||||
val latest = cursor1.getDouble(1)
|
||||
cursor1.close()
|
||||
val postQuery = "SELECT create_at FROM POST WHERE channel_id= ? AND delete_at=0 AND create_at BETWEEN ? AND ? ORDER BY create_at DESC"
|
||||
val cursor2 = db.rawQuery(postQuery, arrayOf(channelId, earliest, latest))
|
||||
if (cursor2.count >= 60) {
|
||||
cursor2.moveToFirst()
|
||||
val createAt = cursor2.getDouble(0)
|
||||
cursor2.close()
|
||||
return createAt
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return lastFetchedAt
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun handlePosts(db: Database, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
|
||||
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
|
||||
if (postsData != null) {
|
||||
val ordered = postsData.getArray("order")?.toArrayList()
|
||||
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
|
||||
val previousPostId = postsData.getString("prev_post_id")
|
||||
val postsInThread = hashMapOf<String, List<JSONObject>>()
|
||||
val postList = posts.toList()
|
||||
var earliest = 0.0
|
||||
var latest = 0.0
|
||||
var lastFetchedAt = 0.0
|
||||
|
||||
if (ordered != null && posts.isNotEmpty()) {
|
||||
val firstId = ordered.first()
|
||||
val lastId = ordered.last()
|
||||
lastFetchedAt = postList.fold(0.0) { acc, next ->
|
||||
val post = next.second as Map<*, *>
|
||||
val createAt = post["create_at"] as Double
|
||||
val updateAt = post["update_at"] as Double
|
||||
val deleteAt = post["delete_at"] as Double
|
||||
val value = maxOf(createAt, updateAt, deleteAt)
|
||||
|
||||
maxOf(value, acc)
|
||||
}
|
||||
var prevPostId = ""
|
||||
|
||||
val sortedPosts = postList.sortedBy { (_, value) ->
|
||||
((value as Map<*, *>)["create_at"] as Double)
|
||||
}
|
||||
|
||||
sortedPosts.forEachIndexed { index, it ->
|
||||
val key = it.first
|
||||
if (it.second != null) {
|
||||
val post = it.second as MutableMap<String, Any?>
|
||||
|
||||
if (index == 0) {
|
||||
post.putIfAbsent("prev_post_id", previousPostId)
|
||||
} else if (prevPostId.isNotEmpty()) {
|
||||
post.putIfAbsent("prev_post_id", prevPostId)
|
||||
}
|
||||
|
||||
if (lastId == key) {
|
||||
earliest = post["create_at"] as Double
|
||||
}
|
||||
if (firstId == key) {
|
||||
latest = post["create_at"] as Double
|
||||
}
|
||||
|
||||
val jsonPost = JSONObject(post)
|
||||
val rootId = post["root_id"] as? String
|
||||
|
||||
if (!rootId.isNullOrEmpty()) {
|
||||
var thread = postsInThread[rootId]?.toMutableList()
|
||||
if (thread == null) {
|
||||
thread = mutableListOf()
|
||||
}
|
||||
|
||||
thread.add(jsonPost)
|
||||
postsInThread[rootId] = thread.toList()
|
||||
}
|
||||
|
||||
if (find(db, "Post", key) == null) {
|
||||
insertPost(db, jsonPost)
|
||||
} else {
|
||||
updatePost(db, jsonPost)
|
||||
}
|
||||
|
||||
if (ordered.contains(key)) {
|
||||
prevPostId = key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!receivingThreads) {
|
||||
handlePostsInChannel(db, channelId, earliest, latest)
|
||||
updateMyChannelLastFetchedAt(db, channelId, lastFetchedAt)
|
||||
}
|
||||
handlePostsInThread(db, postsInThread)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleThreads(db: Database, threads: ReadableArray) {
|
||||
for (i in 0 until threads.size()) {
|
||||
val thread = threads.getMap(i)
|
||||
val threadId = thread.getString("id")
|
||||
|
||||
// Insert/Update the thread
|
||||
val existingRecord = find(db, "Thread", threadId)
|
||||
if (existingRecord == null) {
|
||||
insertThread(db, thread)
|
||||
} else {
|
||||
updateThread(db, thread, existingRecord)
|
||||
}
|
||||
|
||||
// Delete existing and insert thread participants
|
||||
val participants = thread.getArray("participants")
|
||||
if (participants != null) {
|
||||
db.execute("delete from ThreadParticipant where thread_id = ?", arrayOf(threadId))
|
||||
|
||||
if (participants.size() > 0) {
|
||||
insertThreadParticipants(db, threadId!!, participants)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUsers(db: Database, users: ReadableArray) {
|
||||
for (i in 0 until users.size()) {
|
||||
val user = users.getMap(i)
|
||||
val roles = user.getString("roles") ?: ""
|
||||
val isBot = try {
|
||||
user.getBoolean("is_bot")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
}
|
||||
|
||||
val lastPictureUpdate = try { user.getDouble("last_picture_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
|
||||
db.execute(
|
||||
"insert into User (id, auth_service, update_at, delete_at, email, first_name, is_bot, is_guest, " +
|
||||
"last_name, last_picture_update, locale, nickname, position, roles, status, username, notify_props, " +
|
||||
"props, timezone, _status) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
user.getString("id"),
|
||||
user.getString("auth_service"),
|
||||
user.getDouble("update_at"),
|
||||
user.getDouble("delete_at"),
|
||||
user.getString("email"),
|
||||
user.getString("first_name"),
|
||||
isBot,
|
||||
roles.contains("system_guest"),
|
||||
user.getString("last_name"),
|
||||
lastPictureUpdate,
|
||||
user.getString("locale"),
|
||||
user.getString("nickname"),
|
||||
user.getString("position"),
|
||||
roles,
|
||||
"",
|
||||
user.getString("username"),
|
||||
"{}",
|
||||
ReadableMapUtils.toJSONObject(user.getMap("props") ?: Arguments.createMap()).toString(),
|
||||
ReadableMapUtils.toJSONObject(user.getMap("timezone") ?: Arguments.createMap()).toString(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDefaultDatabase(context: Context) {
|
||||
val databaseName = "app.db"
|
||||
val databasePath = Uri.fromFile(context.filesDir).toString() + "/" + databaseName
|
||||
defaultDatabase = Database(databasePath, context)
|
||||
}
|
||||
|
||||
internal fun JSONObject.toMap(): Map<String, Any?> = keys().asSequence().associateWith { it ->
|
||||
private fun insertPost(db: Database, post: JSONObject) {
|
||||
var metadata: JSONObject?
|
||||
var reactions: JSONArray? = null
|
||||
var customEmojis: JSONArray? = null
|
||||
var files: JSONArray? = null
|
||||
|
||||
try {
|
||||
metadata = post.getJSONObject("metadata")
|
||||
reactions = metadata.remove("reactions") as JSONArray?
|
||||
customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
files = metadata.remove("files") as JSONArray?
|
||||
} catch (e: Exception) {
|
||||
// no metadata found
|
||||
metadata = JSONObject()
|
||||
}
|
||||
|
||||
db.execute(
|
||||
"insert into Post " +
|
||||
"(id, channel_id, create_at, delete_at, update_at, edit_at, is_pinned, message, metadata, original_id, pending_post_id, " +
|
||||
"previous_post_id, root_id, type, user_id, props, _status)" +
|
||||
" values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
post.getString("id"),
|
||||
post.getString("channel_id"),
|
||||
post.getDouble("create_at"),
|
||||
post.getDouble("delete_at"),
|
||||
post.getDouble("update_at"),
|
||||
post.getDouble("edit_at"),
|
||||
post.getBoolean("is_pinned"),
|
||||
post.getString("message"),
|
||||
metadata.toString(),
|
||||
post.getString("original_id"),
|
||||
post.getString("pending_post_id"),
|
||||
post.getString("prev_post_id"),
|
||||
post.getString("root_id"),
|
||||
post.getString("type"),
|
||||
post.getString("user_id"),
|
||||
post.getJSONObject("props").toString()
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
|
||||
if (files != null && files.length() > 0) {
|
||||
insertFiles(db, files)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePost(db: Database, post: JSONObject) {
|
||||
var metadata: JSONObject?
|
||||
var reactions: JSONArray? = null
|
||||
var customEmojis: JSONArray? = null
|
||||
|
||||
try {
|
||||
metadata = post.getJSONObject("metadata")
|
||||
reactions = metadata.remove("reactions") as JSONArray?
|
||||
customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
metadata.remove("files")
|
||||
} catch (e: Exception) {
|
||||
// no metadata found
|
||||
metadata = JSONObject()
|
||||
}
|
||||
|
||||
db.execute(
|
||||
"update Post SET channel_id = ?, create_at = ?, delete_at = ?, update_at =?, edit_at =?, " +
|
||||
"is_pinned = ?, message = ?, metadata = ?, original_id = ?, pending_post_id = ?, previous_post_id = ?, " +
|
||||
"root_id = ?, type = ?, user_id = ?, props = ?, _status = 'updated' " +
|
||||
"where id = ?",
|
||||
arrayOf(
|
||||
post.getString("channel_id"),
|
||||
post.getDouble("create_at"),
|
||||
post.getDouble("delete_at"),
|
||||
post.getDouble("update_at"),
|
||||
post.getDouble("edit_at"),
|
||||
post.getBoolean("is_pinned"),
|
||||
post.getString("message"),
|
||||
metadata.toString(),
|
||||
post.getString("original_id"),
|
||||
post.getString("pending_post_id"),
|
||||
post.getString("prev_post_id"),
|
||||
post.getString("root_id"),
|
||||
post.getString("type"),
|
||||
post.getString("user_id"),
|
||||
post.getJSONObject("props").toString(),
|
||||
post.getString("id"),
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
db.execute("delete from Reaction where post_id = ?", arrayOf(post.getString("id")))
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertThread(db: Database, thread: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { false }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"insert into Thread " +
|
||||
"(id, last_reply_at, last_fetched_at, last_viewed_at, reply_count, is_following, unread_replies, unread_mentions, _status)" +
|
||||
" values (?, ?, 0, ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
thread.getString("id"),
|
||||
lastReplyAt,
|
||||
lastViewedAt,
|
||||
replyCount,
|
||||
isFollowing,
|
||||
unreadReplies,
|
||||
unreadMentions
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateThread(db: Database, thread: ReadableMap, existingRecord: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { existingRecord.getInt("is_following") == 1 }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"update Thread SET last_reply_at = ?, last_viewed_at = ?, reply_count = ?, is_following = ?, unread_replies = ?, unread_mentions = ?, _status = 'updated' where id = ?",
|
||||
arrayOf(
|
||||
lastReplyAt,
|
||||
lastViewedAt,
|
||||
replyCount,
|
||||
isFollowing,
|
||||
unreadReplies,
|
||||
unreadMentions,
|
||||
thread.getString("id")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun insertThreadParticipants(db: Database, threadId: String, participants: ReadableArray) {
|
||||
for (i in 0 until participants.size()) {
|
||||
val participant = participants.getMap(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"insert into ThreadParticipant " +
|
||||
"(id, thread_id, user_id, _status)" +
|
||||
" values (?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
id,
|
||||
threadId,
|
||||
participant.getString("id")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertCustomEmojis(db: Database, customEmojis: JSONArray) {
|
||||
for (i in 0 until customEmojis.length()) {
|
||||
val emoji = customEmojis.getJSONObject(i)
|
||||
if(find(db, "CustomEmoji", emoji.getString("id")) == null) {
|
||||
db.execute(
|
||||
"insert into CustomEmoji (id, name, _status) values (?, ?, 'created')",
|
||||
arrayOf(
|
||||
emoji.getString("id"),
|
||||
emoji.getString("name"),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertFiles(db: Database, files: JSONArray) {
|
||||
for (i in 0 until files.length()) {
|
||||
val file = files.getJSONObject(i)
|
||||
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" }
|
||||
val height = try { file.getInt("height") } catch (e: JSONException) { 0 }
|
||||
val width = try { file.getInt("width") } catch (e: JSONException) { 0 }
|
||||
db.execute(
|
||||
"insert into File (id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _status) " +
|
||||
"values (?, ?, ?, ?, '', ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
file.getString("id"),
|
||||
file.getString("extension"),
|
||||
height,
|
||||
miniPreview,
|
||||
file.getString("mime_type"),
|
||||
file.getString("name"),
|
||||
file.getString("post_id"),
|
||||
file.getDouble("size"),
|
||||
width
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertReactions(db: Database, reactions: JSONArray) {
|
||||
for (i in 0 until reactions.length()) {
|
||||
val reaction = reactions.getJSONObject(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"insert into Reaction (id, create_at, emoji_name, post_id, user_id, _status) " +
|
||||
"values (?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
id,
|
||||
reaction.getDouble("create_at"),
|
||||
reaction.getString("emoji_name"),
|
||||
reaction.getString("post_id"),
|
||||
reaction.getString("user_id")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePostsInChannel(db: Database, channelId: String, earliest: Double, latest: Double) {
|
||||
db.rawQuery("select id, channel_id, earliest, latest from PostsInChannel where channel_id = ?", arrayOf(channelId)).use { cursor ->
|
||||
if (cursor.count == 0) {
|
||||
// create new post in channel
|
||||
insertPostInChannel(db, channelId, earliest, latest)
|
||||
return
|
||||
}
|
||||
|
||||
val resultArray = Arguments.createArray()
|
||||
while (cursor.moveToNext()) {
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
resultArray.pushMap(cursorMap)
|
||||
}
|
||||
|
||||
val chunk = findPostInChannel(resultArray, earliest, latest)
|
||||
if (chunk != null) {
|
||||
db.execute(
|
||||
"update PostsInChannel set earliest = ?, latest = ?, _status = 'updated' where id = ?",
|
||||
arrayOf(
|
||||
minOf(earliest, chunk.getDouble("earliest")),
|
||||
maxOf(latest, chunk.getDouble("latest")),
|
||||
chunk.getString("id")
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val newChunk = insertPostInChannel(db, channelId, earliest, latest)
|
||||
mergePostsInChannel(db, resultArray, newChunk)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMyChannelLastFetchedAt(db: Database, channelId: String, lastFetchedAt: Double) {
|
||||
db.execute(
|
||||
"UPDATE MyChannel SET last_fetched_at = ?, _status = 'updated' WHERE id = ?",
|
||||
arrayOf(
|
||||
lastFetchedAt,
|
||||
channelId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? {
|
||||
for (i in 0 until chunks.size()) {
|
||||
val chunk = chunks.getMap(i)
|
||||
if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) {
|
||||
return chunk
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap {
|
||||
val id = RandomId.generate()
|
||||
db.execute("insert into PostsInChannel (id, channel_id, earliest, latest, _status) values (?, ?, ?, ?, 'created')",
|
||||
arrayOf(id, channelId, earliest, latest))
|
||||
|
||||
val map = Arguments.createMap()
|
||||
map.putString("id", id)
|
||||
map.putString("channel_id", channelId)
|
||||
map.putDouble("earliest", earliest)
|
||||
map.putDouble("latest", latest)
|
||||
return map
|
||||
}
|
||||
|
||||
private fun mergePostsInChannel(db: Database, existingChunks: ReadableArray, newChunk: ReadableMap) {
|
||||
for (i in 0 until existingChunks.size()) {
|
||||
val chunk = existingChunks.getMap(i)
|
||||
if (newChunk.getDouble("earliest") <= chunk.getDouble("earliest") &&
|
||||
newChunk.getDouble("latest") >= chunk.getDouble("latest")) {
|
||||
db.execute("delete from PostsInChannel where id = ?", arrayOf(chunk.getString("id")))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePostsInThread(db: Database, postsInThread: Map<String, List<JSONObject>>) {
|
||||
postsInThread.forEach { (key, list) ->
|
||||
val sorted = list.sortedBy { it.getDouble("create_at") }
|
||||
val earliest = sorted.first().getDouble("create_at")
|
||||
val latest = sorted.last().getDouble("create_at")
|
||||
db.rawQuery("select * from PostsInThread where root_id = ? order by latest desc", arrayOf(key)).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
cursor.moveToFirst()
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
db.execute(
|
||||
"update PostsInThread set earliest = ?, latest = ?, _status = 'updated' where id = ?",
|
||||
arrayOf(
|
||||
minOf(earliest, cursorMap.getDouble("earliest")),
|
||||
maxOf(latest, cursorMap.getDouble("latest")),
|
||||
key
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"insert into PostsInThread (id, root_id, earliest, latest, _status) " +
|
||||
"values (?, ?, ?, ?, 'created')",
|
||||
arrayOf(id, key, earliest, latest)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.toMap(): Map<String, *> = keys().asSequence().associateWith { it ->
|
||||
when (val value = this[it])
|
||||
{
|
||||
is JSONArray ->
|
||||
@@ -50,15 +662,9 @@ class DatabaseHelper {
|
||||
val map = (0 until value.length()).associate { Pair(it.toString(), value[it]) }
|
||||
JSONObject(map).toMap().values.toList()
|
||||
}
|
||||
is JSONObject -> {
|
||||
value.toMap()
|
||||
}
|
||||
JSONObject.NULL -> {
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
value
|
||||
}
|
||||
is JSONObject -> value.toMap()
|
||||
JSONObject.NULL -> null
|
||||
else -> value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import com.mattermost.networkclient.APIClientModule;
|
||||
import com.mattermost.networkclient.enums.RetryTypes;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Response;
|
||||
|
||||
|
||||
public class Network {
|
||||
@@ -36,16 +35,6 @@ 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");
|
||||
|
||||
@@ -145,9 +145,9 @@ public class NotificationHelper {
|
||||
for (final StatusBarNotification status : statusNotifications) {
|
||||
Bundle bundle = status.getNotification().extras;
|
||||
if (isThreadNotification) {
|
||||
hasMore = bundle.containsKey("root_id") && bundle.getString("root_id").equals(rootId);
|
||||
hasMore = bundle.getString("root_id").equals(rootId);
|
||||
} else {
|
||||
hasMore = bundle.containsKey("channel_id") && bundle.getString("channel_id").equals(channelId);
|
||||
hasMore = bundle.getString("channel_id").equals(channelId);
|
||||
}
|
||||
if (hasMore) break;
|
||||
}
|
||||
|
||||
@@ -4,151 +4,292 @@ import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import com.facebook.react.bridge.Arguments
|
||||
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
|
||||
import com.mattermost.helpers.database_extension.*
|
||||
import com.mattermost.helpers.push_notification.*
|
||||
|
||||
import com.facebook.react.bridge.WritableNativeArray
|
||||
import com.nozbe.watermelondb.Database
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class PushNotificationDataHelper(private val context: Context) {
|
||||
private var coroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
fun fetchAndStoreDataForPushNotification(initialData: Bundle, isReactInit: Boolean): Bundle? {
|
||||
var result: Bundle? = null
|
||||
val job = coroutineScope.launch(Dispatchers.Default) {
|
||||
result = PushNotificationDataRunnable.start(context, initialData, isReactInit)
|
||||
}
|
||||
runBlocking {
|
||||
job.join()
|
||||
}
|
||||
|
||||
return result
|
||||
private var scope = Executors.newSingleThreadExecutor()
|
||||
fun fetchAndStoreDataForPushNotification(initialData: Bundle) {
|
||||
scope.execute(Runnable {
|
||||
runBlocking {
|
||||
PushNotificationDataRunnable.start(context, initialData)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class PushNotificationDataRunnable {
|
||||
companion object {
|
||||
internal val specialMentions = listOf("all", "here", "channel")
|
||||
private val dbHelper = DatabaseHelper.instance!!
|
||||
private val mutex = Mutex()
|
||||
private val specialMentions = listOf("all", "here", "channel")
|
||||
|
||||
suspend fun start(context: Context, initialData: Bundle, isReactInit: Boolean): Bundle? {
|
||||
// for more info see: https://blog.danlew.net/2020/01/28/coroutines-and-java-synchronization-dont-mix/
|
||||
mutex.withLock {
|
||||
val serverUrl: String = initialData.getString("server_url") ?: return null
|
||||
val db = dbHelper.getDatabaseForServer(context, serverUrl)
|
||||
var result: Bundle? = null
|
||||
@Synchronized
|
||||
suspend fun start(context: Context, initialData: Bundle) {
|
||||
try {
|
||||
val serverUrl: String = initialData.getString("server_url") ?: return
|
||||
val channelId = initialData.getString("channel_id")
|
||||
val rootId = initialData.getString("root_id")
|
||||
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
|
||||
val db = DatabaseHelper.instance!!.getDatabaseForServer(context, serverUrl)
|
||||
Log.i("ReactNative", "Start fetching notification data in server="+serverUrl+" for channel="+channelId)
|
||||
|
||||
try {
|
||||
if (db != null) {
|
||||
val teamId = initialData.getString("team_id")
|
||||
val channelId = initialData.getString("channel_id")
|
||||
val postId = initialData.getString("post_id")
|
||||
val rootId = initialData.getString("root_id")
|
||||
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
|
||||
if (db != null) {
|
||||
var postData: ReadableMap?
|
||||
var posts: ReadableMap? = null
|
||||
var userIdsToLoad: ReadableArray? = null
|
||||
var usernamesToLoad: ReadableArray? = null
|
||||
|
||||
Log.i("ReactNative", "Start fetching notification data in server=$serverUrl for channel=$channelId")
|
||||
var threads: ReadableArray? = null
|
||||
var usersFromThreads: ReadableArray? = null
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val notificationData = Arguments.createMap()
|
||||
coroutineScope {
|
||||
if (channelId != null) {
|
||||
postData = fetchPosts(db, serverUrl, channelId, isCRTEnabled, rootId)
|
||||
|
||||
if (!teamId.isNullOrEmpty()) {
|
||||
val res = fetchTeamIfNeeded(db, serverUrl, teamId)
|
||||
res.first?.let { notificationData.putMap("team", it) }
|
||||
res.second?.let { notificationData.putMap("myTeam", it) }
|
||||
}
|
||||
posts = postData?.getMap("posts")
|
||||
userIdsToLoad = postData?.getArray("userIdsToLoad")
|
||||
usernamesToLoad = postData?.getArray("usernamesToLoad")
|
||||
threads = postData?.getArray("threads")
|
||||
usersFromThreads = postData?.getArray("usersFromThreads")
|
||||
|
||||
if (channelId != null && postId != null) {
|
||||
val channelRes = fetchMyChannel(db, serverUrl, channelId, isCRTEnabled)
|
||||
channelRes.first?.let { notificationData.putMap("channel", it) }
|
||||
channelRes.second?.let { notificationData.putMap("myChannel", it) }
|
||||
val loadedProfiles = channelRes.third
|
||||
|
||||
// Fetch categories if needed
|
||||
if (!teamId.isNullOrEmpty() && notificationData.getMap("myTeam") != null) {
|
||||
// should load all categories
|
||||
val res = fetchMyTeamCategories(db, serverUrl, teamId)
|
||||
res?.let { notificationData.putMap("categories", it) }
|
||||
} else if (notificationData.getMap("channel") != null) {
|
||||
// check if the channel is in the category for the team
|
||||
val res = addToDefaultCategoryIfNeeded(db, notificationData.getMap("channel")!!)
|
||||
res?.let { notificationData.putArray("categoryChannels", it) }
|
||||
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
|
||||
val users = fetchUsersById(serverUrl, userIdsToLoad!!)
|
||||
userIdsToLoad = users?.getArray("data")
|
||||
}
|
||||
|
||||
val postData = fetchPosts(db, serverUrl, channelId, isCRTEnabled, rootId, loadedProfiles)
|
||||
postData?.getMap("posts")?.let { notificationData.putMap("posts", it) }
|
||||
|
||||
var notificationThread: ReadableMap? = null
|
||||
if (isCRTEnabled && !rootId.isNullOrEmpty()) {
|
||||
notificationThread = fetchThread(db, serverUrl, rootId, teamId)
|
||||
if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) {
|
||||
val users = fetchUsersByUsernames(serverUrl, usernamesToLoad!!)
|
||||
usernamesToLoad = users?.getArray("data")
|
||||
}
|
||||
|
||||
getThreadList(notificationThread, postData?.getArray("threads"))?.let {
|
||||
val threadsArray = Arguments.createArray()
|
||||
for(item in it) {
|
||||
threadsArray.pushMap(item)
|
||||
}
|
||||
notificationData.putArray("threads", threadsArray)
|
||||
}
|
||||
|
||||
val userList = fetchNeededUsers(serverUrl, loadedProfiles, postData)
|
||||
notificationData.putArray("users", ReadableArrayUtils.toWritableArray(userList.toArray()))
|
||||
}
|
||||
|
||||
result = Arguments.toBundle(notificationData)
|
||||
|
||||
if (!isReactInit) {
|
||||
dbHelper.saveToDatabase(db, notificationData, teamId, channelId, receivingThreads)
|
||||
}
|
||||
|
||||
Log.i("ReactNative", "Done processing push notification=$serverUrl for channel=$channelId")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
db?.close()
|
||||
Log.i("ReactNative", "DONE fetching notification data")
|
||||
}
|
||||
|
||||
return result
|
||||
db.transaction {
|
||||
if (posts != null && channelId != null) {
|
||||
DatabaseHelper.instance!!.handlePosts(db, posts!!.getMap("data"), channelId, receivingThreads)
|
||||
}
|
||||
|
||||
if (threads != null) {
|
||||
DatabaseHelper.instance!!.handleThreads(db, threads!!)
|
||||
}
|
||||
|
||||
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, userIdsToLoad!!)
|
||||
}
|
||||
|
||||
if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, usernamesToLoad!!)
|
||||
}
|
||||
|
||||
if (usersFromThreads != null) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, usersFromThreads!!)
|
||||
}
|
||||
}
|
||||
|
||||
db.close()
|
||||
Log.i("ReactNative", "Done processing push notification="+serverUrl+" for channel="+channelId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThreadList(notificationThread: ReadableMap?, threads: ReadableArray?): ArrayList<ReadableMap>? {
|
||||
threads?.let {
|
||||
val threadsArray = ArrayList<ReadableMap>()
|
||||
val threadIds = ArrayList<String>()
|
||||
notificationThread?.let { thread ->
|
||||
thread.getString("id")?.let { it1 -> threadIds.add(it1) }
|
||||
threadsArray.add(thread)
|
||||
}
|
||||
for(i in 0 until it.size()) {
|
||||
val thread = it.getMap(i)
|
||||
val threadId = thread.getString("id")
|
||||
if (threadId != null) {
|
||||
if (threadIds.contains(threadId)) {
|
||||
// replace the values for participants and is_following
|
||||
val index = threadsArray.indexOfFirst { el -> el.getString("id") == threadId }
|
||||
val prev = threadsArray[index]
|
||||
val merge = Arguments.createMap()
|
||||
merge.merge(prev)
|
||||
merge.putBoolean("is_following", thread.getBoolean("is_following"))
|
||||
merge.putArray("participants", thread.getArray("participants"))
|
||||
threadsArray[index] = merge
|
||||
} else {
|
||||
threadsArray.add(thread)
|
||||
threadIds.add(threadId)
|
||||
private suspend fun fetchPosts(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean, rootId: String?): ReadableMap? {
|
||||
val regex = Regex("""\B@(([a-z0-9-._]*[a-z0-9_])[.-]*)""", setOf(RegexOption.IGNORE_CASE))
|
||||
val since = DatabaseHelper.instance!!.queryPostSinceForChannel(db, channelId)
|
||||
val currentUserId = DatabaseHelper.instance!!.queryCurrentUserId(db)?.removeSurrounding("\"")
|
||||
val currentUser = DatabaseHelper.instance!!.find(db, "User", currentUserId)
|
||||
val currentUsername = currentUser?.getString("username")
|
||||
|
||||
var additionalParams = ""
|
||||
if (isCRTEnabled) {
|
||||
additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true"
|
||||
}
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val endpoint = if (receivingThreads) {
|
||||
val queryParams = "?skipFetchThreads=false&perPage=60&fromCreatedAt=0&direction=up"
|
||||
"/api/v4/posts/$rootId/thread$queryParams$additionalParams"
|
||||
} else {
|
||||
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
|
||||
"/api/v4/channels/$channelId/posts$queryParams$additionalParams"
|
||||
}
|
||||
|
||||
val postsResponse = fetch(serverUrl, endpoint)
|
||||
val results = Arguments.createMap()
|
||||
|
||||
if (postsResponse != null) {
|
||||
val data = ReadableMapUtils.toMap(postsResponse)
|
||||
results.putMap("posts", postsResponse)
|
||||
val postsData = data["data"] as? Map<*, *>
|
||||
if (postsData != null) {
|
||||
val postsMap = postsData["posts"]
|
||||
if (postsMap != null) {
|
||||
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Any>)
|
||||
val iterator = posts.keySetIterator()
|
||||
val userIds = mutableListOf<String>()
|
||||
val usernames = mutableListOf<String>()
|
||||
|
||||
val threads = WritableNativeArray()
|
||||
val threadParticipantUserIds = mutableListOf<String>() // Used to exclude the "userIds" present in the thread participants
|
||||
val threadParticipantUsernames = mutableListOf<String>() // Used to exclude the "usernames" present in the thread participants
|
||||
val threadParticipantUsers = HashMap<String, ReadableMap>() // All unique users from thread participants are stored here
|
||||
|
||||
while(iterator.hasNextKey()) {
|
||||
val key = iterator.nextKey()
|
||||
val post = posts.getMap(key)
|
||||
val userId = post?.getString("user_id")
|
||||
if (userId != null && userId != currentUserId && !userIds.contains(userId)) {
|
||||
userIds.add(userId)
|
||||
}
|
||||
val message = post?.getString("message")
|
||||
if (message != null) {
|
||||
val matchResults = regex.findAll(message)
|
||||
matchResults.iterator().forEach {
|
||||
val username = it.value.removePrefix("@")
|
||||
if (!usernames.contains(username) && currentUsername != username && !specialMentions.contains(username)) {
|
||||
usernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCRTEnabled) {
|
||||
// Add root post as a thread
|
||||
val threadId = post?.getString("root_id")
|
||||
if (threadId.isNullOrEmpty()) {
|
||||
threads.pushMap(post!!)
|
||||
}
|
||||
|
||||
// Add participant userIds and usernames to exclude them from getting fetched again
|
||||
val participants = post.getArray("participants")
|
||||
if (participants != null) {
|
||||
for (i in 0 until participants.size()) {
|
||||
val participant = participants.getMap(i)
|
||||
|
||||
val participantId = participant.getString("id")
|
||||
if (participantId != currentUserId && participantId != null) {
|
||||
if (!threadParticipantUserIds.contains(participantId)) {
|
||||
threadParticipantUserIds.add(participantId)
|
||||
}
|
||||
|
||||
if (!threadParticipantUsers.containsKey(participantId)) {
|
||||
threadParticipantUsers[participantId] = participant
|
||||
}
|
||||
}
|
||||
|
||||
val username = participant.getString("username")
|
||||
if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) {
|
||||
threadParticipantUsernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val existingUserIds = DatabaseHelper.instance!!.queryIds(db, "User", userIds.toTypedArray())
|
||||
val existingUsernames = DatabaseHelper.instance!!.queryByColumn(db, "User", "username", usernames.toTypedArray())
|
||||
userIds.removeAll { it in existingUserIds }
|
||||
usernames.removeAll { it in existingUsernames }
|
||||
|
||||
if (threadParticipantUserIds.size > 0) {
|
||||
// Do not fetch users found in thread participants as we get the user's data in the posts response already
|
||||
userIds.removeAll { it in threadParticipantUserIds }
|
||||
usernames.removeAll { it in threadParticipantUsernames }
|
||||
|
||||
// Get users from thread participants
|
||||
val existingThreadParticipantUserIds = DatabaseHelper.instance!!.queryIds(db, "User", threadParticipantUserIds.toTypedArray())
|
||||
|
||||
// Exclude the thread participants already present in the DB from getting inserted again
|
||||
val usersFromThreads = WritableNativeArray()
|
||||
threadParticipantUsers.forEach{ (userId, user) ->
|
||||
if (!existingThreadParticipantUserIds.contains(userId)) {
|
||||
usersFromThreads.pushMap(user)
|
||||
}
|
||||
}
|
||||
|
||||
if (usersFromThreads.size() > 0) {
|
||||
results.putArray("usersFromThreads", usersFromThreads)
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.size > 0) {
|
||||
results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray()))
|
||||
}
|
||||
|
||||
if (usernames.size > 0) {
|
||||
results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray()))
|
||||
}
|
||||
|
||||
if (threads.size() > 0) {
|
||||
results.putArray("threads", threads)
|
||||
}
|
||||
}
|
||||
}
|
||||
return threadsArray
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
return null
|
||||
private suspend fun fetchUsersById(serverUrl: String, userIds: ReadableArray): ReadableMap? {
|
||||
val endpoint = "api/v4/users/ids"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds)))
|
||||
return fetchWithPost(serverUrl, endpoint, options)
|
||||
}
|
||||
|
||||
private suspend fun fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableMap? {
|
||||
val endpoint = "api/v4/users/usernames"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames)))
|
||||
return fetchWithPost(serverUrl, endpoint, options)
|
||||
}
|
||||
|
||||
private suspend fun fetch(serverUrl: String, endpoint: String): ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.get(serverUrl, endpoint, null, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
if (response != null && !response.getBoolean("ok")) {
|
||||
val error = response.getMap("data")
|
||||
cont.resumeWith(Result.failure((IOException("Unexpected code ${error?.getInt("status_code")} ${error?.getString("message")}"))))
|
||||
} else {
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchWithPost(serverUrl: String, endpoint: String, options: ReadableMap?) : ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.post(serverUrl, endpoint, options, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.mattermost.helpers;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
|
||||
@@ -110,9 +109,7 @@ public class ReadableArrayUtils {
|
||||
writableArray.pushString((String) value);
|
||||
} else if (value instanceof Map) {
|
||||
writableArray.pushMap(ReadableMapUtils.toWritableMap((Map<String, Object>) value));
|
||||
} else if (value instanceof ReadableMap) {
|
||||
writableArray.pushMap((ReadableMap) value);
|
||||
}else if (value.getClass().isArray()) {
|
||||
} else if (value.getClass().isArray()) {
|
||||
writableArray.pushArray(ReadableArrayUtils.toWritableArray((Object[]) value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.net.Uri;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.content.ContentResolver;
|
||||
import android.os.Environment;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.util.Log;
|
||||
@@ -122,37 +121,24 @@ public class RealPathUtil {
|
||||
|
||||
|
||||
File cacheDir = new File(context.getCacheDir(), CACHE_DIR_NAME);
|
||||
boolean cacheDirExists = cacheDir.exists();
|
||||
if (!cacheDirExists) {
|
||||
cacheDirExists = cacheDir.mkdirs();
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
if (cacheDirExists) {
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
boolean fileCreated = tmpFile.createNewFile();
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
tmpFile.createNewFile();
|
||||
|
||||
if (fileCreated) {
|
||||
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
|
||||
try (FileInputStream inputSrc = new FileInputStream(pfd.getFileDescriptor())) {
|
||||
FileChannel src = inputSrc.getChannel();
|
||||
try (FileOutputStream outputDst = new FileOutputStream(tmpFile)) {
|
||||
FileChannel dst = outputDst.getChannel();
|
||||
dst.transferFrom(src, 0, src.size());
|
||||
src.close();
|
||||
dst.close();
|
||||
}
|
||||
}
|
||||
|
||||
pfd.close();
|
||||
}
|
||||
return tmpFile.getAbsolutePath();
|
||||
}
|
||||
FileChannel src = new FileInputStream(pfd.getFileDescriptor()).getChannel();
|
||||
FileChannel dst = new FileOutputStream(tmpFile).getChannel();
|
||||
dst.transferFrom(src, 0, src.size());
|
||||
src.close();
|
||||
dst.close();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return tmpFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
public static String getDataColumn(Context context, Uri uri, String selection,
|
||||
@@ -196,14 +182,8 @@ public class RealPathUtil {
|
||||
}
|
||||
|
||||
public static String getExtension(String uri) {
|
||||
String extension = "";
|
||||
if (uri == null) {
|
||||
return extension;
|
||||
}
|
||||
|
||||
extension = MimeTypeMap.getFileExtensionFromUrl(uri);
|
||||
if (!extension.equals("")) {
|
||||
return extension;
|
||||
return null;
|
||||
}
|
||||
|
||||
int dot = uri.lastIndexOf(".");
|
||||
@@ -230,15 +210,6 @@ public class RealPathUtil {
|
||||
return getMimeType(file);
|
||||
}
|
||||
|
||||
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
|
||||
try {
|
||||
ContentResolver cR = context.getContentResolver();
|
||||
return cR.getType(uri);
|
||||
} catch (Exception e) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteTempFiles(final File dir) {
|
||||
try {
|
||||
if (dir.isDirectory()) {
|
||||
@@ -258,9 +229,7 @@ public class RealPathUtil {
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileOrDirectory.delete()) {
|
||||
Log.i("ReactNative", "Couldn't delete file " + fileOrDirectory.getName());
|
||||
}
|
||||
fileOrDirectory.delete();
|
||||
}
|
||||
|
||||
private static String sanitizeFilename(String filename) {
|
||||
@@ -271,4 +240,5 @@ public class RealPathUtil {
|
||||
File f = new File(filename);
|
||||
return f.getName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
fun insertCategory(db: Database, category: ReadableMap) {
|
||||
try {
|
||||
val id = category.getString("id") ?: return
|
||||
val collapsed = false
|
||||
val displayName = category.getString("display_name")
|
||||
val muted = category.getBoolean("muted")
|
||||
val sortOrder = category.getInt("sort_order")
|
||||
val sorting = category.getString("sorting") ?: "recent"
|
||||
val teamId = category.getString("team_id")
|
||||
val type = category.getString("type")
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Category
|
||||
(id, collapsed, display_name, muted, sort_order, sorting, team_id, type, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, collapsed, displayName, muted,
|
||||
sortOrder / 10, sorting, teamId, type
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertCategoryChannels(db: Database, categoryId: String, teamId: String, channelIds: ReadableArray) {
|
||||
try {
|
||||
for (i in 0 until channelIds.size()) {
|
||||
val channelId = channelIds.getString(i)
|
||||
val id = "${teamId}_$channelId"
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO CategoryChannel
|
||||
(id, category_id, channel_id, sort_order, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, categoryId, channelId, i)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertCategoriesWithChannels(db: Database, orderCategories: ReadableMap) {
|
||||
val categories = orderCategories.getArray("categories") ?: return
|
||||
for (i in 0 until categories.size()) {
|
||||
val category = categories.getMap(i)
|
||||
val id = category.getString("id")
|
||||
val teamId = category.getString("team_id")
|
||||
val channelIds = category.getArray("channel_ids")
|
||||
insertCategory(db, category)
|
||||
if (id != null && teamId != null) {
|
||||
channelIds?.let { insertCategoryChannels(db, id, teamId, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannelToDefaultCategory(db: Database, categoryChannels: ReadableArray) {
|
||||
try {
|
||||
for (i in 0 until categoryChannels.size()) {
|
||||
val cc = categoryChannels.getMap(i)
|
||||
val id = cc.getString("id")
|
||||
val categoryId = cc.getString("category_id")
|
||||
val channelId = cc.getString("channel_id")
|
||||
val count = countByColumn(db, "CategoryChannel", "category_id", categoryId)
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO CategoryChannel
|
||||
(id, category_id, channel_id, sort_order, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, categoryId, channelId, if (count > 0) count + 1 else count)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
fun findChannel(db: Database?, channelId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "Channel", channelId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun findMyChannel(db: Database?, channelId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "MyChannel", channelId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
internal fun handleChannel(db: Database, channel: ReadableMap) {
|
||||
try {
|
||||
val exists = channel.getString("id")?.let { findChannel(db, it) } ?: false
|
||||
if (!exists) {
|
||||
val json = ReadableMapUtils.toJSONObject(channel)
|
||||
if (insertChannel(db, json)) {
|
||||
insertChannelInfo(db, json)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DatabaseHelper.handleMyChannel(db: Database, myChannel: ReadableMap, postsData: ReadableMap?, receivingThreads: Boolean) {
|
||||
try {
|
||||
val json = ReadableMapUtils.toJSONObject(myChannel)
|
||||
val exists = myChannel.getString("id")?.let { findMyChannel(db, it) } ?: false
|
||||
|
||||
if (postsData != null && !receivingThreads) {
|
||||
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
|
||||
val postList = posts.toList()
|
||||
val lastFetchedAt = postList.fold(0.0) { acc, next ->
|
||||
val post = next.second as Map<*, *>
|
||||
val createAt = post["create_at"] as Double
|
||||
val updateAt = post["update_at"] as Double
|
||||
val deleteAt = post["delete_at"] as Double
|
||||
val value = maxOf(createAt, updateAt, deleteAt)
|
||||
|
||||
maxOf(value, acc)
|
||||
}
|
||||
json.put("last_fetched_at", lastFetchedAt)
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
updateMyChannel(db, json)
|
||||
return
|
||||
}
|
||||
|
||||
if (insertMyChannel(db, json)) {
|
||||
insertMyChannelSettings(db, json)
|
||||
insertChannelMember(db, json)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannel(db: Database, channel: JSONObject): Boolean {
|
||||
val id = try { channel.getString("id") } catch (e: JSONException) { return false }
|
||||
val createAt = try { channel.getDouble("create_at") } catch (e: JSONException) { 0 }
|
||||
val deleteAt = try { channel.getDouble("delete_at") } catch (e: JSONException) { 0 }
|
||||
val updateAt = try { channel.getDouble("update_at") } catch (e: JSONException) { 0 }
|
||||
val creatorId = try { channel.getString("creator_id") } catch (e: JSONException) { "" }
|
||||
val displayName = try { channel.getString("display_name") } catch (e: JSONException) { "" }
|
||||
val name = try { channel.getString("name") } catch (e: JSONException) { "" }
|
||||
val teamId = try { channel.getString("team_id") } catch (e: JSONException) { "" }
|
||||
val type = try { channel.getString("type") } catch (e: JSONException) { "O" }
|
||||
val isGroupConstrained = try { channel.getBoolean("group_constrained") } catch (e: JSONException) { false }
|
||||
val shared = try { channel.getBoolean("shared") } catch (e: JSONException) { false }
|
||||
|
||||
return try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Channel
|
||||
(id, create_at, delete_at, update_at, creator_id, display_name, name, team_id, type, is_group_constrained, shared, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, createAt, deleteAt, updateAt,
|
||||
creatorId, displayName, name, teamId, type,
|
||||
isGroupConstrained, shared
|
||||
)
|
||||
)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannelInfo(db: Database, channel: JSONObject) {
|
||||
val id = try { channel.getString("id") } catch (e: JSONException) { return }
|
||||
val header = try { channel.getString("header") } catch (e: JSONException) { "" }
|
||||
val purpose = try { channel.getString("purpose") } catch (e: JSONException) { "" }
|
||||
|
||||
try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ChannelInfo
|
||||
(id, header, purpose, guest_count, member_count, pinned_post_count, _changed, _status)
|
||||
VALUES (?, ?, ?, 0, 0, 0, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, header, purpose)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMyChannel(db: Database, myChanel: JSONObject): Boolean {
|
||||
return try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return false }
|
||||
val roles = try { myChanel.getString("roles") } catch (e: JSONException) { "" }
|
||||
val msgCount = try { myChanel.getInt("message_count") } catch (e: JSONException) { 0 }
|
||||
val mentionsCount = try { myChanel.getInt("mentions_count") } catch (e: JSONException) { 0 }
|
||||
val isUnread = try { myChanel.getBoolean("is_unread") } catch (e: JSONException) { false }
|
||||
val lastPostAt = try { myChanel.getDouble("last_post_at") } catch (e: JSONException) { 0 }
|
||||
val lastViewedAt = try { myChanel.getDouble("last_viewed_at") } catch (e: JSONException) { 0 }
|
||||
val viewedAt = 0
|
||||
val lastFetchedAt = try { myChanel.getDouble("last_fetched_at") } catch (e: JSONException) { 0 }
|
||||
val manuallyUnread = false
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO MyChannel
|
||||
(id, roles, message_count, mentions_count, is_unread, manually_unread,
|
||||
last_post_at, last_viewed_at, viewed_at, last_fetched_at, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""",
|
||||
arrayOf(
|
||||
id, roles, msgCount, mentionsCount, isUnread, manuallyUnread,
|
||||
lastPostAt, lastViewedAt, viewedAt, lastFetchedAt
|
||||
)
|
||||
)
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMyChannelSettings(db: Database, myChanel: JSONObject) {
|
||||
try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val notifyProps = try { myChanel.getString("notify_props") } catch (e: JSONException) { return }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO MyChannelSettings (id, notify_props, _changed, _status)
|
||||
VALUES (?, ?, '', 'created')
|
||||
""",
|
||||
arrayOf(id, notifyProps)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannelMember(db: Database, myChanel: JSONObject) {
|
||||
try {
|
||||
val userId = queryCurrentUserId(db) ?: return
|
||||
val channelId = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val schemeAdmin = try { myChanel.getBoolean("scheme_admin") } catch (e: JSONException) { false }
|
||||
val id = "$channelId-$userId"
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ChannelMembership
|
||||
(id, channel_id, user_id, scheme_admin, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""",
|
||||
arrayOf(id, channelId, userId, schemeAdmin)
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMyChannel(db: Database, myChanel: JSONObject) {
|
||||
try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val msgCount = try { myChanel.getInt("message_count") } catch (e: JSONException) { 0 }
|
||||
val mentionsCount = try { myChanel.getInt("mentions_count") } catch (e: JSONException) { 0 }
|
||||
val isUnread = try { myChanel.getBoolean("is_unread") } catch (e: JSONException) { false }
|
||||
val lastPostAt = try { myChanel.getDouble("last_post_at") } catch (e: JSONException) { 0 }
|
||||
val lastViewedAt = try { myChanel.getDouble("last_viewed_at") } catch (e: JSONException) { 0 }
|
||||
val lastFetchedAt = try { myChanel.getDouble("last_fetched_at") } catch (e: JSONException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE MyChannel SET message_count=?, mentions_count=?, is_unread=?,
|
||||
last_post_at=?, last_viewed_at=?, last_fetched_at=?, _status = 'updated'
|
||||
WHERE id=?
|
||||
""",
|
||||
arrayOf(
|
||||
msgCount, mentionsCount, isUnread,
|
||||
lastPostAt, lastViewedAt, lastFetchedAt, id
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
|
||||
internal fun insertCustomEmojis(db: Database, customEmojis: JSONArray) {
|
||||
for (i in 0 until customEmojis.length()) {
|
||||
try {
|
||||
val emoji = customEmojis.getJSONObject(i)
|
||||
if (find(db, "CustomEmoji", emoji.getString("id")) == null) {
|
||||
db.execute(
|
||||
"INSERT INTO CustomEmoji (id, name, _changed, _status) VALUES (?, ?, '', 'created')",
|
||||
arrayOf(
|
||||
emoji.getString("id"),
|
||||
emoji.getString("name"),
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
|
||||
internal fun insertFiles(db: Database, files: JSONArray) {
|
||||
try {
|
||||
for (i in 0 until files.length()) {
|
||||
val file = files.getJSONObject(i)
|
||||
val id = file.getString("id")
|
||||
val extension = file.getString("extension")
|
||||
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" }
|
||||
val height = try { file.getInt("height") } catch (e: JSONException) { 0 }
|
||||
val mime = file.getString("mime_type")
|
||||
val name = file.getString("name")
|
||||
val postId = file.getString("post_id")
|
||||
val size = try { file.getDouble("size") } catch (e: JSONException) { 0 }
|
||||
val width = try { file.getInt("width") } catch (e: JSONException) { 0 }
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO File
|
||||
(id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, extension, height, miniPreview,
|
||||
mime, name, postId, size, width
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.QueryArgs
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
import java.util.*
|
||||
import kotlin.Exception
|
||||
|
||||
internal fun DatabaseHelper.saveToDatabase(db: Database, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) {
|
||||
db.transaction {
|
||||
val posts = data.getMap("posts")
|
||||
data.getMap("team")?.let { insertTeam(db, it) }
|
||||
data.getMap("myTeam")?.let { insertMyTeam(db, it) }
|
||||
data.getMap("channel")?.let { handleChannel(db, it) }
|
||||
data.getMap("myChannel")?.let { handleMyChannel(db, it, posts, receivingThreads) }
|
||||
data.getMap("categories")?.let { insertCategoriesWithChannels(db, it) }
|
||||
data.getArray("categoryChannels")?.let { insertChannelToDefaultCategory(db, it) }
|
||||
if (channelId != null) {
|
||||
handlePosts(db, posts, channelId, receivingThreads)
|
||||
}
|
||||
data.getArray("threads")?.let {
|
||||
val threadsArray = ArrayList<ReadableMap>()
|
||||
for (i in 0 until it.size()) {
|
||||
threadsArray.add(it.getMap(i))
|
||||
}
|
||||
handleThreads(db, threadsArray, teamId)
|
||||
}
|
||||
data.getArray("users")?.let { handleUsers(db, it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun DatabaseHelper.getServerUrlForIdentifier(identifier: String): String? {
|
||||
try {
|
||||
val query = "SELECT url FROM Servers WHERE identifier=?"
|
||||
defaultDatabase!!.rawQuery(query, arrayOf(identifier)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): Database? {
|
||||
try {
|
||||
val query = "SELECT db_path FROM Servers WHERE url=?"
|
||||
defaultDatabase!!.rawQuery(query, arrayOf(serverUrl)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val databasePath = cursor.getString(0)
|
||||
return Database(databasePath, context!!)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun find(db: Database, tableName: String, id: String?): ReadableMap? {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT * FROM $tableName WHERE id == ? LIMIT 1",
|
||||
arrayOf(id)
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return null
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun findByColumns(db: Database, tableName: String, columnNames: Array<String>, values: QueryArgs): ReadableMap? {
|
||||
try {
|
||||
val whereString = columnNames.joinToString(" AND ") { "$it = ?" }
|
||||
db.rawQuery(
|
||||
"SELECT * FROM $tableName WHERE $whereString LIMIT 1",
|
||||
values
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return null
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun queryIds(db: Database, tableName: String, ids: Array<String>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(ids).map { "?" }.toArray())
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
db.rawQuery("SELECT DISTINCT id FROM $tableName WHERE id IN ($args)", ids as Array<Any?>).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex("id")
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun queryByColumn(db: Database, tableName: String, columnName: String, values: Array<Any?>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(values).map { "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("SELECT DISTINCT $columnName FROM $tableName WHERE $columnName IN ($args)", values).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex(columnName)
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun countByColumn(db: Database, tableName: String, columnName: String, value: Any?): Int {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT COUNT(*) FROM $tableName WHERE $columnName == ? LIMIT 1",
|
||||
arrayOf(value)
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return 0
|
||||
}
|
||||
cursor.moveToFirst()
|
||||
return cursor.getInt(0)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import kotlin.Exception
|
||||
|
||||
internal fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
|
||||
db.rawQuery(postsInChannelQuery, arrayOf(channelId)).use { cursor1 ->
|
||||
if (cursor1.count == 1) {
|
||||
cursor1.moveToFirst()
|
||||
val earliest = cursor1.getDouble(0)
|
||||
val latest = cursor1.getDouble(1)
|
||||
val postQuery = "SELECT create_at FROM POST WHERE channel_id= ? AND delete_at=0 AND create_at BETWEEN ? AND ? ORDER BY create_at DESC"
|
||||
|
||||
db.rawQuery(postQuery, arrayOf(channelId, earliest, latest)).use { cursor2 ->
|
||||
if (cursor2.count >= 60) {
|
||||
cursor2.moveToFirst()
|
||||
return cursor2.getDouble(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
|
||||
db.rawQuery(postsInChannelQuery, arrayOf(channelId)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val lastFetchedAt = cursor.getDouble(0)
|
||||
if (lastFetchedAt == 0.0) {
|
||||
return queryLastPostCreateAt(db, channelId)
|
||||
}
|
||||
return lastFetchedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryLastPostInThread(db: Database?, rootId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val query = "SELECT create_at FROM Post WHERE root_id=? AND delete_at=0 ORDER BY create_at DESC LIMIT 1"
|
||||
db.rawQuery(query, arrayOf(rootId)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getDouble(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun insertPost(db: Database, post: JSONObject) {
|
||||
try {
|
||||
val id = try { post.getString("id") } catch (e: JSONException) { return }
|
||||
val channelId = try { post.getString("channel_id") } catch (e: JSONException) { return }
|
||||
val userId = try { post.getString("user_id") } catch (e: JSONException) { return }
|
||||
val createAt = try { post.getDouble("create_at") } catch (e: JSONException) { return }
|
||||
val deleteAt = try { post.getDouble("delete_at") } catch (e: JSONException) { 0 }
|
||||
val updateAt = try { post.getDouble("update_at") } catch (e: JSONException) { 0 }
|
||||
val editAt = try { post.getDouble("edit_at") } catch (e: JSONException) { 0 }
|
||||
val isPinned = try { post.getBoolean("is_pinned") } catch (e: JSONException) { false }
|
||||
val message = try { post.getString("message") } catch (e: JSONException) { "" }
|
||||
val metadata = try { post.getJSONObject("metadata") } catch (e: JSONException) { JSONObject() }
|
||||
val originalId = try { post.getString("original_id") } catch (e: JSONException) { "" }
|
||||
val pendingId = try { post.getString("pending_post_id") } catch (e: JSONException) { "" }
|
||||
val prevId = try { post.getString("prev_post_id") } catch (e: JSONException) { "" }
|
||||
val rootId = try { post.getString("root_id") } catch (e: JSONException) { "" }
|
||||
val type = try { post.getString("type") } catch (e: JSONException) { "" }
|
||||
val props = try { post.getJSONObject("props").toString() } catch (e: JSONException) { "" }
|
||||
val reactions = metadata.remove("reactions") as JSONArray?
|
||||
val customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
val files = metadata.remove("files") as JSONArray?
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Post
|
||||
(id, channel_id, create_at, delete_at, update_at, edit_at, is_pinned, message, metadata, original_id, pending_post_id,
|
||||
previous_post_id, root_id, type, user_id, props, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, channelId, createAt, deleteAt, updateAt, editAt,
|
||||
isPinned, message, metadata.toString(),
|
||||
originalId, pendingId, prevId, rootId,
|
||||
type, userId, props
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
|
||||
if (files != null && files.length() > 0) {
|
||||
insertFiles(db, files)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updatePost(db: Database, post: JSONObject) {
|
||||
try {
|
||||
val id = try { post.getString("id") } catch (e: JSONException) { return }
|
||||
val channelId = try { post.getString("channel_id") } catch (e: JSONException) { return }
|
||||
val userId = try { post.getString("user_id") } catch (e: JSONException) { return }
|
||||
val createAt = try { post.getDouble("create_at") } catch (e: JSONException) { return }
|
||||
val deleteAt = try { post.getDouble("delete_at") } catch (e: JSONException) { 0 }
|
||||
val updateAt = try { post.getDouble("update_at") } catch (e: JSONException) { 0 }
|
||||
val editAt = try { post.getDouble("edit_at") } catch (e: JSONException) { 0 }
|
||||
val isPinned = try { post.getBoolean("is_pinned") } catch (e: JSONException) { false }
|
||||
val message = try { post.getString("message") } catch (e: JSONException) { "" }
|
||||
val metadata = try { post.getJSONObject("metadata") } catch (e: JSONException) { JSONObject() }
|
||||
val originalId = try { post.getString("original_id") } catch (e: JSONException) { "" }
|
||||
val pendingId = try { post.getString("pending_post_id") } catch (e: JSONException) { "" }
|
||||
val prevId = try { post.getString("prev_post_id") } catch (e: JSONException) { "" }
|
||||
val rootId = try { post.getString("root_id") } catch (e: JSONException) { "" }
|
||||
val type = try { post.getString("type") } catch (e: JSONException) { "" }
|
||||
val props = try { post.getJSONObject("props").toString() } catch (e: JSONException) { "" }
|
||||
val reactions = metadata.remove("reactions") as JSONArray?
|
||||
val customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
|
||||
metadata.remove("files")
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE Post SET channel_id = ?, create_at = ?, delete_at = ?, update_at =?, edit_at =?,
|
||||
is_pinned = ?, message = ?, metadata = ?, original_id = ?, pending_post_id = ?, previous_post_id = ?,
|
||||
root_id = ?, type = ?, user_id = ?, props = ?, _status = 'updated'
|
||||
WHERE id = ?
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
channelId, createAt, deleteAt, updateAt, editAt,
|
||||
isPinned, message, metadata.toString(),
|
||||
originalId, pendingId, prevId, rootId,
|
||||
type, userId, props,
|
||||
id,
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
db.execute("DELETE FROM Reaction WHERE post_id = ?", arrayOf(post.getString("id")))
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun DatabaseHelper.handlePosts(db: Database, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
|
||||
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
|
||||
try {
|
||||
if (postsData != null) {
|
||||
val ordered = postsData.getArray("order")?.toArrayList()
|
||||
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
|
||||
val previousPostId = postsData.getString("prev_post_id")
|
||||
val postsInThread = hashMapOf<String, List<JSONObject>>()
|
||||
val postList = posts.toList()
|
||||
var earliest = 0.0
|
||||
var latest = 0.0
|
||||
|
||||
if (ordered != null && posts.isNotEmpty()) {
|
||||
val firstId = ordered.first()
|
||||
val lastId = ordered.last()
|
||||
var prevPostId = ""
|
||||
|
||||
val sortedPosts = postList.sortedBy { (_, value) ->
|
||||
((value as Map<*, *>)["create_at"] as Double)
|
||||
}
|
||||
|
||||
sortedPosts.forEachIndexed { index, it ->
|
||||
val key = it.first
|
||||
if (it.second != null) {
|
||||
@Suppress("UNCHECKED_CAST", "UNCHECKED_CAST")
|
||||
val post: MutableMap<String, Any?> = it.second as MutableMap<String, Any?>
|
||||
|
||||
if (index == 0) {
|
||||
post.putIfAbsent("prev_post_id", previousPostId)
|
||||
} else if (prevPostId.isNotEmpty()) {
|
||||
post.putIfAbsent("prev_post_id", prevPostId)
|
||||
}
|
||||
|
||||
if (lastId == key) {
|
||||
earliest = post["create_at"] as Double
|
||||
}
|
||||
if (firstId == key) {
|
||||
latest = post["create_at"] as Double
|
||||
}
|
||||
|
||||
val jsonPost = JSONObject(post)
|
||||
val postId = post["id"] as? String ?: ""
|
||||
val rootId = post["root_id"] as? String ?: ""
|
||||
val postInThread = rootId.ifEmpty { postId }
|
||||
var thread = postsInThread[postInThread]?.toMutableList()
|
||||
if (thread == null) {
|
||||
thread = mutableListOf()
|
||||
}
|
||||
|
||||
thread.add(jsonPost)
|
||||
postsInThread[postInThread] = thread.toList()
|
||||
|
||||
if (find(db, "Post", key) == null) {
|
||||
insertPost(db, jsonPost)
|
||||
} else {
|
||||
updatePost(db, jsonPost)
|
||||
}
|
||||
|
||||
if (ordered.contains(key)) {
|
||||
prevPostId = key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!receivingThreads) {
|
||||
handlePostsInChannel(db, channelId, earliest, latest)
|
||||
}
|
||||
handlePostsInThread(db, postsInThread)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
|
||||
internal fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? {
|
||||
for (i in 0 until chunks.size()) {
|
||||
val chunk = chunks.getMap(i)
|
||||
if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) {
|
||||
return chunk
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap? {
|
||||
return try {
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO PostsInChannel
|
||||
(id, channel_id, earliest, latest, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, channelId, earliest, latest))
|
||||
|
||||
val map = Arguments.createMap()
|
||||
map.putString("id", id)
|
||||
map.putString("channel_id", channelId)
|
||||
map.putDouble("earliest", earliest)
|
||||
map.putDouble("latest", latest)
|
||||
map
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun mergePostsInChannel(db: Database, existingChunks: ReadableArray, newChunk: ReadableMap) {
|
||||
for (i in 0 until existingChunks.size()) {
|
||||
try {
|
||||
val chunk = existingChunks.getMap(i)
|
||||
if (newChunk.getDouble("earliest") <= chunk.getDouble("earliest") &&
|
||||
newChunk.getDouble("latest") >= chunk.getDouble("latest")) {
|
||||
db.execute("DELETE FROM PostsInChannel WHERE id = ?", arrayOf(chunk.getString("id")))
|
||||
break
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun handlePostsInChannel(db: Database, channelId: String, earliest: Double, latest: Double) {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT id, channel_id, earliest, latest FROM PostsInChannel WHERE channel_id = ?",
|
||||
arrayOf(channelId)
|
||||
).use { cursor ->
|
||||
if (cursor.count == 0) {
|
||||
// create new post in channel
|
||||
insertPostInChannel(db, channelId, earliest, latest)
|
||||
return
|
||||
}
|
||||
|
||||
val resultArray = Arguments.createArray()
|
||||
while (cursor.moveToNext()) {
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
resultArray.pushMap(cursorMap)
|
||||
}
|
||||
|
||||
val chunk = findPostInChannel(resultArray, earliest, latest)
|
||||
if (chunk != null) {
|
||||
db.execute(
|
||||
"UPDATE PostsInChannel SET earliest = ?, latest = ?, _status = 'updated' WHERE id = ?",
|
||||
arrayOf(
|
||||
minOf(earliest, chunk.getDouble("earliest")),
|
||||
maxOf(latest, chunk.getDouble("latest")),
|
||||
chunk.getString("id")
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val newChunk = insertPostInChannel(db, channelId, earliest, latest)
|
||||
newChunk?.let { mergePostsInChannel(db, resultArray, it) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
|
||||
fun getTeammateDisplayNameSetting(db: Database): String {
|
||||
val configSetting = queryConfigDisplayNameSetting(db)
|
||||
if (configSetting != null) {
|
||||
return configSetting
|
||||
}
|
||||
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT value FROM Preference where category = ? AND name = ? limit 1",
|
||||
arrayOf("display_settings", "name_format")
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return "username"
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap?.getString("value") ?: "username"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return "username"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
|
||||
internal fun insertReactions(db: Database, reactions: JSONArray) {
|
||||
for (i in 0 until reactions.length()) {
|
||||
try {
|
||||
val reaction = reactions.getJSONObject(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Reaction
|
||||
(id, create_at, emoji_name, post_id, user_id, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id,
|
||||
reaction.getDouble("create_at"), reaction.getString("emoji_name"),
|
||||
reaction.getString("post_id"), reaction.getString("user_id")
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONObject
|
||||
|
||||
fun queryCurrentUserId(db: Database): String? {
|
||||
val result = find(db, "System", "currentUserId")
|
||||
return result?.getString("value")?.removeSurrounding("\"")
|
||||
}
|
||||
|
||||
fun queryCurrentTeamId(db: Database): String? {
|
||||
val result = find(db, "System", "currentTeamId")
|
||||
return result?.getString("value")?.removeSurrounding("\"")
|
||||
}
|
||||
|
||||
fun queryConfigDisplayNameSetting(db: Database): String? {
|
||||
val license = find(db, "System", "license")
|
||||
val lockDisplayName = find(db, "Config", "LockTeammateNameDisplay")
|
||||
val displayName = find(db, "Config", "TeammateNameDisplay")
|
||||
|
||||
val licenseValue = license?.getString("value") ?: ""
|
||||
val lockDisplayNameValue = lockDisplayName?.getString("value") ?: "false"
|
||||
val displayNameValue = displayName?.getString("value") ?: "full_name"
|
||||
val licenseJson = JSONObject(licenseValue)
|
||||
val licenseLock = try { licenseJson.getString("LockTeammateNameDisplay") } catch (e: Exception) { "false"}
|
||||
|
||||
if (licenseLock == "true" && lockDisplayNameValue == "true") {
|
||||
return displayNameValue
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
|
||||
fun findTeam(db: Database?, teamId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "Team", teamId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun findMyTeam(db: Database?, teamId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "MyTeam", teamId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun queryMyTeams(db: Database?): ArrayList<ReadableMap>? {
|
||||
db?.rawQuery("SELECT * FROM MyTeam")?.use { cursor ->
|
||||
val results = ArrayList<ReadableMap>()
|
||||
if (cursor.count > 0) {
|
||||
while(cursor.moveToNext()) {
|
||||
val map = Arguments.createMap()
|
||||
map.mapCursor(cursor)
|
||||
results.add(map)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun insertTeam(db: Database, team: ReadableMap): Boolean {
|
||||
val id = try { team.getString("id") } catch (e: Exception) { return false }
|
||||
val deleteAt = try {team.getDouble("delete_at") } catch (e: Exception) { 0 }
|
||||
if (deleteAt.toInt() > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
val isAllowOpenInvite = try { team.getBoolean("allow_open_invite") } catch (e: NoSuchKeyException) { false }
|
||||
val description = try { team.getString("description") } catch (e: NoSuchKeyException) { "" }
|
||||
val displayName = try { team.getString("display_name") } catch (e: NoSuchKeyException) { "" }
|
||||
val name = try { team.getString("name") } catch (e: NoSuchKeyException) { "" }
|
||||
val updateAt = try { team.getDouble("update_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val type = try { team.getString("type") } catch (e: NoSuchKeyException) { "O" }
|
||||
val allowedDomains = try { team.getString("allowed_domains") } catch (e: NoSuchKeyException) { "" }
|
||||
val isGroupConstrained = try { team.getBoolean("group_constrained") } catch (e: NoSuchKeyException) { false }
|
||||
val lastTeamIconUpdatedAt = try { team.getDouble("last_team_icon_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
val inviteId = try { team.getString("invite_id") } catch (e: NoSuchKeyException) { "" }
|
||||
val status = "created"
|
||||
|
||||
return try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Team (
|
||||
id, allow_open_invite, description, display_name, name, update_at, type, allowed_domains,
|
||||
group_constrained, last_team_icon_update, invite_id, _changed, _status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?)
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, isAllowOpenInvite, description, displayName, name, updateAt,
|
||||
type, allowedDomains, isGroupConstrained, lastTeamIconUpdatedAt, inviteId, status
|
||||
)
|
||||
)
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMyTeam(db: Database, myTeam: ReadableMap): Boolean {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return false
|
||||
val id = try { myTeam.getString("id") } catch (e: NoSuchKeyException) { return false }
|
||||
val roles = try { myTeam.getString("roles") } catch (e: NoSuchKeyException) { "" }
|
||||
val schemeAdmin = try { myTeam.getBoolean("scheme_admin") } catch (e: NoSuchKeyException) { false }
|
||||
val status = "created"
|
||||
val membershipId = "$id-$currentUserId"
|
||||
|
||||
return try {
|
||||
db.execute(
|
||||
"INSERT INTO MyTeam (id, roles, _changed, _status) VALUES (?, ?, '', ?)",
|
||||
arrayOf(id, roles, status)
|
||||
)
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO TeamMembership (id, team_id, user_id, scheme_admin, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', ?)
|
||||
""".trimIndent(),
|
||||
arrayOf(membershipId, id, currentUserId, schemeAdmin, status)
|
||||
)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
import org.json.JSONObject
|
||||
|
||||
internal fun insertThread(db: Database, thread: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
try {
|
||||
val id = try { thread.getString("id") } catch (e: NoSuchKeyException) { return }
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { false }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Thread
|
||||
(id, last_reply_at, last_fetched_at, last_viewed_at, reply_count, is_following, unread_replies, unread_mentions, viewed_at, _changed, _status)
|
||||
VALUES (?, ?, 0, ?, ?, ?, ?, ?, 0, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, lastReplyAt, lastViewedAt,
|
||||
replyCount, isFollowing, unreadReplies, unreadMentions
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateThread(db: Database, thread: ReadableMap, existingRecord: ReadableMap) {
|
||||
try {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val id = try { thread.getString("id") } catch (e: NoSuchKeyException) { return }
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { existingRecord.getInt("is_following") == 1 }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE Thread SET
|
||||
last_reply_at = ?, last_viewed_at = ?, reply_count = ?, is_following = ?, unread_replies = ?,
|
||||
unread_mentions = ?, _status = 'updated' where id = ?
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
lastReplyAt, lastViewedAt, replyCount,
|
||||
isFollowing, unreadReplies, unreadMentions, id
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun insertThreadParticipants(db: Database, threadId: String, participants: ReadableArray) {
|
||||
for (i in 0 until participants.size()) {
|
||||
try {
|
||||
val participant = participants.getMap(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ThreadParticipant
|
||||
(id, thread_id, user_id, _changed, _status)
|
||||
VALUES (?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, threadId, participant.getString("id"))
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun insertTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double) {
|
||||
try {
|
||||
val query = """
|
||||
INSERT INTO TeamThreadsSync (id, _changed, _status, earliest, latest)
|
||||
VALUES (?, '', 'created', ?, ?)
|
||||
"""
|
||||
db.execute(query, arrayOf(teamId, earliest, latest))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double, existingRecord: ReadableMap) {
|
||||
try {
|
||||
val storeEarliest = minOf(earliest, existingRecord.getDouble("earliest"))
|
||||
val storeLatest = maxOf(latest, existingRecord.getDouble("latest"))
|
||||
val query = "UPDATE TeamThreadsSync SET earliest=?, latest=? WHERE id=?"
|
||||
db.execute(query, arrayOf(storeEarliest, storeLatest, teamId))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun syncParticipants(db: Database, thread: ReadableMap) {
|
||||
try {
|
||||
val threadId = thread.getString("id")
|
||||
val participants = thread.getArray("participants")
|
||||
if (participants != null) {
|
||||
db.execute("DELETE FROM ThreadParticipant WHERE thread_id = ?", arrayOf(threadId))
|
||||
|
||||
if (participants.size() > 0) {
|
||||
insertThreadParticipants(db, threadId!!, participants)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun handlePostsInThread(db: Database, postsInThread: Map<String, List<JSONObject>>) {
|
||||
postsInThread.forEach { (key, list) ->
|
||||
try {
|
||||
val sorted = list.sortedBy { it.getDouble("create_at") }
|
||||
val earliest = sorted.first().getDouble("create_at")
|
||||
val latest = sorted.last().getDouble("create_at")
|
||||
db.rawQuery("SELECT * FROM PostsInThread WHERE root_id = ? ORDER BY latest DESC", arrayOf(key)).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
cursor.moveToFirst()
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
val storeEarliest = minOf(earliest, cursorMap.getDouble("earliest"))
|
||||
val storeLatest = maxOf(latest, cursorMap.getDouble("latest"))
|
||||
db.execute(
|
||||
"UPDATE PostsInThread SET earliest = ?, latest = ?, _status = 'updated' WHERE root_id = ?",
|
||||
arrayOf(
|
||||
storeEarliest,
|
||||
storeLatest,
|
||||
key
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO PostsInThread
|
||||
(id, root_id, earliest, latest, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, key, earliest, latest)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleThreads(db: Database, threads: ArrayList<ReadableMap>, teamId: String?) {
|
||||
val teamIds = ArrayList<String>()
|
||||
if (teamId.isNullOrEmpty()) {
|
||||
val myTeams = queryMyTeams(db)
|
||||
if (myTeams != null) {
|
||||
for (myTeam in myTeams) {
|
||||
myTeam.getString("id")?.let { teamIds.add(it) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
teamIds.add(teamId)
|
||||
}
|
||||
|
||||
for (i in 0 until threads.size) {
|
||||
try {
|
||||
val thread = threads[i]
|
||||
handleThread(db, thread, teamIds)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
handleTeamThreadsSync(db, threads, teamIds)
|
||||
}
|
||||
|
||||
fun handleThread(db: Database, thread: ReadableMap, teamIds: ArrayList<String>) {
|
||||
// Insert/Update the thread
|
||||
val threadId = thread.getString("id")
|
||||
val isFollowing = thread.getBoolean("is_following")
|
||||
val existingRecord = find(db, "Thread", threadId)
|
||||
if (existingRecord == null) {
|
||||
insertThread(db, thread)
|
||||
} else {
|
||||
updateThread(db, thread, existingRecord)
|
||||
}
|
||||
|
||||
syncParticipants(db, thread)
|
||||
|
||||
// this is per team
|
||||
if (isFollowing) {
|
||||
for (teamId in teamIds) {
|
||||
handleThreadInTeam(db, thread, teamId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleThreadInTeam(db: Database, thread: ReadableMap, teamId: String) {
|
||||
val threadId = thread.getString("id") ?: return
|
||||
val existingRecord = findByColumns(
|
||||
db,
|
||||
"ThreadsInTeam",
|
||||
arrayOf("thread_id", "team_id"),
|
||||
arrayOf(threadId, teamId)
|
||||
)
|
||||
if (existingRecord == null) {
|
||||
try {
|
||||
val id = RandomId.generate()
|
||||
val query = """
|
||||
INSERT INTO ThreadsInTeam (id, team_id, thread_id, _changed, _status)
|
||||
VALUES (?, ?, ?, '', 'created')
|
||||
"""
|
||||
db.execute(query, arrayOf(id, teamId, threadId))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleTeamThreadsSync(db: Database, threadList: ArrayList<ReadableMap>, teamIds: ArrayList<String>) {
|
||||
val sortedList = threadList.filter{ it.getBoolean("is_following") }
|
||||
.sortedBy { it.getDouble("last_reply_at") }
|
||||
.map { it.getDouble("last_reply_at") }
|
||||
val earliest = sortedList.first()
|
||||
val latest = sortedList.last()
|
||||
|
||||
for (teamId in teamIds) {
|
||||
val existingTeamThreadsSync = find(db, "TeamThreadsSync", teamId)
|
||||
if (existingTeamThreadsSync == null) {
|
||||
insertTeamThreadsSync(db, teamId, earliest, latest)
|
||||
} else {
|
||||
updateTeamThreadsSync(db, teamId, earliest, latest, existingTeamThreadsSync)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
fun getLastPictureUpdate(db: Database?, userId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
var id = userId
|
||||
if (userId == "me") {
|
||||
(queryCurrentUserId(db) ?: userId).also { id = it }
|
||||
}
|
||||
val userQuery = "SELECT last_picture_update FROM User WHERE id=?"
|
||||
db.rawQuery(userQuery, arrayOf(id)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getDouble(0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getCurrentUserLocale(db: Database): String {
|
||||
try {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return "en"
|
||||
val userQuery = "SELECT locale FROM User WHERE id=?"
|
||||
db.rawQuery(userQuery, arrayOf(currentUserId)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return "en"
|
||||
}
|
||||
|
||||
fun handleUsers(db: Database, users: ReadableArray) {
|
||||
for (i in 0 until users.size()) {
|
||||
val user = users.getMap(i)
|
||||
val roles = user.getString("roles") ?: ""
|
||||
val isBot = try {
|
||||
user.getBoolean("is_bot")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
}
|
||||
|
||||
val lastPictureUpdate = try { user.getDouble("last_picture_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO User (id, auth_service, update_at, delete_at, email, first_name, is_bot, is_guest,
|
||||
last_name, last_picture_update, locale, nickname, position, roles, status, username, notify_props,
|
||||
props, timezone, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
user.getString("id"),
|
||||
user.getString("auth_service"), user.getDouble("update_at"), user.getDouble("delete_at"),
|
||||
user.getString("email"), user.getString("first_name"), isBot,
|
||||
roles.contains("system_guest"), user.getString("last_name"), lastPictureUpdate,
|
||||
user.getString("locale"), user.getString("nickname"), user.getString("position"),
|
||||
roles, "", user.getString("username"), "{}",
|
||||
ReadableMapUtils.toJSONObject(user.getMap("props")
|
||||
?: Arguments.createMap()).toString(),
|
||||
ReadableMapUtils.toJSONObject(user.getMap("timezone")
|
||||
?: Arguments.createMap()).toString(),
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.findByColumns
|
||||
import com.mattermost.helpers.database_extension.queryCurrentUserId
|
||||
import com.mattermost.helpers.database_extension.queryMyTeams
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: Database, serverUrl: String, teamId: String): ReadableMap? {
|
||||
return try {
|
||||
val userId = queryCurrentUserId(db)
|
||||
val categories = fetch(serverUrl, "/api/v4/users/$userId/teams/$teamId/channels/categories")
|
||||
categories?.getMap("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: Database, channel: ReadableMap): ReadableArray? {
|
||||
val channelId = channel.getString("id") ?: return null
|
||||
val channelType = channel.getString("type")
|
||||
val categoryChannels = Arguments.createArray()
|
||||
if (channelType == "D" || channelType == "G") {
|
||||
val myTeams = queryMyTeams(db)
|
||||
myTeams?.let {
|
||||
for (myTeam in it) {
|
||||
val map = categoryChannelForTeam(db, channelId, myTeam.getString("id"), "direct_messages")
|
||||
if (map != null) {
|
||||
categoryChannels.pushMap(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val map = categoryChannelForTeam(db, channelId, channel.getString("team_id"), "channels")
|
||||
if (map != null) {
|
||||
categoryChannels.pushMap(map)
|
||||
}
|
||||
}
|
||||
|
||||
return categoryChannels
|
||||
}
|
||||
|
||||
private fun categoryChannelForTeam(db: Database, channelId: String, teamId: String?, type: String): ReadableMap? {
|
||||
teamId?.let { id ->
|
||||
val category = findByColumns(db, "Category", arrayOf("type", "team_id"), arrayOf(type, id))
|
||||
val categoryId = category?.getString("id")
|
||||
categoryId?.let { cId ->
|
||||
val cc = findByColumns(
|
||||
db,
|
||||
"CategoryChannel",
|
||||
arrayOf("category_id", "channel_id"),
|
||||
arrayOf(cId, channelId)
|
||||
)
|
||||
if (cc == null) {
|
||||
val map = Arguments.createMap()
|
||||
map.putString("channel_id", channelId)
|
||||
map.putString("category_id", cId)
|
||||
map.putString("id", "${id}_$channelId")
|
||||
return map
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.findChannel
|
||||
import com.mattermost.helpers.database_extension.getCurrentUserLocale
|
||||
import com.mattermost.helpers.database_extension.getTeammateDisplayNameSetting
|
||||
import com.mattermost.helpers.database_extension.queryCurrentUserId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchMyChannel(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean): Triple<ReadableMap?, ReadableMap?, ReadableArray?> {
|
||||
val channel = fetch(serverUrl, "/api/v4/channels/$channelId")
|
||||
var channelData = channel?.getMap("data")
|
||||
val myChannelData = channelData?.let { fetchMyChannelData(serverUrl, channelId, isCRTEnabled, it) }
|
||||
val channelType = channelData?.getString("type")
|
||||
var profilesArray: ReadableArray? = null
|
||||
|
||||
if (channelData != null && channelType != null && !findChannel(db, channelId)) {
|
||||
val displayNameSetting = getTeammateDisplayNameSetting(db)
|
||||
|
||||
when (channelType) {
|
||||
"D" -> {
|
||||
profilesArray = fetchProfileInChannel(db, serverUrl, channelId)
|
||||
if ((profilesArray?.size() ?: 0) > 0) {
|
||||
val displayName = displayUsername(profilesArray!!.getMap(0), displayNameSetting)
|
||||
val data = Arguments.createMap()
|
||||
data.merge(channelData)
|
||||
data.putString("display_name", displayName)
|
||||
channelData = data
|
||||
}
|
||||
}
|
||||
"G" -> {
|
||||
profilesArray = fetchProfileInChannel(db, serverUrl, channelId)
|
||||
if ((profilesArray?.size() ?: 0) > 0) {
|
||||
val localeString = getCurrentUserLocale(db)
|
||||
val localeArray = localeString.split("-")
|
||||
val locale = if (localeArray.size == 1) {
|
||||
Locale(localeString)
|
||||
} else {
|
||||
Locale(localeArray[0], localeArray[1])
|
||||
}
|
||||
val displayName = displayGroupMessageName(profilesArray!!, locale, displayNameSetting)
|
||||
val data = Arguments.createMap()
|
||||
data.merge(channelData)
|
||||
data.putString("display_name", displayName)
|
||||
channelData = data
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return Triple(channelData, myChannelData, profilesArray)
|
||||
}
|
||||
|
||||
private suspend fun PushNotificationDataRunnable.Companion.fetchMyChannelData(serverUrl: String, channelId: String, isCRTEnabled: Boolean, channelData: ReadableMap): ReadableMap? {
|
||||
try {
|
||||
val myChannel = fetch(serverUrl, "/api/v4/channels/$channelId/members/me")
|
||||
val myChannelData = myChannel?.getMap("data")
|
||||
if (myChannelData != null) {
|
||||
val data = Arguments.createMap()
|
||||
data.merge(myChannelData)
|
||||
data.putString("id", channelId)
|
||||
|
||||
val totalMsg = if (isCRTEnabled) {
|
||||
channelData.getInt("total_msg_count_root")
|
||||
} else {
|
||||
channelData.getInt("total_msg_count")
|
||||
}
|
||||
|
||||
val myMsgCount = if (isCRTEnabled) {
|
||||
myChannelData.getInt("msg_count_root")
|
||||
} else {
|
||||
myChannelData.getInt("msg_count")
|
||||
}
|
||||
|
||||
val mentionCount = if (isCRTEnabled) {
|
||||
myChannelData.getInt("mention_count_root")
|
||||
} else {
|
||||
myChannelData.getInt("mention_count")
|
||||
}
|
||||
|
||||
val lastPostAt = if (isCRTEnabled) {
|
||||
try {
|
||||
channelData.getDouble("last_root_post_at")
|
||||
} catch (e: Exception) {
|
||||
channelData.getDouble("last_post_at")
|
||||
}
|
||||
} else {
|
||||
channelData.getDouble("last_post_at")
|
||||
}
|
||||
|
||||
val messageCount = 0.coerceAtLeast(totalMsg - myMsgCount)
|
||||
data.putInt("message_count", messageCount)
|
||||
data.putInt("mentions_count", mentionCount)
|
||||
data.putBoolean("is_unread", messageCount > 0)
|
||||
data.putDouble("last_post_at", lastPostAt)
|
||||
return data
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun PushNotificationDataRunnable.Companion.fetchProfileInChannel(db: Database, serverUrl: String, channelId: String): ReadableArray? {
|
||||
return try {
|
||||
val currentUserId = queryCurrentUserId(db)
|
||||
val profilesInChannel = fetch(serverUrl, "/api/v4/users?in_channel=${channelId}&page=0&per_page=8&sort=")
|
||||
val profilesArray = profilesInChannel?.getArray("data")
|
||||
val result = Arguments.createArray()
|
||||
if (profilesArray != null) {
|
||||
for (i in 0 until profilesArray.size()) {
|
||||
val profile = profilesArray.getMap(i)
|
||||
if (profile.getString("id") != currentUserId) {
|
||||
result.pushMap(profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun PushNotificationDataRunnable.Companion.displayUsername(user: ReadableMap, displayNameSetting: String): String {
|
||||
val name = user.getString("username") ?: ""
|
||||
val nickname = user.getString("nickname")
|
||||
val firstName = user.getString("first_name") ?: ""
|
||||
val lastName = user.getString("last_name") ?: ""
|
||||
return when (displayNameSetting) {
|
||||
"nickname_full_name" -> {
|
||||
(nickname ?: "$firstName $lastName").trim()
|
||||
}
|
||||
"full_name" -> {
|
||||
"$firstName $lastName".trim()
|
||||
}
|
||||
else -> {
|
||||
name.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PushNotificationDataRunnable.Companion.displayGroupMessageName(profilesArray: ReadableArray, locale: Locale, displayNameSetting: String): String {
|
||||
val names = ArrayList<String>()
|
||||
for (i in 0 until profilesArray.size()) {
|
||||
val profile = profilesArray.getMap(i)
|
||||
names.add(displayUsername(profile, displayNameSetting))
|
||||
}
|
||||
|
||||
return names.sortedWith { s1, s2 ->
|
||||
Collator.getInstance(locale).compare(s1, s2)
|
||||
}.joinToString(", ").trim()
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.Network
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.ResolvePromise
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetch(serverUrl: String, endpoint: String): ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.get(serverUrl, endpoint, null, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
if (response != null && !response.getBoolean("ok")) {
|
||||
val error = response.getMap("data")
|
||||
cont.resumeWith(Result.failure((IOException("Unexpected code ${error?.getInt("status_code")} ${error?.getString("message")}"))))
|
||||
} else {
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchWithPost(serverUrl: String, endpoint: String, options: ReadableMap?) : ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.post(serverUrl, endpoint, options, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.WritableNativeArray
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.ReadableArrayUtils
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.mattermost.helpers.database_extension.*
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts(
|
||||
db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean,
|
||||
rootId: String?, loadedProfiles: ReadableArray?
|
||||
): ReadableMap? {
|
||||
return try {
|
||||
val regex = Regex("""\B@(([a-z\d-._]*[a-z\d_])[.-]*)""", setOf(RegexOption.IGNORE_CASE))
|
||||
val currentUserId = queryCurrentUserId(db)
|
||||
val currentUser = find(db, "User", currentUserId)
|
||||
val currentUsername = currentUser?.getString("username")
|
||||
|
||||
var additionalParams = ""
|
||||
if (isCRTEnabled) {
|
||||
additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true"
|
||||
}
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val endpoint = if (receivingThreads) {
|
||||
val since = rootId?.let { queryLastPostInThread(db, it) }
|
||||
val queryParams = if (since == null) "?perPage=60&fromCreatedAt=0&direction=up" else
|
||||
"?fromCreateAt=${since.toLong()}&direction=down"
|
||||
|
||||
"/api/v4/posts/$rootId/thread$queryParams$additionalParams"
|
||||
} else {
|
||||
val since = queryPostSinceForChannel(db, channelId)
|
||||
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
|
||||
"/api/v4/channels/$channelId/posts$queryParams$additionalParams"
|
||||
}
|
||||
|
||||
val postsResponse = fetch(serverUrl, endpoint)
|
||||
val postData = postsResponse?.getMap("data")
|
||||
val results = Arguments.createMap()
|
||||
|
||||
if (postData != null) {
|
||||
val data = ReadableMapUtils.toMap(postData)
|
||||
results.putMap("posts", postData)
|
||||
if (data != null) {
|
||||
val postsMap = data["posts"]
|
||||
if (postsMap != null) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Any>)
|
||||
val iterator = posts.keySetIterator()
|
||||
val userIds = mutableListOf<String>()
|
||||
val usernames = mutableListOf<String>()
|
||||
|
||||
val threads = WritableNativeArray()
|
||||
val threadParticipantUserIds = mutableListOf<String>() // Used to exclude the "userIds" present in the thread participants
|
||||
val threadParticipantUsernames = mutableListOf<String>() // Used to exclude the "usernames" present in the thread participants
|
||||
val threadParticipantUsers = HashMap<String, ReadableMap>() // All unique users from thread participants are stored here
|
||||
val userIdsAlreadyLoaded = mutableListOf<String>()
|
||||
if (loadedProfiles != null) {
|
||||
for (i in 0 until loadedProfiles.size()) {
|
||||
loadedProfiles.getMap(i).getString("id")?.let { userIdsAlreadyLoaded.add(it) }
|
||||
}
|
||||
}
|
||||
|
||||
while (iterator.hasNextKey()) {
|
||||
val key = iterator.nextKey()
|
||||
val post = posts.getMap(key)
|
||||
val userId = post?.getString("user_id")
|
||||
if (userId != null && userId != currentUserId && !userIdsAlreadyLoaded.contains(userId) && !userIds.contains(userId)) {
|
||||
userIds.add(userId)
|
||||
}
|
||||
val message = post?.getString("message")
|
||||
if (message != null) {
|
||||
val matchResults = regex.findAll(message)
|
||||
matchResults.iterator().forEach {
|
||||
val username = it.value.removePrefix("@")
|
||||
if (!usernames.contains(username) && currentUsername != username && !specialMentions.contains(username)) {
|
||||
usernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCRTEnabled) {
|
||||
// Add root post as a thread
|
||||
val threadId = post?.getString("root_id")
|
||||
if (threadId.isNullOrEmpty()) {
|
||||
post?.let {
|
||||
val thread = Arguments.createMap()
|
||||
thread.putString("id", it.getString("id"))
|
||||
thread.putInt("reply_count", it.getInt("reply_count"))
|
||||
thread.putDouble("last_reply_at", 0.0)
|
||||
thread.putDouble("last_viewed_at", 0.0)
|
||||
thread.putArray("participants", it.getArray("participants"))
|
||||
thread.putMap("post", it)
|
||||
thread.putBoolean("is_following", try {
|
||||
it.getBoolean("is_following")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
})
|
||||
thread.putInt("unread_replies", 0)
|
||||
thread.putInt("unread_mentions", 0)
|
||||
thread.putDouble("delete_at", it.getDouble("delete_at"))
|
||||
threads.pushMap(thread)
|
||||
}
|
||||
}
|
||||
|
||||
// Add participant userIds and usernames to exclude them from getting fetched again
|
||||
val participants = post?.getArray("participants")
|
||||
participants?.let {
|
||||
for (i in 0 until it.size()) {
|
||||
val participant = it.getMap(i)
|
||||
|
||||
val participantId = participant.getString("id")
|
||||
if (participantId != currentUserId && participantId != null) {
|
||||
if (!threadParticipantUserIds.contains(participantId) && !userIdsAlreadyLoaded.contains(participantId)) {
|
||||
threadParticipantUserIds.add(participantId)
|
||||
}
|
||||
|
||||
if (!threadParticipantUsers.containsKey(participantId)) {
|
||||
threadParticipantUsers[participantId] = participant
|
||||
}
|
||||
}
|
||||
|
||||
val username = participant.getString("username")
|
||||
if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) {
|
||||
threadParticipantUsernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val existingUserIds = queryIds(db, "User", userIds.toTypedArray())
|
||||
val existingUsernames = queryByColumn(db, "User", "username", usernames.toTypedArray())
|
||||
userIds.removeAll { it in existingUserIds }
|
||||
usernames.removeAll { it in existingUsernames }
|
||||
|
||||
if (threadParticipantUserIds.size > 0) {
|
||||
// Do not fetch users found in thread participants as we get the user's data in the posts response already
|
||||
userIds.removeAll { it in threadParticipantUserIds }
|
||||
usernames.removeAll { it in threadParticipantUsernames }
|
||||
|
||||
// Get users from thread participants
|
||||
val existingThreadParticipantUserIds = queryIds(db, "User", threadParticipantUserIds.toTypedArray())
|
||||
|
||||
// Exclude the thread participants already present in the DB from getting inserted again
|
||||
val usersFromThreads = WritableNativeArray()
|
||||
threadParticipantUsers.forEach { (userId, user) ->
|
||||
if (!existingThreadParticipantUserIds.contains(userId)) {
|
||||
usersFromThreads.pushMap(user)
|
||||
}
|
||||
}
|
||||
|
||||
if (usersFromThreads.size() > 0) {
|
||||
results.putArray("usersFromThreads", usersFromThreads)
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.size > 0) {
|
||||
results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray()))
|
||||
}
|
||||
|
||||
if (usernames.size > 0) {
|
||||
results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray()))
|
||||
}
|
||||
|
||||
if (threads.size() > 0) {
|
||||
results.putArray("threads", threads)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.findMyTeam
|
||||
import com.mattermost.helpers.database_extension.findTeam
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchTeamIfNeeded(db: Database, serverUrl: String, teamId: String): Pair<ReadableMap?, ReadableMap?> {
|
||||
return try {
|
||||
var team: ReadableMap? = null
|
||||
var myTeam: ReadableMap? = null
|
||||
val teamExists = findTeam(db, teamId)
|
||||
val myTeamExists = findMyTeam(db, teamId)
|
||||
if (!teamExists) {
|
||||
team = fetch(serverUrl, "/api/v4/teams/$teamId")
|
||||
}
|
||||
|
||||
if (!myTeamExists) {
|
||||
myTeam = fetch(serverUrl, "/api/v4/teams/$teamId/members/me")
|
||||
}
|
||||
|
||||
Pair(team, myTeam)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(null, null)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.*
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchThread(db: Database, serverUrl: String, threadId: String, teamId: String?): ReadableMap? {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return null
|
||||
val threadTeamId = (if (teamId.isNullOrEmpty()) queryCurrentTeamId(db) else teamId) ?: return null
|
||||
|
||||
return try {
|
||||
val thread = fetch(serverUrl, "/api/v4/users/$currentUserId/teams/${threadTeamId}/threads/$threadId")
|
||||
thread?.getMap("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.ReadableArrayUtils
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchUsersById(serverUrl: String, userIds: ReadableArray): ReadableArray? {
|
||||
return try {
|
||||
val endpoint = "api/v4/users/ids"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds)))
|
||||
val result = fetchWithPost(serverUrl, endpoint, options)
|
||||
result?.getArray("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableArray? {
|
||||
return try {
|
||||
val endpoint = "api/v4/users/usernames"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames)))
|
||||
val result = fetchWithPost(serverUrl, endpoint, options)
|
||||
result?.getArray("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchNeededUsers(serverUrl: String, loadedUsers: ReadableArray?, data: ReadableMap?): ArrayList<Any> {
|
||||
val userList = ArrayList<Any>()
|
||||
loadedUsers?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
data?.getArray("userIdsToLoad")?.let { ids ->
|
||||
if (ids.size() > 0) {
|
||||
val result = fetchUsersById(serverUrl, ids)
|
||||
result?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
}
|
||||
}
|
||||
|
||||
data?.getArray("usernamesToLoad")?.let { ids ->
|
||||
if (ids.size() > 0) {
|
||||
val result = fetchUsersByUsernames(serverUrl, ids)
|
||||
result?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
}
|
||||
}
|
||||
|
||||
data?.getArray("usersFromThreads")?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
|
||||
return userList
|
||||
}
|
||||
|
||||
internal fun PushNotificationDataRunnable.Companion.addUsersToList(users: ReadableArray, list: ArrayList<Any>) {
|
||||
for (i in 0 until users.size()) {
|
||||
list.add(users.getMap(i))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,34 +7,30 @@ import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.mattermost.helpers.CustomPushNotificationHelper;
|
||||
import com.mattermost.helpers.DatabaseHelper;
|
||||
import com.mattermost.helpers.Network;
|
||||
import com.mattermost.helpers.NotificationHelper;
|
||||
import com.mattermost.helpers.PushNotificationDataHelper;
|
||||
import com.mattermost.helpers.ReadableMapUtils;
|
||||
import com.mattermost.share.ShareModule;
|
||||
import com.mattermost.helpers.ResolvePromise;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
|
||||
import static com.mattermost.helpers.database_extension.GeneralKt.*;
|
||||
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
|
||||
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
private final PushNotificationDataHelper dataHelper;
|
||||
|
||||
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,57 +53,45 @@ public class CustomPushNotification extends PushNotification {
|
||||
int notificationId = NotificationHelper.getNotificationId(initialData);
|
||||
|
||||
String serverUrl = addServerUrlToBundle(initialData);
|
||||
boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
|
||||
|
||||
if (ackId != null && serverUrl != null) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
current.putAll(response);
|
||||
mNotificationProps = createProps(current);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message) {
|
||||
Log.e("ReactNative", code + ": " + message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
finishProcessingNotification(serverUrl, type, channelId, notificationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpened() {
|
||||
if (mNotificationProps != null) {
|
||||
digestNotification();
|
||||
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishProcessingNotification(final String serverUrl, @NonNull final String type, final String channelId, final int notificationId) {
|
||||
final boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
|
||||
|
||||
switch (type) {
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_SESSION:
|
||||
ShareModule shareModule = ShareModule.getInstance();
|
||||
String currentActivityName = shareModule != null ? shareModule.getCurrentActivityName() : "";
|
||||
Log.i("ReactNative", currentActivityName);
|
||||
if (!mAppLifecycleFacade.isAppVisible() || !currentActivityName.equals("MainActivity")) {
|
||||
if (!mAppLifecycleFacade.isAppVisible()) {
|
||||
boolean createSummary = type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE);
|
||||
if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) {
|
||||
if (channelId != null) {
|
||||
Bundle notificationBundle = mNotificationProps.asBundle();
|
||||
if (serverUrl != null) {
|
||||
if (serverUrl != null && !isReactInit) {
|
||||
// We will only fetch the data related to the notification on the native side
|
||||
// as updating the data directly to the db removes the wal & shm files needed
|
||||
// by watermelonDB, if the DB is updated while WDB is running it causes WDB to
|
||||
// detect the database as malformed, thus the app stop working and a restart is required.
|
||||
// Data will be fetch from within the JS context instead.
|
||||
Bundle notificationResult = dataHelper.fetchAndStoreDataForPushNotification(notificationBundle, isReactInit);
|
||||
if (notificationResult != null) {
|
||||
notificationBundle.putBundle("data", notificationResult);
|
||||
mNotificationProps = createProps(notificationBundle);
|
||||
}
|
||||
dataHelper.fetchAndStoreDataForPushNotification(notificationBundle);
|
||||
}
|
||||
createSummary = NotificationHelper.addNotificationToPreferences(
|
||||
mContext,
|
||||
@@ -130,6 +114,16 @@ 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);
|
||||
@@ -151,25 +145,26 @@ 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());
|
||||
}
|
||||
|
||||
private String addServerUrlToBundle(Bundle bundle) {
|
||||
DatabaseHelper dbHelper = DatabaseHelper.Companion.getInstance();
|
||||
String serverId = bundle.getString("server_id");
|
||||
String serverUrl = null;
|
||||
if (dbHelper != null) {
|
||||
if (serverId == null) {
|
||||
serverUrl = dbHelper.getOnlyServerUrl();
|
||||
} else {
|
||||
serverUrl = getServerUrlForIdentifier(dbHelper, serverId);
|
||||
}
|
||||
String serverUrl;
|
||||
if (serverId == null) {
|
||||
serverUrl = Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).getOnlyServerUrl();
|
||||
} else {
|
||||
serverUrl = Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).getServerUrlForIdentifier(serverId);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(serverUrl)) {
|
||||
bundle.putString("server_url", serverUrl);
|
||||
mNotificationProps = createProps(bundle);
|
||||
}
|
||||
if (!TextUtils.isEmpty(serverUrl)) {
|
||||
bundle.putString("server_url", serverUrl);
|
||||
mNotificationProps = createProps(bundle);
|
||||
}
|
||||
|
||||
return serverUrl;
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
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>
|
||||
|
||||
fun onCreate() {
|
||||
observable = WindowInfoTracker.getOrCreate(activity)
|
||||
.windowLayoutInfoObservable(activity)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,69 +1,53 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
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;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class MainActivity extends NavigationActivity {
|
||||
private boolean HWKeyboardConnected = false;
|
||||
private final FoldableObserver foldableObserver = new FoldableObserver(this);
|
||||
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "Mattermost";
|
||||
}
|
||||
public static class MainActivityDelegate extends ReactActivityDelegate {
|
||||
public MainActivityDelegate(NavigationActivity activity, String mainComponentName) {
|
||||
super(activity, mainComponentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
Objects.requireNonNull(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
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(null);
|
||||
super.onCreate(savedInstanceState);
|
||||
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
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
|
||||
@@ -101,7 +85,7 @@ public class MainActivity extends NavigationActivity {
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
private void setHWKeyboardConnected() {
|
||||
HWKeyboardConnected = getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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;
|
||||
@@ -11,23 +12,23 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
import com.mattermost.share.ShareModule;
|
||||
import com.wix.reactnativenotifications.RNNotificationsPackage;
|
||||
|
||||
import com.reactnativenavigation.NavigationApplication;
|
||||
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
|
||||
import com.wix.reactnativenotifications.core.notification.IPushNotification;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.INotificationsDrawerApplication;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
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.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost;
|
||||
import com.facebook.react.config.ReactFeatureFlags;
|
||||
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,16 +37,17 @@ 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 DefaultReactNativeHost(this) {
|
||||
new ReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
@@ -66,14 +68,10 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
switch (name) {
|
||||
case "MattermostManaged":
|
||||
return MattermostManagedModule.getInstance(reactContext);
|
||||
case "MattermostShare":
|
||||
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);
|
||||
case "Notifications":
|
||||
return NotificationsModule.getInstance(instance, reactContext);
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not find module " + name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +80,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
return () -> {
|
||||
Map<String, ReactModuleInfo> map = new HashMap<>();
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -108,26 +104,30 @@ 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() {
|
||||
return mReactNativeHost;
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
return mNewArchitectureNativeHost;
|
||||
} else {
|
||||
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
|
||||
@@ -139,13 +139,6 @@ 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
|
||||
@@ -158,4 +151,26 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -21,21 +20,16 @@ import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
import com.mattermost.helpers.Credentials;
|
||||
import com.reactlibrary.createthumbnail.CreateThumbnailModule;
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.Objects;
|
||||
|
||||
public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
private static final String SAVE_EVENT = "MattermostManagedSaveFile";
|
||||
@@ -46,6 +40,8 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
private Promise mPickerPromise;
|
||||
private String fileContent;
|
||||
|
||||
private static final String TAG = MattermostManagedModule.class.getSimpleName();
|
||||
|
||||
private MattermostManagedModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
@@ -131,6 +127,19 @@ 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;
|
||||
@@ -147,7 +156,7 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
try {
|
||||
final String packageName = currentActivity.getPackageName();
|
||||
final String authority = packageName + ".provider";
|
||||
final String authority = new StringBuilder(packageName).append(".provider").toString();
|
||||
contentUri = FileProvider.getUriForFile(currentActivity, authority, newFile);
|
||||
}
|
||||
catch(IllegalArgumentException e) {
|
||||
@@ -174,7 +183,7 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
intent.setType(mimeType);
|
||||
intent.putExtra(Intent.EXTRA_TITLE, filename);
|
||||
|
||||
PackageManager pm = Objects.requireNonNull(getCurrentActivity()).getPackageManager();
|
||||
PackageManager pm = getCurrentActivity().getPackageManager();
|
||||
if (intent.resolveActivity(pm) != null) {
|
||||
try {
|
||||
getCurrentActivity().startActivityForResult(intent, SAVE_REQUEST);
|
||||
@@ -197,30 +206,6 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void createThumbnail(ReadableMap options, Promise promise) {
|
||||
try {
|
||||
WritableMap optionsMap = Arguments.createMap();
|
||||
optionsMap.merge(options);
|
||||
String url = options.hasKey("url") ? options.getString("url") : "";
|
||||
URL videoUrl = new URL(url);
|
||||
String serverUrl = videoUrl.getProtocol() + "://" + videoUrl.getHost() + ":" + videoUrl.getPort();
|
||||
String token = Credentials.getCredentialsForServerSync(this.reactContext, serverUrl);
|
||||
if (!TextUtils.isEmpty(token)) {
|
||||
WritableMap headers = Arguments.createMap();
|
||||
if (optionsMap.hasKey("headers")) {
|
||||
headers.merge(Objects.requireNonNull(optionsMap.getMap("headers")));
|
||||
}
|
||||
headers.putString("Authorization", "Bearer " + token);
|
||||
optionsMap.putMap("headers", headers);
|
||||
}
|
||||
CreateThumbnailModule thumb = new CreateThumbnailModule(this.reactContext);
|
||||
thumb.create(optionsMap.copy(), promise);
|
||||
} catch (Exception e) {
|
||||
promise.reject("CreateThumbnail_ERROR", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SaveDataTask extends GuardedResultAsyncTask<Object> {
|
||||
private final WeakReference<Context> weakContext;
|
||||
private final String fromFile;
|
||||
@@ -235,21 +220,34 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
|
||||
@Override
|
||||
protected Object doInBackgroundGuarded() {
|
||||
FileChannel source = null;
|
||||
FileChannel dest = null;
|
||||
try {
|
||||
ParcelFileDescriptor pfd = weakContext.get().getContentResolver().openFileDescriptor(toFile, "w");
|
||||
File input = new File(this.fromFile);
|
||||
try (FileInputStream fileInputStream = new FileInputStream(input)) {
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor())) {
|
||||
FileChannel source = fileInputStream.getChannel();
|
||||
FileChannel dest = fileOutputStream.getChannel();
|
||||
dest.transferFrom(source, 0, source.size());
|
||||
source.close();
|
||||
dest.close();
|
||||
}
|
||||
}
|
||||
pfd.close();
|
||||
FileInputStream fileInputStream = new FileInputStream(input);
|
||||
ParcelFileDescriptor pfd = weakContext.get().getContentResolver().openFileDescriptor(toFile, "w");
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
|
||||
source = fileInputStream.getChannel();
|
||||
dest = fileOutputStream.getChannel();
|
||||
dest.transferFrom(source, 0, source.size());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (source != null) {
|
||||
try {
|
||||
source.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (dest != null) {
|
||||
try {
|
||||
dest.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -8,16 +8,28 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.Person;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
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.mattermost.helpers.*;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
|
||||
|
||||
@@ -41,7 +53,12 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
final int notificationId = intent.getIntExtra(CustomPushNotificationHelper.NOTIFICATION_ID, -1);
|
||||
final String serverUrl = bundle.getString("server_url");
|
||||
if (serverUrl != null) {
|
||||
replyToMessage(serverUrl, notificationId, message);
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
|
||||
|
||||
if (token != null) {
|
||||
replyToMessage(serverUrl, token, notificationId, message);
|
||||
}
|
||||
} else {
|
||||
onReplyFailed(notificationId);
|
||||
}
|
||||
@@ -50,7 +67,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
protected void replyToMessage(final String serverUrl, final int notificationId, final CharSequence message) {
|
||||
protected void replyToMessage(final String serverUrl, final String token, 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");
|
||||
@@ -58,53 +75,63 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
rootId = postId;
|
||||
}
|
||||
|
||||
if (serverUrl == null) {
|
||||
if (token == null || serverUrl == null) {
|
||||
onReplyFailed(notificationId);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
String postsEndpoint = "/api/v4/posts?set_online=false";
|
||||
Network.post(serverUrl, postsEndpoint, options, new ResolvePromise() {
|
||||
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() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (value != null) {
|
||||
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||
Log.i("ReactNative", String.format("Reply FAILED exception %s", e.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", "Reply FAILED resolved without value");
|
||||
Log.i("ReactNative",
|
||||
String.format("Reply FAILED status %s BODY %s",
|
||||
response.code(),
|
||||
Objects.requireNonNull(response.body()).string()
|
||||
)
|
||||
);
|
||||
onReplyFailed(notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(Throwable reason) {
|
||||
Log.i("ReactNative", String.format("Reply FAILED exception %s", reason.getMessage()));
|
||||
onReplyFailed(notificationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
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.");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
@@ -1,60 +1,135 @@
|
||||
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.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.mattermost.helpers.*;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public class ReceiptDelivery {
|
||||
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"};
|
||||
private static final int[] FIBONACCI_BACKOFF = new int[] { 0, 1, 2, 3, 5, 8 };
|
||||
|
||||
public static Bundle send(final String ackId, final String serverUrl, final String postId, final String type, final boolean isIdLoaded) {
|
||||
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);
|
||||
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with ID-LOADED=%s", ackId, type, serverUrl, isIdLoaded));
|
||||
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);
|
||||
execute(serverUrl, postId, token, ackId, type, isIdLoaded, promise);
|
||||
}
|
||||
|
||||
try (Response response = Network.postSync(serverUrl, "api/v4/notifications/ack", options)) {
|
||||
String responseBody = Objects.requireNonNull(response.body()).string();
|
||||
JSONObject jsonResponse = new JSONObject(responseBody);
|
||||
return parseAckResponse(jsonResponse);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Bundle parseAckResponse(JSONObject jsonResponse) {
|
||||
private static void makeServerRequest(OkHttpClient client, Request request, Boolean isIdLoaded, int reRequestCount, ResolvePromise promise) {
|
||||
try {
|
||||
Response response = client.newCall(request).execute();
|
||||
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);
|
||||
Bundle bundle = new Bundle();
|
||||
for (String key : ackKeys) {
|
||||
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) {
|
||||
if (jsonResponse.has(key)) {
|
||||
bundle.putString(key, jsonResponse.getString(key));
|
||||
}
|
||||
}
|
||||
return bundle;
|
||||
promise.resolve(bundle);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
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"
|
||||
|
||||
private fun sendEvent(params: WritableMap?) {
|
||||
reactContext
|
||||
.getJSModule(RCTDeviceEventEmitter::class.java)
|
||||
.emit("SplitViewChanged", 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(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
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.mattermost.rnbeta.MainApplication;
|
||||
|
||||
public class ShareActivity extends ReactActivity {
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "MattermostShare";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
MainApplication app = (MainApplication) this.getApplication();
|
||||
app.sharedExtensionIsOpened = true;
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.mattermost.helpers.Credentials;
|
||||
import com.mattermost.rnbeta.MainApplication;
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class ShareModule extends ReactContextBaseJavaModule {
|
||||
private final OkHttpClient client = new OkHttpClient();
|
||||
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
private static ShareModule instance;
|
||||
private final MainApplication mApplication;
|
||||
private ReactApplicationContext mReactContext;
|
||||
private File tempFolder;
|
||||
|
||||
private ShareModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mReactContext = reactContext;
|
||||
mApplication = (MainApplication)reactContext.getApplicationContext();
|
||||
}
|
||||
|
||||
public static ShareModule getInstance(ReactApplicationContext reactContext) {
|
||||
if (instance == null) {
|
||||
instance = new ShareModule(reactContext);
|
||||
} else {
|
||||
instance.mReactContext = reactContext;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static ShareModule getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "MattermostShare";
|
||||
}
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
public String getCurrentActivityName() {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity != null) {
|
||||
String activityName = currentActivity.getComponentName().getClassName();
|
||||
String[] components = activityName.split("\\.");
|
||||
return components[components.length - 1];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void clear() {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity != null && this.getCurrentActivityName().equals("ShareActivity")) {
|
||||
Intent intent = currentActivity.getIntent();
|
||||
intent.setAction("");
|
||||
intent.removeExtra(Intent.EXTRA_TEXT);
|
||||
intent.removeExtra(Intent.EXTRA_STREAM);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
HashMap<String, Object> constants = new HashMap<>(1);
|
||||
constants.put("cacheDirName", RealPathUtil.CACHE_DIR_NAME);
|
||||
constants.put("isOpened", mApplication.sharedExtensionIsOpened);
|
||||
return constants;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void close(ReadableMap data) {
|
||||
this.clear();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity == null || !this.getCurrentActivityName().equals("ShareActivity")) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentActivity.finishAndRemoveTask();
|
||||
if (data != null && data.hasKey("serverUrl")) {
|
||||
ReadableArray files = data.getArray("files");
|
||||
String serverUrl = data.getString("serverUrl");
|
||||
final String token = Credentials.getCredentialsForServerSync(mReactContext, serverUrl);
|
||||
JSONObject postData = buildPostObject(data);
|
||||
|
||||
if (files != null && files.size() > 0) {
|
||||
uploadFiles(serverUrl, token, files, postData);
|
||||
} else {
|
||||
try {
|
||||
post(serverUrl, token, postData);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mApplication.sharedExtensionIsOpened = false;
|
||||
RealPathUtil.deleteTempFiles(this.tempFolder);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getSharedData(Promise promise) {
|
||||
promise.resolve(processIntent());
|
||||
}
|
||||
|
||||
public WritableArray processIntent() {
|
||||
String type, action, extra;
|
||||
WritableArray items = Arguments.createArray();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (currentActivity != null) {
|
||||
this.tempFolder = new File(currentActivity.getCacheDir(), RealPathUtil.CACHE_DIR_NAME);
|
||||
Intent intent = currentActivity.getIntent();
|
||||
action = intent.getAction();
|
||||
type = intent.getType();
|
||||
extra = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type) && extra != null) {
|
||||
items.pushMap(ShareUtils.getTextItem(extra));
|
||||
} else if (Intent.ACTION_SEND.equals(action)) {
|
||||
if (extra != null) {
|
||||
items.pushMap(ShareUtils.getTextItem(extra));
|
||||
}
|
||||
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
if (uri != null) {
|
||||
ReadableMap fileInfo = ShareUtils.getFileItem(currentActivity, uri);
|
||||
if (fileInfo != null) {
|
||||
items.pushMap(fileInfo);
|
||||
}
|
||||
}
|
||||
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||
if (extra != null) {
|
||||
items.pushMap(ShareUtils.getTextItem(extra));
|
||||
}
|
||||
|
||||
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
for (Uri uri : uris) {
|
||||
ReadableMap fileInfo = ShareUtils.getFileItem(currentActivity, uri);
|
||||
if (fileInfo != null) {
|
||||
items.pushMap(fileInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private JSONObject buildPostObject(ReadableMap data) {
|
||||
JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("user_id", data.getString("userId"));
|
||||
if (data.hasKey("channelId")) {
|
||||
json.put("channel_id", data.getString("channelId"));
|
||||
}
|
||||
if (data.hasKey("message")) {
|
||||
json.put("message", data.getString("message"));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private void post(String serverUrl, String token, JSONObject postData) throws IOException {
|
||||
RequestBody body = RequestBody.create(postData.toString(), JSON);
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", "BEARER " + token)
|
||||
.url(serverUrl + "/api/v4/posts")
|
||||
.post(body)
|
||||
.build();
|
||||
client.newCall(request).execute();
|
||||
}
|
||||
|
||||
private void uploadFiles(String serverUrl, String token, ReadableArray files, JSONObject postData) {
|
||||
try {
|
||||
MultipartBody.Builder builder = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM);
|
||||
|
||||
for(int i = 0 ; i < files.size() ; i++) {
|
||||
ReadableMap file = files.getMap(i);
|
||||
String mime = file.getString("type");
|
||||
String fullPath = file.getString("value");
|
||||
if (fullPath != null) {
|
||||
String filePath = fullPath.replaceFirst("file://", "");
|
||||
File fileInfo = new File(filePath);
|
||||
if (fileInfo.exists() && mime != null) {
|
||||
final MediaType MEDIA_TYPE = MediaType.parse(mime);
|
||||
builder.addFormDataPart("files", file.getString("filename"), RequestBody.create(fileInfo, MEDIA_TYPE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.addFormDataPart("channel_id", postData.getString("channel_id"));
|
||||
RequestBody body = builder.build();
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", "BEARER " + token)
|
||||
.url(serverUrl + "/api/v4/files")
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (response.isSuccessful()) {
|
||||
String responseData = Objects.requireNonNull(response.body()).string();
|
||||
JSONObject responseJson = new JSONObject(responseData);
|
||||
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
|
||||
JSONArray file_ids = new JSONArray();
|
||||
for(int i = 0 ; i < fileInfoArray.length() ; i++) {
|
||||
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
|
||||
file_ids.put(fileInfo.getString("id"));
|
||||
}
|
||||
postData.put("file_ids", file_ids);
|
||||
post(serverUrl, token, postData);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ShareUtils {
|
||||
public static ReadableMap getTextItem(String text) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putString("value", text);
|
||||
map.putString("type", "");
|
||||
map.putBoolean("isString", true);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static ReadableMap getFileItem(Activity activity, Uri uri) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
String filePath = RealPathUtil.getRealPathFromURI(activity, uri);
|
||||
if (filePath == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File file = new File(filePath);
|
||||
String type = RealPathUtil.getMimeTypeFromUri(activity, uri);
|
||||
if (type != null) {
|
||||
if (type.startsWith("image/")) {
|
||||
BitmapFactory.Options bitMapOption = ShareUtils.getImageDimensions(filePath);
|
||||
map.putInt("height", bitMapOption.outHeight);
|
||||
map.putInt("width", bitMapOption.outWidth);
|
||||
|
||||
} else if (type.startsWith("video/")) {
|
||||
File cacheDir = new File(activity.getCacheDir(), RealPathUtil.CACHE_DIR_NAME);
|
||||
addVideoThumbnailToMap(cacheDir, activity.getApplicationContext(), map, "file://" + filePath);
|
||||
}
|
||||
} else {
|
||||
type = "application/octet-stream";
|
||||
}
|
||||
|
||||
map.putString("value", "file://" + filePath);
|
||||
map.putDouble("size", (double) file.length());
|
||||
map.putString("filename", file.getName());
|
||||
map.putString("type", type);
|
||||
map.putString("extension", RealPathUtil.getExtension(filePath).replaceFirst(".", ""));
|
||||
map.putBoolean("isString", false);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static BitmapFactory.Options getImageDimensions(String filePath) {
|
||||
BitmapFactory.Options bitMapOption = new BitmapFactory.Options();
|
||||
bitMapOption.inJustDecodeBounds=true;
|
||||
BitmapFactory.decodeFile(filePath, bitMapOption);
|
||||
return bitMapOption;
|
||||
}
|
||||
|
||||
private static void addVideoThumbnailToMap(File cacheDir, Context context, WritableMap map, String filePath) {
|
||||
String fileName = ("thumb-" + UUID.randomUUID().toString()) + ".png";
|
||||
OutputStream fOut = null;
|
||||
|
||||
try {
|
||||
File file = new File(cacheDir, fileName);
|
||||
Bitmap image = getBitmapAtTime(context, filePath, 1);
|
||||
if (file.createNewFile()) {
|
||||
fOut = new FileOutputStream(file);
|
||||
image.compress(Bitmap.CompressFormat.PNG, 100, fOut);
|
||||
fOut.flush();
|
||||
fOut.close();
|
||||
|
||||
map.putString("videoThumb", "file://" + file.getAbsolutePath());
|
||||
map.putInt("width", image.getWidth());
|
||||
map.putInt("height", image.getHeight());
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap getBitmapAtTime(Context context, String filePath, int time) {
|
||||
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));
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
7
android/app/src/main/jni/CMakeLists.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
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)
|
||||
32
android/app/src/main/jni/MainApplicationModuleProvider.cpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#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 ¶ms) {
|
||||
// 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
|
||||
16
android/app/src/main/jni/MainApplicationModuleProvider.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#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 ¶ms);
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
@@ -0,0 +1,45 @@
|
||||
#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 ¶ms) {
|
||||
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
|
||||
@@ -0,0 +1,38 @@
|
||||
#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 ¶ms) 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
|
||||
65
android/app/src/main/jni/MainComponentsRegistry.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
#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
|
||||
32
android/app/src/main/jni/MainComponentsRegistry.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#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
|
||||
11
android/app/src/main/jni/OnLoad.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#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();
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 3.0 MiB |
@@ -2,5 +2,5 @@
|
||||
<resources>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
<color name="splashscreen_bg">#090A0B</color>
|
||||
<color name="splashscreen_bg">#1E325C</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<files-path name="files" path="." />
|
||||
<external-files-path name="external_files" path="." />
|
||||
<external-path name="external" path="." />
|
||||
<cache-path name="cache" path="." />
|
||||
<root-path name="root" path="." />
|
||||
</paths>
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,22 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "33.0.0"
|
||||
buildToolsVersion = "31.0.0"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
supportLibVersion = "33.0.0"
|
||||
kotlinVersion = "1.7.21"
|
||||
kotlin_version = "1.7.21"
|
||||
firebaseVersion = "23.1.1"
|
||||
compileSdkVersion = 31
|
||||
targetSdkVersion = 31
|
||||
supportLibVersion = "31.0.0"
|
||||
kotlinVersion = "1.5.30"
|
||||
kotlin_version = "1.5.30"
|
||||
firebaseVersion = "21.0.0"
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
|
||||
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
|
||||
ndkVersion = "23.1.7779620"
|
||||
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"
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -21,9 +25,10 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.3.1")
|
||||
classpath("com.android.tools.build:gradle:7.2.1")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath('com.google.gms:google-services:4.3.15')
|
||||
classpath("de.undercouch:gradle-download-task:5.0.1")
|
||||
classpath('com.google.gms:google-services:4.3.10')
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
@@ -33,9 +38,50 @@ 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.177.0
|
||||
FLIPPER_VERSION=0.125.0
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
@@ -40,8 +40,4 @@ 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
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
newArchEnabled=false
|
||||
@@ -1,12 +1,17 @@
|
||||
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)
|
||||
include ':react-native-video'
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
|
||||
include ':watermelondb'
|
||||
project(':watermelondb').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android')
|
||||
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')
|
||||
|
||||
@@ -1,52 +1,41 @@
|
||||
// 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';
|
||||
|
||||
export const storeGlobal = async (id: string, value: unknown, prepareRecordsOnly = false) => {
|
||||
export const storeDeviceToken = async (token: string, prepareRecordsOnly = false) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
return operator.handleGlobal({
|
||||
globals: [{id, value}],
|
||||
globals: [{id: GLOBAL_IDENTIFIERS.DEVICE_TOKEN, value: token}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('storeGlobal', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const storeDeviceToken = async (token: string, prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.DEVICE_TOKEN, token, 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);
|
||||
try {
|
||||
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
return operator.handleGlobal({
|
||||
globals: [{id: GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, value: 'true'}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const storeProfileLongPressTutorial = async (prepareRecordsOnly = false) => {
|
||||
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) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.DONT_ASK_FOR_REVIEW, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeLastAskForReview = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.LAST_ASK_FOR_REVIEW, Date.now(), prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeFirstLaunch = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.FIRST_LAUNCH, Date.now(), prepareRecordsOnly);
|
||||
try {
|
||||
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
return operator.handleGlobal({
|
||||
globals: [{id: GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, value: 'true'}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// 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';
|
||||
import {prepareCategories, prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById} from '@queries/servers/categories';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {queryMyTeams} from '@queries/servers/team';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {pluckUnique} from '@utils/helpers';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
@@ -31,18 +34,48 @@ export const deleteCategory = async (serverUrl: string, categoryId: string) => {
|
||||
|
||||
export async function storeCategories(serverUrl: string, categories: CategoryWithChannels[], prune = false, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const models = await prepareCategoriesAndCategoriesChannels(operator, categories, prune);
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
const preparedCategories = prepareCategories(operator, categories);
|
||||
if (preparedCategories) {
|
||||
modelPromises.push(preparedCategories);
|
||||
}
|
||||
|
||||
const preparedCategoryChannels = prepareCategoryChannels(operator, categories);
|
||||
if (preparedCategoryChannels) {
|
||||
modelPromises.push(preparedCategoryChannels);
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
const flattenedModels = models.flat();
|
||||
|
||||
if (prune && categories.length) {
|
||||
const remoteCategoryIds = new Set(categories.map((cat) => cat.id));
|
||||
|
||||
// If the passed categories have more than one team, we want to update across teams
|
||||
const teamIds = pluckUnique('team_id')(categories) as string[];
|
||||
const localCategories = await queryCategoriesByTeamIds(database, teamIds).fetch();
|
||||
|
||||
localCategories.
|
||||
filter((category) => category.type === 'custom').
|
||||
forEach((localCategory) => {
|
||||
if (!remoteCategoryIds.has(localCategory.id)) {
|
||||
localCategory.prepareDestroyPermanently();
|
||||
flattenedModels.push(localCategory);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (prepareRecordsOnly) {
|
||||
return {models};
|
||||
return {models: flattenedModels};
|
||||
}
|
||||
|
||||
if (models.length > 0) {
|
||||
await operator.batchRecords(models, 'storeCategories');
|
||||
if (flattenedModels?.length > 0) {
|
||||
await operator.batchRecords(flattenedModels);
|
||||
}
|
||||
|
||||
return {models};
|
||||
return {models: flattenedModels};
|
||||
} catch (error) {
|
||||
logError('FAILED TO STORE CATEGORIES', error);
|
||||
return {error};
|
||||
@@ -79,6 +112,7 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
|
||||
return {error: 'no current user id'};
|
||||
}
|
||||
|
||||
const models: Model[] = [];
|
||||
const categoriesWithChannels: CategoryWithChannels[] = [];
|
||||
|
||||
if (isDMorGM(channel)) {
|
||||
@@ -98,12 +132,13 @@ 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, 'addChannelToDefaultCategory');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// 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';
|
||||
@@ -13,9 +15,6 @@ 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();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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';
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
prepareDeleteChannel, prepareMyChannelsForTeam, queryAllMyChannel,
|
||||
getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes,
|
||||
} from '@queries/servers/channel';
|
||||
import {queryDisplayNamePreferences} from '@queries/servers/preference';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, removeChannelFromTeamHistory} from '@queries/servers/team';
|
||||
import {getCurrentUser, queryUsersById} from '@queries/servers/user';
|
||||
@@ -23,7 +24,6 @@ import {isTablet} from '@utils/helpers';
|
||||
import {logError, logInfo} from '@utils/log';
|
||||
import {displayGroupMessageName, displayUsername, getUserIdFromChannelName} from '@utils/user';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
@@ -76,13 +76,13 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
|
||||
}
|
||||
|
||||
models = (await Promise.all(modelPromises)).flat();
|
||||
const {member: viewedAt} = await markChannelAsViewed(serverUrl, channelId, false, true);
|
||||
const {member: viewedAt} = await markChannelAsViewed(serverUrl, channelId, true);
|
||||
if (viewedAt) {
|
||||
models.push(viewedAt);
|
||||
}
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
await operator.batchRecords(models, 'switchToChannel');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
if (isTabletDevice) {
|
||||
@@ -124,7 +124,7 @@ export async function removeCurrentUserFromChannel(serverUrl: string, channelId:
|
||||
await removeChannelFromTeamHistory(operator, teamId, channel.id, false);
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
await operator.batchRecords(models, 'removeCurrentUserFromChannel');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
}
|
||||
return {models};
|
||||
@@ -145,7 +145,7 @@ export async function setChannelDeleteAt(serverUrl: string, channelId: string, d
|
||||
const model = channel.prepareUpdate((c) => {
|
||||
c.deleteAt = deleteAt;
|
||||
});
|
||||
await operator.batchRecords([model], 'setChannelDeleteAt');
|
||||
await operator.batchRecords([model]);
|
||||
} catch (error) {
|
||||
logError('FAILED TO BATCH CHANGES FOR CHANNEL DELETE AT', error);
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export async function selectAllMyChannelIds(serverUrl: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function markChannelAsViewed(serverUrl: string, channelId: string, onlyCounts = false, prepareRecordsOnly = false) {
|
||||
export async function markChannelAsViewed(serverUrl: string, channelId: string, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const member = await getMyChannel(database, channelId);
|
||||
@@ -172,14 +172,12 @@ export async function markChannelAsViewed(serverUrl: string, channelId: string,
|
||||
m.isUnread = false;
|
||||
m.mentionsCount = 0;
|
||||
m.manuallyUnread = false;
|
||||
if (!onlyCounts) {
|
||||
m.viewedAt = member.lastViewedAt;
|
||||
m.lastViewedAt = Date.now();
|
||||
}
|
||||
m.viewedAt = member.lastViewedAt;
|
||||
m.lastViewedAt = Date.now();
|
||||
});
|
||||
PushNotifications.removeChannelNotifications(serverUrl, channelId);
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([member], 'markChannelAsViewed');
|
||||
await operator.batchRecords([member]);
|
||||
}
|
||||
|
||||
return {member};
|
||||
@@ -206,7 +204,7 @@ export async function markChannelAsUnread(serverUrl: string, channelId: string,
|
||||
m.isUnread = true;
|
||||
});
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([member], 'markChannelAsUnread');
|
||||
await operator.batchRecords([member]);
|
||||
}
|
||||
|
||||
return {member};
|
||||
@@ -226,7 +224,7 @@ export async function resetMessageCount(serverUrl: string, channelId: string) {
|
||||
member.prepareUpdate((m) => {
|
||||
m.messageCount = 0;
|
||||
});
|
||||
await operator.batchRecords([member], 'resetMessageCount');
|
||||
await operator.batchRecords([member]);
|
||||
|
||||
return member;
|
||||
} catch (error) {
|
||||
@@ -254,7 +252,7 @@ export async function storeMyChannelsForTeam(serverUrl: string, teamId: string,
|
||||
}
|
||||
|
||||
if (flattenedModels.length) {
|
||||
await operator.batchRecords(flattenedModels, 'storeMyChannelsForTeam');
|
||||
await operator.batchRecords(flattenedModels);
|
||||
}
|
||||
|
||||
return {models: flattenedModels};
|
||||
@@ -273,7 +271,7 @@ export async function updateMyChannelFromWebsocket(serverUrl: string, channelMem
|
||||
m.roles = channelMember.roles;
|
||||
});
|
||||
if (!prepareRecordsOnly) {
|
||||
operator.batchRecords([member], 'updateMyChannelFromWebsocket');
|
||||
operator.batchRecords([member]);
|
||||
}
|
||||
}
|
||||
return {model: member};
|
||||
@@ -293,7 +291,7 @@ export async function updateChannelInfoFromChannel(serverUrl: string, channel: C
|
||||
}],
|
||||
prepareRecordsOnly: true});
|
||||
if (!prepareRecordsOnly) {
|
||||
operator.batchRecords(newInfo, 'updateChannelInfoFromChannel');
|
||||
operator.batchRecords(newInfo);
|
||||
}
|
||||
return {model: newInfo};
|
||||
} catch (error) {
|
||||
@@ -317,7 +315,7 @@ export async function updateLastPostAt(serverUrl: string, channelId: string, las
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([myChannel], 'updateLastPostAt');
|
||||
await operator.batchRecords([myChannel]);
|
||||
}
|
||||
|
||||
return {member: myChannel};
|
||||
@@ -345,7 +343,7 @@ export async function updateMyChannelLastFetchedAt(serverUrl: string, channelId:
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([myChannel], 'updateMyChannelLastFetchedAt');
|
||||
await operator.batchRecords([myChannel]);
|
||||
}
|
||||
|
||||
return {member: myChannel};
|
||||
@@ -369,7 +367,7 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha
|
||||
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const displaySettings = getTeammateNameDisplaySetting(preferences, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const models: Model[] = [];
|
||||
for await (const channel of channels) {
|
||||
@@ -403,7 +401,7 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha
|
||||
}
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
await operator.batchRecords(models, 'updateChannelsDisplayName');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function updateDraftFile(serverUrl: string, channelId: string, root
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([draft], 'updateDraftFile');
|
||||
await operator.batchRecords([draft]);
|
||||
}
|
||||
|
||||
return {draft};
|
||||
@@ -58,7 +58,7 @@ export async function removeDraftFile(serverUrl: string, channelId: string, root
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([draft], 'removeDraftFile');
|
||||
await operator.batchRecords([draft]);
|
||||
}
|
||||
|
||||
return {draft};
|
||||
@@ -99,7 +99,7 @@ export async function updateDraftMessage(serverUrl: string, channelId: string, r
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([draft], 'updateDraftMessage');
|
||||
await operator.batchRecords([draft]);
|
||||
}
|
||||
|
||||
return {draft};
|
||||
@@ -129,7 +129,7 @@ export async function addFilesToDraft(serverUrl: string, channelId: string, root
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([draft], 'addFilesToDraft');
|
||||
await operator.batchRecords([draft]);
|
||||
}
|
||||
|
||||
return {draft};
|
||||
|
||||
58
app/actions/local/notification.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPostById, queryPostsInChannel, queryPostsInThread} from '@queries/servers/post';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export const updatePostSinceCache = async (serverUrl: string, notification: NotificationWithData) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
if (notification.payload?.channel_id) {
|
||||
const chunks = await queryPostsInChannel(database, notification.payload.channel_id).fetch();
|
||||
if (chunks.length) {
|
||||
const recent = chunks[0];
|
||||
const lastPost = await getPostById(database, notification.payload.post_id);
|
||||
if (lastPost) {
|
||||
await operator.database.write(async () => {
|
||||
await recent.update(() => {
|
||||
recent.latest = lastPost.createAt;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
logError('Failed updatePostSinceCache', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePostsInThreadsSinceCache = async (serverUrl: string, notification: NotificationWithData) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
if (notification.payload?.root_id) {
|
||||
const {database} = operator;
|
||||
const chunks = await queryPostsInThread(database, notification.payload.root_id).fetch();
|
||||
if (chunks.length) {
|
||||
const recent = chunks[0];
|
||||
const lastPost = await getPostById(database, notification.payload.post_id);
|
||||
if (lastPost) {
|
||||
await operator.database.write(async () => {
|
||||
await recent.update(() => {
|
||||
recent.latest = lastPost.createAt;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import {fetchPostAuthors} from '@actions/remote/post';
|
||||
import {ActionType, Post} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPostById, prepareDeletePost, queryPostsById} from '@queries/servers/post';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
@@ -19,8 +18,6 @@ import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const {SERVER: {DRAFT, FILE, POST, POSTS_IN_THREAD, REACTION, THREAD, THREAD_PARTICIPANT, THREADS_IN_TEAM}} = MM_TABLES;
|
||||
|
||||
export const sendAddToChannelEphemeralPost = async (serverUrl: string, user: UserModel, addedUsernames: string[], messages: string[], channeId: string, postRootId = '') => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
@@ -127,14 +124,14 @@ export async function removePost(serverUrl: string, post: PostModel | Post) {
|
||||
}
|
||||
|
||||
if (removeModels.length) {
|
||||
await operator.batchRecords(removeModels, 'removePost (combined user activity)');
|
||||
await operator.batchRecords(removeModels);
|
||||
}
|
||||
} else {
|
||||
const postModel = await getPostById(database, post.id);
|
||||
if (postModel) {
|
||||
const preparedPost = await prepareDeletePost(postModel);
|
||||
if (preparedPost.length) {
|
||||
await operator.batchRecords(preparedPost, 'removePost');
|
||||
await operator.batchRecords(preparedPost);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +159,7 @@ export async function markPostAsDeleted(serverUrl: string, post: Post, prepareRe
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([dbPost], 'markPostAsDeleted');
|
||||
await operator.batchRecords([dbPost]);
|
||||
}
|
||||
return {model};
|
||||
} catch (error) {
|
||||
@@ -229,7 +226,7 @@ export async function storePostsForChannel(
|
||||
}
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
await operator.batchRecords(models, 'storePostsForChannel');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
@@ -247,31 +244,3 @@ export async function getPosts(serverUrl: string, ids: string[]) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePosts(serverUrl: string, postIds: string[]) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const postsFormatted = `'${postIds.join("','")}'`;
|
||||
|
||||
await database.write(() => {
|
||||
return database.adapter.unsafeExecute({
|
||||
sqls: [
|
||||
[`DELETE FROM ${POST} where id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${REACTION} where post_id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${FILE} where post_id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${DRAFT} where root_id IN (${postsFormatted})`, []],
|
||||
|
||||
[`DELETE FROM ${POSTS_IN_THREAD} where root_id IN (${postsFormatted})`, []],
|
||||
|
||||
[`DELETE FROM ${THREAD} where id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${THREAD_PARTICIPANT} where thread_id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${THREADS_IN_TEAM} where thread_id IN (${postsFormatted})`, []],
|
||||
],
|
||||
});
|
||||
});
|
||||
return {error: false};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import {queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {getConfig, getLicense, getGlobalDataRetentionPolicy, getGranularDataRetentionPolicies, getLastGlobalDataRetentionRun, getIsDataRetentionEnabled} from '@queries/servers/system';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import {deletePosts} from './post';
|
||||
|
||||
import type {DataRetentionPoliciesRequest} from '@actions/remote/systems';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const {SERVER: {POST}} = MM_TABLES;
|
||||
|
||||
export async function storeConfigAndLicense(serverUrl: string, config: ClientConfig, license: ClientLicense) {
|
||||
try {
|
||||
// If we have credentials for this server then update the values in the database
|
||||
@@ -49,224 +40,31 @@ export async function storeConfig(serverUrl: string, config: ClientConfig | unde
|
||||
if (!config) {
|
||||
return [];
|
||||
}
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentConfig = await getConfig(database);
|
||||
const configsToUpdate: IdValue[] = [];
|
||||
const configsToDelete: IdValue[] = [];
|
||||
|
||||
try {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentConfig = await getConfig(database);
|
||||
const configsToUpdate: IdValue[] = [];
|
||||
const configsToDelete: IdValue[] = [];
|
||||
let k: keyof ClientConfig;
|
||||
for (k in config) {
|
||||
if (currentConfig?.[k] !== config[k]) {
|
||||
configsToUpdate.push({
|
||||
id: k,
|
||||
value: config[k],
|
||||
});
|
||||
}
|
||||
}
|
||||
for (k in currentConfig) {
|
||||
if (config[k] === undefined) {
|
||||
configsToDelete.push({
|
||||
id: k,
|
||||
value: currentConfig[k],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let k: keyof ClientConfig;
|
||||
for (k in config) {
|
||||
if (currentConfig?.[k] !== config[k]) {
|
||||
configsToUpdate.push({
|
||||
id: k,
|
||||
value: config[k],
|
||||
});
|
||||
}
|
||||
}
|
||||
for (k in currentConfig) {
|
||||
if (config[k] === undefined) {
|
||||
configsToDelete.push({
|
||||
id: k,
|
||||
value: currentConfig[k],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (configsToDelete.length || configsToUpdate.length) {
|
||||
return operator.handleConfigs({configs: configsToUpdate, configsToDelete, prepareRecordsOnly});
|
||||
}
|
||||
} catch (error) {
|
||||
logError('storeConfig', error);
|
||||
if (configsToDelete.length || configsToUpdate.length) {
|
||||
return operator.handleConfigs({configs: configsToUpdate, configsToDelete, prepareRecordsOnly});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function storeDataRetentionPolicies(serverUrl: string, data: DataRetentionPoliciesRequest, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {globalPolicy, teamPolicies, channelPolicies} = data;
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const systems: IdValue[] = [{
|
||||
id: SYSTEM_IDENTIFIERS.DATA_RETENTION_POLICIES,
|
||||
value: globalPolicy || {},
|
||||
}, {
|
||||
id: SYSTEM_IDENTIFIERS.GRANULAR_DATA_RETENTION_POLICIES,
|
||||
value: {
|
||||
team: teamPolicies || [],
|
||||
channel: channelPolicies || [],
|
||||
},
|
||||
}];
|
||||
|
||||
return operator.handleSystem({
|
||||
systems,
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateLastDataRetentionRun(serverUrl: string, value?: number, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const systems: IdValue[] = [{
|
||||
id: SYSTEM_IDENTIFIERS.LAST_DATA_RETENTION_RUN,
|
||||
value: value || Date.now(),
|
||||
}];
|
||||
|
||||
return operator.handleSystem({systems, prepareRecordsOnly});
|
||||
} catch (error) {
|
||||
logError('Failed updateLastDataRetentionRun', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function dataRetentionCleanup(serverUrl: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
|
||||
if (!isDataRetentionEnabled) {
|
||||
return {error: undefined};
|
||||
}
|
||||
|
||||
const lastRunAt = await getLastGlobalDataRetentionRun(database);
|
||||
const lastCleanedToday = new Date(lastRunAt).toDateString() === new Date().toDateString();
|
||||
|
||||
// Do not run if clean up is already done today
|
||||
if (lastRunAt && lastCleanedToday) {
|
||||
return {error: undefined};
|
||||
}
|
||||
|
||||
const globalPolicy = await getGlobalDataRetentionPolicy(database);
|
||||
const granularPoliciesData = await getGranularDataRetentionPolicies(database);
|
||||
|
||||
// Get global data retention cutoff
|
||||
let globalRetentionCutoff = 0;
|
||||
if (globalPolicy?.message_deletion_enabled) {
|
||||
globalRetentionCutoff = globalPolicy.message_retention_cutoff;
|
||||
}
|
||||
|
||||
// Get Granular data retention policies
|
||||
let teamPolicies: TeamDataRetentionPolicy[] = [];
|
||||
let channelPolicies: ChannelDataRetentionPolicy[] = [];
|
||||
if (granularPoliciesData) {
|
||||
teamPolicies = granularPoliciesData.team;
|
||||
channelPolicies = granularPoliciesData.channel;
|
||||
}
|
||||
|
||||
const channelsCutoffs: {[key: string]: number} = {};
|
||||
|
||||
// Get channel level cutoff from team policies
|
||||
for await (const teamPolicy of teamPolicies) {
|
||||
const {team_id, post_duration} = teamPolicy;
|
||||
const channelIds = await queryAllChannelsForTeam(database, team_id).fetchIds();
|
||||
if (channelIds.length) {
|
||||
const cutoff = getDataRetentionPolicyCutoff(post_duration);
|
||||
channelIds.forEach((channelId) => {
|
||||
channelsCutoffs[channelId] = cutoff;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get channel level cutoff from channel policies
|
||||
channelPolicies.forEach(({channel_id, post_duration}) => {
|
||||
channelsCutoffs[channel_id] = getDataRetentionPolicyCutoff(post_duration);
|
||||
});
|
||||
|
||||
const conditions = [];
|
||||
|
||||
const channelIds = Object.keys(channelsCutoffs);
|
||||
if (channelIds.length) {
|
||||
// Fetch posts by channel level cutoff
|
||||
for (const channelId of channelIds) {
|
||||
const cutoff = channelsCutoffs[channelId];
|
||||
conditions.push(`(channel_id='${channelId}' AND create_at < ${cutoff})`);
|
||||
}
|
||||
|
||||
// Fetch posts by global cutoff which are not already fetched by channel level cutoff
|
||||
conditions.push(`(channel_id NOT IN ('${channelIds.join("','")}') AND create_at < ${globalRetentionCutoff})`);
|
||||
} else {
|
||||
conditions.push(`create_at < ${globalRetentionCutoff}`);
|
||||
}
|
||||
|
||||
const postIds = await database.get<PostModel>(POST).query(
|
||||
Q.unsafeSqlQuery(`SELECT * FROM ${POST} where ${conditions.join(' OR ')}`),
|
||||
).fetchIds();
|
||||
|
||||
if (postIds.length) {
|
||||
const batchSize = 1000;
|
||||
const deletePromises = [];
|
||||
for (let i = 0; i < postIds.length; i += batchSize) {
|
||||
const batch = postIds.slice(i, batchSize);
|
||||
deletePromises.push(
|
||||
deletePosts(serverUrl, batch),
|
||||
);
|
||||
}
|
||||
const deleteResult = await Promise.all(deletePromises);
|
||||
for (const {error} of deleteResult) {
|
||||
if (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await updateLastDataRetentionRun(serverUrl);
|
||||
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
logError('An error occurred while performing data retention cleanup', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
// Returns cutoff time based on the policy's post_duration
|
||||
function getDataRetentionPolicyCutoff(postDuration: number) {
|
||||
const periodDate = new Date();
|
||||
periodDate.setDate(periodDate.getDate() - postDuration);
|
||||
periodDate.setHours(0);
|
||||
periodDate.setMinutes(0);
|
||||
periodDate.setSeconds(0);
|
||||
return periodDate.getTime();
|
||||
}
|
||||
|
||||
export async function setLastServerVersionCheck(serverUrl: string, reset = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
await operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.LAST_SERVER_VERSION_CHECK,
|
||||
value: reset ? 0 : Date.now(),
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('setLastServerVersionCheck', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setGlobalThreadsTab(serverUrl: string, globalThreadsTab: GlobalThreadsTab) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
await operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.GLOBAL_THREADS_TAB,
|
||||
value: globalThreadsTab,
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('setGlobalThreadsTab', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function dismissAnnouncement(serverUrl: string, announcementText: string) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.LAST_DISMISSED_BANNER, value: announcementText}], prepareRecordsOnly: false});
|
||||
} catch (error) {
|
||||
logError('An error occurred while dismissing an announcement', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareDeleteTeam, getMyTeamById, queryTeamSearchHistoryByTeamId, removeTeamFromTeamHistory, getTeamSearchHistoryById, getTeamById} from '@queries/servers/team';
|
||||
import {prepareDeleteTeam, getMyTeamById, queryTeamSearchHistoryByTeamId, removeTeamFromTeamHistory, getTeamSearchHistoryById} from '@queries/servers/team';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
@@ -12,7 +12,7 @@ export async function removeUserFromTeam(serverUrl: string, teamId: string) {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const myTeam = await getMyTeamById(database, teamId);
|
||||
if (myTeam) {
|
||||
const team = await getTeamById(database, myTeam.id);
|
||||
const team = await myTeam.team.fetch();
|
||||
if (!team) {
|
||||
throw new Error('Team not found');
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export async function removeUserFromTeam(serverUrl: string, teamId: string) {
|
||||
models.push(...system);
|
||||
}
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models, 'removeUserFromTeam');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function addSearchToTeamSearchHistory(serverUrl: string, teamId: st
|
||||
}
|
||||
}
|
||||
|
||||
await operator.batchRecords(models, 'addSearchToTeamHistory');
|
||||
await operator.batchRecords(models);
|
||||
return {searchModel};
|
||||
} catch (error) {
|
||||
logError('Failed addSearchToTeamSearchHistory', error);
|
||||
|
||||
@@ -40,7 +40,7 @@ export const switchToGlobalThreads = async (serverUrl: string, teamId?: string,
|
||||
models.push(...history);
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models, 'switchToGlobalThreads');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
@@ -79,25 +79,17 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
|
||||
const teamId = channel.teamId || currentTeamId;
|
||||
|
||||
let switchingTeams = false;
|
||||
if (currentTeamId === teamId) {
|
||||
const models = await prepareCommonSystemValues(operator, {
|
||||
currentChannelId: channel.id,
|
||||
});
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models, 'switchToThread');
|
||||
}
|
||||
} else {
|
||||
if (currentTeamId !== teamId) {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
switchingTeams = true;
|
||||
modelPromises.push(addTeamToTeamHistory(operator, teamId, true));
|
||||
const commonValues: PrepareCommonSystemValuesArgs = {
|
||||
currentChannelId: channel.id,
|
||||
currentTeamId: teamId,
|
||||
};
|
||||
modelPromises.push(prepareCommonSystemValues(operator, commonValues));
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models, 'switchToThread');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +168,7 @@ export async function createThreadFromNewPost(serverUrl: string, post: Post, pre
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const models: Model[] = [];
|
||||
if (post.root_id) {
|
||||
// Update the thread data: `reply_count`
|
||||
// Update the thread data: `reply_count`
|
||||
const {model: threadModel} = await updateThread(serverUrl, post.root_id, {reply_count: post.reply_count}, true);
|
||||
if (threadModel) {
|
||||
models.push(threadModel);
|
||||
@@ -201,7 +193,7 @@ export async function createThreadFromNewPost(serverUrl: string, post: Post, pre
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models, 'createThreadFromNewPost');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
@@ -212,7 +204,7 @@ export async function createThreadFromNewPost(serverUrl: string, post: Post, pre
|
||||
}
|
||||
|
||||
// On receiving threads, Along with the "threads" & "thread participants", extract and save "posts" & "users"
|
||||
export async function processReceivedThreads(serverUrl: string, threads: Thread[], teamId: string, prepareRecordsOnly = false) {
|
||||
export async function processReceivedThreads(serverUrl: string, threads: Thread[], teamId: string, loadedInGlobalThreads = false, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentUserId = await getCurrentUserId(database);
|
||||
@@ -244,6 +236,7 @@ export async function processReceivedThreads(serverUrl: string, threads: Thread[
|
||||
threads: threadsToHandle,
|
||||
teamId,
|
||||
prepareRecordsOnly: true,
|
||||
loadedInGlobalThreads,
|
||||
});
|
||||
|
||||
const models = [...postModels, ...threadModels];
|
||||
@@ -257,7 +250,7 @@ export async function processReceivedThreads(serverUrl: string, threads: Thread[
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models, 'processReceivedThreads');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
return {models};
|
||||
} catch (error) {
|
||||
@@ -277,7 +270,7 @@ export async function markTeamThreadsAsRead(serverUrl: string, teamId: string, p
|
||||
record.viewedAt = Date.now();
|
||||
}));
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models, 'markTeamThreadsAsRead');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
return {models};
|
||||
} catch (error) {
|
||||
@@ -300,7 +293,7 @@ export async function markThreadAsViewed(serverUrl: string, threadId: string, pr
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([thread], 'markThreadAsViewed');
|
||||
await operator.batchRecords([thread]);
|
||||
}
|
||||
|
||||
return {model: thread};
|
||||
@@ -327,7 +320,7 @@ export async function updateThread(serverUrl: string, threadId: string, updatedT
|
||||
record.unreadReplies = updatedThread.unread_replies ?? record.unreadReplies;
|
||||
});
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([model], 'updateThread');
|
||||
await operator.batchRecords([model]);
|
||||
}
|
||||
return {model};
|
||||
} catch (error) {
|
||||
@@ -335,17 +328,3 @@ export async function updateThread(serverUrl: string, threadId: string, updatedT
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTeamThreadsSync(serverUrl: string, data: TeamThreadsSync, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const models = await operator.handleTeamThreadsSync({data: [data], prepareRecordsOnly});
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models, 'updateTeamThreadsSync');
|
||||
}
|
||||
return {models};
|
||||
} catch (error) {
|
||||
logError('Failed updateTeamThreadsSync', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function setCurrentUserStatusOffline(serverUrl: string) {
|
||||
}
|
||||
|
||||
user.prepareStatus(General.OFFLINE);
|
||||
await operator.batchRecords([user], 'setCurrentUserStatusOffline');
|
||||
await operator.batchRecords([user]);
|
||||
return null;
|
||||
} catch (error) {
|
||||
logError('Failed setCurrentUserStatusOffline', error);
|
||||
@@ -54,7 +54,7 @@ export async function updateLocalCustomStatus(serverUrl: string, user: UserModel
|
||||
}
|
||||
}
|
||||
|
||||
await operator.batchRecords(models, 'updateLocalCustomStatus');
|
||||
await operator.batchRecords(models);
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
@@ -97,37 +97,27 @@ export const updateRecentCustomStatuses = async (serverUrl: string, customStatus
|
||||
export const updateLocalUser = async (
|
||||
serverUrl: string,
|
||||
userDetails: Partial<UserProfile> & { status?: string},
|
||||
userId?: string,
|
||||
) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
let user: UserModel | undefined;
|
||||
|
||||
if (userId) {
|
||||
user = await getUserById(database, userId);
|
||||
} else {
|
||||
user = await getCurrentUser(database);
|
||||
}
|
||||
|
||||
const user = await getCurrentUser(database);
|
||||
if (user) {
|
||||
const u = user;
|
||||
await database.write(async () => {
|
||||
await u.update((userRecord: UserModel) => {
|
||||
userRecord.authService = userDetails.auth_service ?? u.authService;
|
||||
userRecord.email = userDetails.email ?? u.email;
|
||||
userRecord.firstName = userDetails.first_name ?? u.firstName;
|
||||
userRecord.lastName = userDetails.last_name ?? u.lastName;
|
||||
userRecord.lastPictureUpdate = userDetails.last_picture_update ?? u.lastPictureUpdate;
|
||||
userRecord.locale = userDetails.locale ?? u.locale;
|
||||
userRecord.nickname = userDetails.nickname ?? u.nickname;
|
||||
userRecord.notifyProps = userDetails.notify_props ?? u.notifyProps;
|
||||
userRecord.position = userDetails?.position ?? u.position;
|
||||
userRecord.props = userDetails.props ?? u.props;
|
||||
userRecord.roles = userDetails.roles ?? u.roles;
|
||||
userRecord.status = userDetails?.status ?? u.status;
|
||||
userRecord.timezone = userDetails.timezone ?? u.timezone;
|
||||
userRecord.username = userDetails.username ?? u.username;
|
||||
await user.update((userRecord: UserModel) => {
|
||||
userRecord.authService = userDetails.auth_service ?? user.authService;
|
||||
userRecord.email = userDetails.email ?? user.email;
|
||||
userRecord.firstName = userDetails.first_name ?? user.firstName;
|
||||
userRecord.lastName = userDetails.last_name ?? user.lastName;
|
||||
userRecord.lastPictureUpdate = userDetails.last_picture_update ?? user.lastPictureUpdate;
|
||||
userRecord.locale = userDetails.locale ?? user.locale;
|
||||
userRecord.nickname = userDetails.nickname ?? user.nickname;
|
||||
userRecord.notifyProps = userDetails.notify_props ?? user.notifyProps;
|
||||
userRecord.position = userDetails?.position ?? user.position;
|
||||
userRecord.props = userDetails.props ?? user.props;
|
||||
userRecord.roles = userDetails.roles ?? user.roles;
|
||||
userRecord.status = userDetails?.status ?? user.status;
|
||||
userRecord.timezone = userDetails.timezone ?? user.timezone;
|
||||
userRecord.username = userDetails.username ?? user.username;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// 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
|
||||
@@ -206,7 +207,7 @@ export function postEphemeralCallResponseForPost(serverUrl: string, response: Ap
|
||||
serverUrl,
|
||||
message,
|
||||
post.channelId,
|
||||
post.rootId || post.id,
|
||||
post.rootId,
|
||||
response.app_metadata?.bot_user_id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,32 +2,30 @@
|
||||
// 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';
|
||||
import {markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {switchToGlobalThreads} from '@actions/local/thread';
|
||||
import {updateLocalUser} from '@actions/local/user';
|
||||
import {loadCallForChannel} from '@calls/actions/calls';
|
||||
import {DeepLink, Events, General, Preferences, Screens} from '@constants';
|
||||
import {Events, General, Preferences, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {privateChannelJoinPrompt} from '@helpers/api/channel';
|
||||
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, deleteChannelMembership, queryChannelsById} from '@queries/servers/channel';
|
||||
import {queryDisplayNamePreferences} 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';
|
||||
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getCommonSystemValues, getConfig, getCurrentTeamId, getCurrentUserId, getLicense, setCurrentChannelId} from '@queries/servers/system';
|
||||
import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {dismissAllModals, popToRoot} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from '@utils/channel';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug, logError, logInfo} from '@utils/log';
|
||||
import {logError, logInfo} from '@utils/log';
|
||||
import {showMuteChannelSnackbar} from '@utils/snack_bar';
|
||||
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
|
||||
import {displayGroupMessageName, displayUsername} from '@utils/user';
|
||||
|
||||
import {fetchGroupsForChannelIfConstrained} from './groups';
|
||||
@@ -36,13 +34,10 @@ import {setDirectChannelVisible} from './preference';
|
||||
import {fetchRolesIfNeeded} from './role';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from './team';
|
||||
import {fetchProfilesInChannel, fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
|
||||
import {fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type ClientError from '@client/rest/error';
|
||||
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[];
|
||||
@@ -51,99 +46,6 @@ export type MyChannelsRequest = {
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
export type ChannelMembersRequest = {
|
||||
members?: ChannelMembership[];
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
export async function removeMemberFromChannel(serverUrl: string, channelId: string, userId: string) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
await client.removeFromChannel(userId, channelId);
|
||||
await deleteChannelMembership(operator, userId, channelId);
|
||||
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
logError('removeMemberFromChannel', error);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchChannelMembersByIds(serverUrl: string, channelId: string, userIds: string[], fetchOnly = false): Promise<ChannelMembersRequest> {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const members = await client.getChannelMembersByIds(channelId, userIds);
|
||||
|
||||
if (!fetchOnly) {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
if (operator && members.length) {
|
||||
const memberships = members.map((u) => ({
|
||||
channel_id: channelId,
|
||||
user_id: u.user_id,
|
||||
scheme_admin: u.scheme_admin,
|
||||
}));
|
||||
await operator.handleChannelMembership({
|
||||
channelMemberships: memberships,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {members};
|
||||
} catch (error) {
|
||||
logError('fetchChannelMembersByIds', error);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
export async function updateChannelMemberSchemeRoles(serverUrl: string, channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean, fetchOnly = false) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.updateChannelMemberSchemeRoles(channelId, userId, isSchemeUser, isSchemeAdmin);
|
||||
|
||||
if (!fetchOnly) {
|
||||
return getMemberInChannel(serverUrl, channelId, userId);
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
logError('updateChannelMemberSchemeRoles', error);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMemberInChannel(serverUrl: string, channelId: string, userId: string, fetchOnly = false) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const member = await client.getMemberInChannel(channelId, userId);
|
||||
|
||||
if (!fetchOnly) {
|
||||
updateLocalUser(serverUrl, member, userId);
|
||||
}
|
||||
return {member, error: undefined};
|
||||
} catch (error) {
|
||||
logError('getMemberInChannel', error);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchChannelMemberships(serverUrl: string, channelId: string, options: GetUsersOptions, fetchOnly = false) {
|
||||
const {users = []} = await fetchProfilesInChannel(serverUrl, channelId, undefined, options, fetchOnly);
|
||||
const userIds = users.map((u) => u.id);
|
||||
|
||||
// MM-49896 https://mattermost.atlassian.net/browse/MM-49896
|
||||
// We are not sure the getChannelMembers API returns the same members
|
||||
// from getProfilesInChannel. This guarantees a 1:1 match of the
|
||||
// user IDs
|
||||
const {members = []} = await fetchChannelMembersByIds(serverUrl, channelId, userIds, true);
|
||||
return {users, members};
|
||||
}
|
||||
|
||||
export async function addMembersToChannel(serverUrl: string, channelId: string, userIds: string[], postRootId = '', fetchOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
@@ -174,7 +76,7 @@ export async function addMembersToChannel(serverUrl: string, channelId: string,
|
||||
}));
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat(), 'addMembersToChannel');
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
return {channelMemberships};
|
||||
} catch (error) {
|
||||
@@ -251,7 +153,7 @@ export async function createChannel(serverUrl: string, displayName: string, purp
|
||||
models.push(...categoriesModels.models);
|
||||
}
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models, 'createChannel');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
fetchChannelStats(serverUrl, channelData.id, false);
|
||||
EphemeralStore.creatingChannel = false;
|
||||
@@ -296,7 +198,7 @@ export async function patchChannel(serverUrl: string, channelPatch: Partial<Chan
|
||||
models.push(channel);
|
||||
}
|
||||
if (models?.length) {
|
||||
await operator.batchRecords(models.flat(), 'patchChannel');
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
return {channel: channelData};
|
||||
} catch (error) {
|
||||
@@ -343,7 +245,7 @@ export async function leaveChannel(serverUrl: string, channelId: string) {
|
||||
models.push(...removeUserModels);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models, 'leaveChannel');
|
||||
await operator.batchRecords(models);
|
||||
|
||||
if (isTabletDevice) {
|
||||
switchToLastChannel(serverUrl);
|
||||
@@ -393,7 +295,7 @@ export async function fetchChannelCreator(serverUrl: string, channelId: string,
|
||||
}));
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat(), 'fetchChannelCreator');
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
|
||||
return {user};
|
||||
@@ -455,9 +357,6 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string,
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fetchOnly) {
|
||||
setTeamLoading(serverUrl, true);
|
||||
}
|
||||
const [allChannels, channelMemberships, categoriesWithOrder] = await Promise.all([
|
||||
client.getMyChannels(teamId, includeDeleted, since),
|
||||
client.getMyChannelMembers(teamId),
|
||||
@@ -484,16 +383,12 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string,
|
||||
const {models: catModels} = await storeCategories(serverUrl, categories, true, true); // Re-sync
|
||||
const models = (chModels || []).concat(catModels || []);
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models, 'fetchMyChannelsForTeam');
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
|
||||
return {channels, memberships, categories};
|
||||
} catch (error) {
|
||||
if (!fetchOnly) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
@@ -533,37 +428,38 @@ 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) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const displayNameByChannel: Record<string, string> = {};
|
||||
const users: UserProfile[] = [];
|
||||
const updatedChannels = new Set<Channel>();
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
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]));
|
||||
const {database} = operator;
|
||||
const displayNameByChannel: Record<string, string> = {};
|
||||
const users: UserProfile[] = [];
|
||||
const updatedChannels = new Set<Channel>();
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
gms.push(c);
|
||||
continue;
|
||||
}
|
||||
gms.push(c);
|
||||
}
|
||||
|
||||
try {
|
||||
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),
|
||||
@@ -609,7 +505,7 @@ export async function fetchMissingDirectChannelsInfo(serverUrl: string, directCh
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat(), 'fetchMissingDirectChannelInfo');
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
|
||||
return {directChannels: updatedChannelsArray, users};
|
||||
@@ -624,7 +520,7 @@ export async function fetchDirectChannelsInfo(serverUrl: string, directChannels:
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const preferences = await queryDisplayNamePreferences(database).fetch();
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).fetch();
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences, config?.LockTeammateNameDisplay, config?.TeammateNameDisplay, license);
|
||||
@@ -687,7 +583,7 @@ export async function joinChannel(serverUrl: string, teamId: string, channelId?:
|
||||
}
|
||||
if (flattenedModels?.length > 0) {
|
||||
try {
|
||||
await operator.batchRecords(flattenedModels, 'joinChannel');
|
||||
await operator.batchRecords(flattenedModels);
|
||||
} catch {
|
||||
logError('FAILED TO BATCH CHANNELS');
|
||||
}
|
||||
@@ -702,7 +598,6 @@ export async function joinChannel(serverUrl: string, teamId: string, channelId?:
|
||||
}
|
||||
|
||||
if (channelId || channel?.id) {
|
||||
loadCallForChannel(serverUrl, channelId || channel!.id);
|
||||
EphemeralStore.removeJoiningChannel(channelId || channel!.id);
|
||||
}
|
||||
return {channel, member};
|
||||
@@ -726,15 +621,11 @@ export async function joinChannelIfNeeded(serverUrl: string, channelId: string)
|
||||
}
|
||||
}
|
||||
|
||||
export async function markChannelAsRead(serverUrl: string, channelId: string, updateLocal = false) {
|
||||
export async function markChannelAsRead(serverUrl: string, channelId: string) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.viewMyChannel(channelId);
|
||||
|
||||
if (updateLocal) {
|
||||
await markChannelAsViewed(serverUrl, channelId, true);
|
||||
}
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
@@ -760,7 +651,7 @@ export async function switchToChannelByName(serverUrl: string, channelName: stri
|
||||
let joinedTeam = false;
|
||||
let teamId = '';
|
||||
try {
|
||||
if (teamName === DeepLink.Redirect) {
|
||||
if (teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
|
||||
teamId = await getCurrentTeamId(database);
|
||||
} else {
|
||||
const team = await getTeamByName(database, teamName);
|
||||
@@ -818,29 +709,6 @@ export async function switchToChannelByName(serverUrl: string, channelName: stri
|
||||
}
|
||||
}
|
||||
|
||||
export async function goToNPSChannel(serverUrl: string) {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await client.getUserByUsername(General.NPS_PLUGIN_BOT_USERNAME);
|
||||
const {data, error} = await createDirectChannel(serverUrl, user.id);
|
||||
if (error || !data) {
|
||||
throw error || new Error('channel not found');
|
||||
}
|
||||
await switchToChannelById(serverUrl, data.id, data.team_id);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export async function createDirectChannel(serverUrl: string, userId: string, displayName = '') {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
@@ -874,7 +742,7 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
|
||||
if (displayName) {
|
||||
created.display_name = displayName;
|
||||
} else {
|
||||
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
@@ -917,7 +785,7 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
|
||||
models.push(...userModels);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models, 'createDirectChannel');
|
||||
await operator.batchRecords(models);
|
||||
EphemeralStore.creatingDMorGMTeammates = [];
|
||||
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
|
||||
return {data: created};
|
||||
@@ -1016,7 +884,7 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
|
||||
return {data: created};
|
||||
}
|
||||
|
||||
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
@@ -1051,7 +919,7 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
|
||||
}
|
||||
|
||||
models.push(...userModels);
|
||||
operator.batchRecords(models, 'createGroupChannel');
|
||||
operator.batchRecords(models);
|
||||
}
|
||||
}
|
||||
EphemeralStore.creatingDMorGMTeammates = [];
|
||||
@@ -1362,7 +1230,7 @@ export const unarchiveChannel = async (serverUrl: string, channelId: string) =>
|
||||
try {
|
||||
EphemeralStore.addArchivingChannel(channelId);
|
||||
await client.unarchiveChannel(channelId);
|
||||
await setChannelDeleteAt(serverUrl, channelId, 0);
|
||||
await setChannelDeleteAt(serverUrl, channelId, Date.now());
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
@@ -1396,7 +1264,7 @@ export const convertChannelToPrivate = async (serverUrl: string, channelId: stri
|
||||
channel.prepareUpdate((c) => {
|
||||
c.type = General.PRIVATE_CHANNEL;
|
||||
});
|
||||
await operator.batchRecords([channel], 'convertChannelToPrivate');
|
||||
await operator.batchRecords([channel]);
|
||||
}
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
@@ -1406,45 +1274,3 @@ export const convertChannelToPrivate = async (serverUrl: string, channelId: stri
|
||||
EphemeralStore.removeConvertingChannel(channelId);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleKickFromChannel = async (serverUrl: string, channelId: string, event: string = Events.LEAVE_CHANNEL) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
if (currentChannelId !== channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentServer = await getActiveServer();
|
||||
if (currentServer?.url === serverUrl) {
|
||||
const channel = await getChannelById(database, channelId);
|
||||
DeviceEventEmitter.emit(event, channel?.displayName);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
}
|
||||
|
||||
const tabletDevice = await isTablet();
|
||||
|
||||
if (tabletDevice) {
|
||||
const teamId = await getCurrentTeamId(database);
|
||||
await removeChannelFromTeamHistory(operator, teamId, channelId);
|
||||
const newChannelId = await getNthLastChannelFromTeam(database, teamId, 0, channelId);
|
||||
if (newChannelId) {
|
||||
if (currentServer?.url === serverUrl) {
|
||||
if (newChannelId === Screens.GLOBAL_THREADS) {
|
||||
await switchToGlobalThreads(serverUrl, teamId, false);
|
||||
} else {
|
||||
await switchToChannelById(serverUrl, newChannelId, teamId, true);
|
||||
}
|
||||
} else {
|
||||
await setCurrentTeamAndChannelId(operator, teamId, channelId);
|
||||
}
|
||||
} // TODO else jump to "join a channel" screen https://mattermost.atlassian.net/browse/MM-41051
|
||||
} else {
|
||||
await setCurrentChannelId(operator, '');
|
||||
}
|
||||
} catch (error) {
|
||||
logDebug('cannot kick user from channel', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
// 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 {showPermalink} from '@actions/remote/permalink';
|
||||
import {Client} from '@client/rest';
|
||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||
import {AppCallResponseTypes} from '@constants/apps';
|
||||
import DeepLinkType from '@constants/deep_linking';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getConfig, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {showAppForm} from '@screens/navigation';
|
||||
import {handleDeepLink, matchDeepLink} from '@utils/deep_link';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
import {getTeammateNameDisplay, queryUsersByUsername} from '@queries/servers/user';
|
||||
import {showAppForm, showModal} from '@screens/navigation';
|
||||
import * as DraftUtils from '@utils/draft';
|
||||
import {matchDeepLink, tryOpenURL} from '@utils/url';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
import {makeDirectChannel, switchToChannelById, switchToChannelByName} from './channel';
|
||||
|
||||
import type {DeepLinkChannel, DeepLinkPermalink, DeepLinkDM, DeepLinkGM, DeepLinkPlugin} from '@typings/launch';
|
||||
|
||||
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;
|
||||
@@ -106,7 +113,7 @@ const executeAppCommand = async (serverUrl: string, intl: IntlShape, parser: App
|
||||
return {data: {}};
|
||||
case AppCallResponseTypes.FORM:
|
||||
if (callResp.form) {
|
||||
showAppForm(callResp.form, creq.context);
|
||||
showAppForm(callResp.form);
|
||||
}
|
||||
return {data: {}};
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
@@ -137,9 +144,60 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc
|
||||
|
||||
const config = await getConfig(database);
|
||||
const match = matchDeepLink(location, serverUrl, config?.SiteURL);
|
||||
let linkServerUrl: string | undefined;
|
||||
if (match?.data?.serverUrl) {
|
||||
linkServerUrl = DatabaseManager.searchUrl(match.data.serverUrl);
|
||||
}
|
||||
|
||||
if (match) {
|
||||
handleDeepLink(match, intl, location);
|
||||
if (match && linkServerUrl) {
|
||||
switch (match.type) {
|
||||
case DeepLinkType.Channel: {
|
||||
const data = match.data as DeepLinkChannel;
|
||||
switchToChannelByName(linkServerUrl, data.channelName, data.teamName, DraftUtils.errorBadChannel, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.Permalink: {
|
||||
const data = match.data as DeepLinkPermalink;
|
||||
showPermalink(linkServerUrl, data.teamName, data.postId, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.DirectMessage: {
|
||||
const data = match.data as DeepLinkDM;
|
||||
if (!data.userName) {
|
||||
DraftUtils.errorUnkownUser(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
if (data.serverUrl !== serverUrl) {
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
}
|
||||
const user = (await queryUsersByUsername(database, [data.userName]).fetch())[0];
|
||||
if (!user) {
|
||||
DraftUtils.errorUnkownUser(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
makeDirectChannel(linkServerUrl, user.id, displayUsername(user, intl.locale, await getTeammateNameDisplay(database)), true);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.GroupMessage: {
|
||||
const data = match.data as DeepLinkGM;
|
||||
if (!data.channelId) {
|
||||
DraftUtils.errorBadChannel(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
switchToChannelById(linkServerUrl, data.channelId);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.Plugin: {
|
||||
const data = match.data as DeepLinkPlugin;
|
||||
showModal('PluginInternal', data.id, {link: location});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const {formatMessage} = intl;
|
||||
const onError = () => Alert.alert(
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
// 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 {
|
||||
@@ -88,17 +87,10 @@ const debouncedFetchEmojiByNames = debounce(async (serverUrl: string) => {
|
||||
promises.push(client.getCustomEmojiByName(name));
|
||||
}
|
||||
|
||||
const emojis = await Promise.all(promises);
|
||||
|
||||
try {
|
||||
const emojisResult = await Promise.allSettled(promises);
|
||||
const emojis = emojisResult.reduce<CustomEmoji[]>((result, e) => {
|
||||
if (e.status === 'fulfilled') {
|
||||
result.push(e.value);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
if (emojis.length) {
|
||||
await operator.handleCustomEmojis({emojis, prepareRecordsOnly: false});
|
||||
}
|
||||
await operator.handleCustomEmojis({emojis, prepareRecordsOnly: false});
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
|
||||
@@ -1,38 +1,82 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {setLastServerVersionCheck} from '@actions/local/systems';
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {prepareCommonSystemValues} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getCurrentChannelId, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {deleteV1Data} from '@utils/file';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logInfo} from '@utils/log';
|
||||
|
||||
import {verifyPushProxy} from './common';
|
||||
import {registerDeviceToken, syncOtherServers, verifyPushProxy} from './common';
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
|
||||
export async function appEntry(serverUrl: string, since = 0) {
|
||||
export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
if (!since) {
|
||||
if (Object.keys(DatabaseManager.serverDatabases).length === 1) {
|
||||
await setLastServerVersionCheck(serverUrl, true);
|
||||
}
|
||||
registerDeviceToken(serverUrl);
|
||||
}
|
||||
|
||||
// clear lastUnreadChannelId
|
||||
const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''});
|
||||
if (removeLastUnreadChannelId) {
|
||||
await operator.batchRecords(removeLastUnreadChannelId, 'appEntry - removeLastUnreadChannelId');
|
||||
operator.batchRecords(removeLastUnreadChannelId);
|
||||
}
|
||||
|
||||
WebsocketManager.openAll();
|
||||
const {database} = operator;
|
||||
|
||||
const tabletDevice = await isTablet();
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
const lastDisconnectedAt = (await getWebSocketLastDisconnected(database)) || since;
|
||||
|
||||
const entryData = await entry(serverUrl, currentTeamId, currentChannelId, since);
|
||||
if ('error' in entryData) {
|
||||
return {error: entryData.error};
|
||||
}
|
||||
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData} = entryData;
|
||||
if (isUpgrade && meData?.user) {
|
||||
const me = await prepareCommonSystemValues(operator, {currentUserId: meData.user.id});
|
||||
if (me?.length) {
|
||||
await operator.batchRecords(me);
|
||||
}
|
||||
}
|
||||
|
||||
let switchToChannel = false;
|
||||
|
||||
// Immediately set the new team as the current team in the database so that the UI
|
||||
// renders the correct team.
|
||||
if (tabletDevice && initialChannelId) {
|
||||
switchToChannel = true;
|
||||
switchToChannelById(serverUrl, initialChannelId, initialTeamId);
|
||||
} else {
|
||||
setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
|
||||
}
|
||||
|
||||
const dt = Date.now();
|
||||
await operator.batchRecords(models);
|
||||
logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = meData?.user || (await getCurrentUser(database))!;
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
|
||||
|
||||
if (!since) {
|
||||
// Load data from other servers
|
||||
syncOtherServers(serverUrl);
|
||||
}
|
||||
|
||||
verifyPushProxy(serverUrl);
|
||||
|
||||
return {};
|
||||
return {userId: currentUserId};
|
||||
}
|
||||
|
||||
export async function upgradeEntry(serverUrl: string) {
|
||||
@@ -40,12 +84,12 @@ export async function upgradeEntry(serverUrl: string) {
|
||||
|
||||
try {
|
||||
const configAndLicense = await fetchConfigAndLicense(serverUrl, false);
|
||||
const entryData = await appEntry(serverUrl, 0);
|
||||
const entryData = await appEntry(serverUrl, 0, true);
|
||||
const error = configAndLicense.error || entryData.error;
|
||||
|
||||
if (!error) {
|
||||
await DatabaseManager.updateServerIdentifier(serverUrl, configAndLicense.config!.DiagnosticId);
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
DatabaseManager.updateServerIdentifier(serverUrl, configAndLicense.config!.DiagnosticId);
|
||||
DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
deleteV1Data();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {Database, Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {fetchAllTeams, fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, MyTeamsRequest} from '@actions/remote/team';
|
||||
import {fetchNewThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlAllChannels} from '@client/graphQL/entry';
|
||||
import {General, Preferences, Screens} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||
@@ -19,19 +22,18 @@ import {selectDefaultTeam} from '@helpers/api/team';
|
||||
import {DEFAULT_LOCALE} from '@i18n';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
|
||||
import {queryAllServers} from '@queries/app/servers';
|
||||
import {prepareMyChannelsForTeam, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig, getCurrentChannelId, getCurrentTeamId, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getConfig, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {getMemberChannelsFromGQLQuery, gqlToClientChannelMembership} from '@utils/graphql';
|
||||
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;
|
||||
@@ -106,12 +108,6 @@ export const entryRest = async (serverUrl: string, teamId?: string, channelId?:
|
||||
}
|
||||
|
||||
const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds, isCRTEnabled} = fetchedData;
|
||||
const chError = chData?.error as ClientError | undefined;
|
||||
if (chError?.status_code === 403) {
|
||||
// if the user does not have appropriate permissions, which means the user those not belong to the team,
|
||||
// we set it as there is no errors, so that the teams and others can be properly handled
|
||||
chData!.error = undefined;
|
||||
}
|
||||
const error = teamData.error || chData?.error || prefData.error || meData.error;
|
||||
if (error) {
|
||||
return {error};
|
||||
@@ -149,7 +145,7 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
|
||||
const confReq = await fetchConfigAndLicense(serverUrl);
|
||||
const prefData = await fetchMyPreferences(serverUrl, fetchOnly);
|
||||
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, confReq.config?.CollapsedThreads, confReq.config?.FeatureFlagCollapsedThreads, confReq.config?.Version));
|
||||
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, confReq.config?.CollapsedThreads, confReq.config?.FeatureFlagCollapsedThreads));
|
||||
if (prefData.preferences) {
|
||||
const crtToggled = await getHasCRTChanged(database, prefData.preferences);
|
||||
if (crtToggled) {
|
||||
@@ -179,7 +175,7 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
if (!initialTeamId && teamData.teams?.length && teamData.memberships?.length) {
|
||||
// If no initial team was set in the database but got teams in the response
|
||||
const config = await getConfig(database);
|
||||
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
|
||||
const teamOrderPreference = getPreferenceValue(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
|
||||
const teamMembers = new Set(teamData.memberships.filter((m) => m.delete_at === 0).map((m) => m.team_id));
|
||||
const myTeams = teamData.teams!.filter((t) => teamMembers.has(t.id));
|
||||
const defaultTeam = selectDefaultTeam(myTeams, meData.user?.locale || DEFAULT_LOCALE, teamOrderPreference, config?.ExperimentalPrimaryTeam);
|
||||
@@ -312,10 +308,15 @@ export async function restDeferredAppEntryActions(
|
||||
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
|
||||
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
initialTeamId?: string, initialChannelId?: string) {
|
||||
// defer sidebar DM & GM profiles
|
||||
let channelsToFetchProfiles: Set<Channel>|undefined;
|
||||
setTimeout(async () => {
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
const directChannels = chData.channels.filter(isDMorGM);
|
||||
channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, true);
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
|
||||
@@ -324,32 +325,29 @@ export async function restDeferredAppEntryActions(
|
||||
fetchTeamsChannelsAndUnreadPosts(serverUrl, since, teamData.teams, teamData.memberships, initialTeamId);
|
||||
}
|
||||
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
||||
if (initialTeamId) {
|
||||
await syncTeamThreads(serverUrl, initialTeamId);
|
||||
await fetchNewThreads(serverUrl, initialTeamId, false);
|
||||
}
|
||||
|
||||
if (teamData.teams?.length) {
|
||||
for await (const team of teamData.teams) {
|
||||
if (team.id !== initialTeamId) {
|
||||
// need to await here since GM/DM threads in different teams overlap
|
||||
await syncTeamThreads(serverUrl, team.id);
|
||||
await fetchNewThreads(serverUrl, team.id, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCanJoinTeams(serverUrl);
|
||||
await fetchAllTeams(serverUrl);
|
||||
await updateAllUsersSince(serverUrl, since);
|
||||
|
||||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, currentUserId);
|
||||
|
||||
// defer sidebar DM & GM profiles
|
||||
setTimeout(async () => {
|
||||
const directChannels = chData?.channels?.filter(isDMorGM);
|
||||
const channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
if (channelsToFetchProfiles.size) {
|
||||
if (channelsToFetchProfiles?.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId);
|
||||
}
|
||||
@@ -364,14 +362,107 @@ export const registerDeviceToken = async (serverUrl: string) => {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const deviceToken = await getDeviceToken();
|
||||
if (deviceToken) {
|
||||
client.attachDevice(deviceToken);
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (appDatabase) {
|
||||
const deviceToken = await getDeviceToken(appDatabase);
|
||||
if (deviceToken) {
|
||||
client.attachDevice(deviceToken);
|
||||
}
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
};
|
||||
|
||||
export const syncOtherServers = async (serverUrl: string) => {
|
||||
const database = DatabaseManager.appDatabase?.database;
|
||||
if (database) {
|
||||
const servers = await queryAllServers(database);
|
||||
for (const server of servers) {
|
||||
if (server.url !== serverUrl && server.lastActiveAt > 0) {
|
||||
registerDeviceToken(server.url);
|
||||
syncAllChannelMembersAndThreads(server.url);
|
||||
autoUpdateTimezone(server.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const syncAllChannelMembersAndThreads = async (serverUrl: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await getConfig(database);
|
||||
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
const error = await graphQLSyncAllChannelMembers(serverUrl);
|
||||
if (error) {
|
||||
logDebug('failed graphQL, falling back to rest', error);
|
||||
restSyncAllChannelMembers(serverUrl);
|
||||
}
|
||||
} else {
|
||||
restSyncAllChannelMembers(serverUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return 'Server database not found';
|
||||
}
|
||||
|
||||
const response = await gqlAllChannels(serverUrl);
|
||||
if ('error' in response) {
|
||||
return response.error;
|
||||
}
|
||||
|
||||
if (response.errors) {
|
||||
return response.errors[0].message;
|
||||
}
|
||||
|
||||
const userId = await getCurrentUserId(operator.database);
|
||||
|
||||
const channels = getMemberChannelsFromGQLQuery(response.data);
|
||||
const memberships = response.data.channelMembers?.map((m) => gqlToClientChannelMembership(m, userId));
|
||||
|
||||
if (channels && memberships) {
|
||||
const modelPromises = await prepareMyChannelsForTeam(operator, '', channels, memberships, undefined, true);
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
if (models.length) {
|
||||
operator.batchRecords(models);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const restSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const myTeams = await client.getMyTeams();
|
||||
const preferences = await client.getMyPreferences();
|
||||
const config = await client.getClientConfigOld();
|
||||
|
||||
let excludeDirect = false;
|
||||
for (const myTeam of myTeams) {
|
||||
fetchMyChannelsForTeam(serverUrl, myTeam.id, false, 0, false, excludeDirect);
|
||||
excludeDirect = true;
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
||||
fetchNewThreads(serverUrl, myTeam.id, false);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
};
|
||||
|
||||
export async function verifyPushProxy(serverUrl: string) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
@@ -388,7 +479,12 @@ export async function verifyPushProxy(serverUrl: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceId = await getDeviceToken();
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceId = await getDeviceToken(appDatabase);
|
||||
if (!deviceId) {
|
||||
return;
|
||||
}
|
||||
@@ -416,57 +512,3 @@ export async function verifyPushProxy(serverUrl: string) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleEntryAfterLoadNavigation(
|
||||
serverUrl: string,
|
||||
teamMembers: TeamMembership[],
|
||||
channelMembers: ChannelMember[],
|
||||
currentTeamId: string,
|
||||
currentChannelId: string,
|
||||
initialTeamId: string,
|
||||
initialChannelId: string,
|
||||
) {
|
||||
try {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const currentTeamIdAfterLoad = await getCurrentTeamId(database);
|
||||
const currentChannelIdAfterLoad = await getCurrentChannelId(database);
|
||||
const mountedScreens = NavigationStore.getScreensInStack();
|
||||
const isChannelScreenMounted = mountedScreens.includes(Screens.CHANNEL);
|
||||
const isThreadsMounted = mountedScreens.includes(Screens.THREAD);
|
||||
const tabletDevice = await isTablet();
|
||||
|
||||
if (!currentTeamIdAfterLoad) {
|
||||
// First load or no team
|
||||
if (tabletDevice) {
|
||||
await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
|
||||
} else {
|
||||
await setCurrentTeamAndChannelId(operator, initialTeamId, '');
|
||||
}
|
||||
} else if (currentTeamIdAfterLoad !== currentTeamId) {
|
||||
// Switched teams while loading
|
||||
if (!teamMembers.find((t) => t.team_id === currentTeamIdAfterLoad && t.delete_at === 0)) {
|
||||
await handleKickFromTeam(serverUrl, currentTeamIdAfterLoad);
|
||||
}
|
||||
} else if (currentTeamIdAfterLoad !== initialTeamId) {
|
||||
await handleKickFromTeam(serverUrl, currentTeamIdAfterLoad);
|
||||
} else if (currentChannelIdAfterLoad !== currentChannelId) {
|
||||
// Switched channels while loading
|
||||
if (!channelMembers.find((m) => m.channel_id === currentChannelIdAfterLoad)) {
|
||||
if (tabletDevice || isChannelScreenMounted || isThreadsMounted) {
|
||||
await handleKickFromChannel(serverUrl, currentChannelIdAfterLoad);
|
||||
} else {
|
||||
await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
|
||||
}
|
||||
}
|
||||
} else if (currentChannelIdAfterLoad && currentChannelIdAfterLoad !== initialChannelId) {
|
||||
if (tabletDevice || isChannelScreenMounted || isThreadsMounted) {
|
||||
await handleKickFromChannel(serverUrl, currentChannelIdAfterLoad);
|
||||
} else {
|
||||
await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logDebug('could not manage the entry after load navigation', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
// 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 {fetchDataRetentionPolicy} from '@actions/remote/systems';
|
||||
import {MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, fetchProfilesInGroupChannels, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {MyTeamsRequest} from '@actions/remote/team';
|
||||
import {fetchNewThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry';
|
||||
import {General, Preferences} from '@constants';
|
||||
import {Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPreferenceValue} from '@helpers/api/preference';
|
||||
import {selectDefaultTeam} from '@helpers/api/team';
|
||||
import {queryAllChannels, queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig, getIsDataRetentionEnabled} from '@queries/servers/system';
|
||||
import {getConfig} from '@queries/servers/system';
|
||||
import {filterAndTransformRoles, getMemberChannelsFromGQLQuery, getMemberTeamsFromGQLQuery, gqlToClientChannelMembership, gqlToClientPreference, gqlToClientSidebarCategory, gqlToClientTeamMembership, gqlToClientUser} from '@utils/graphql';
|
||||
import {logDebug} from '@utils/log';
|
||||
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';
|
||||
|
||||
const FETCH_MISSING_GM_TIMEOUT = 2500;
|
||||
|
||||
export async function deferredAppEntryGraphQLActions(
|
||||
serverUrl: string,
|
||||
since: number,
|
||||
@@ -50,20 +48,20 @@ export async function deferredAppEntryGraphQLActions(
|
||||
setTimeout(() => {
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, true);
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
||||
if (initialTeamId) {
|
||||
await syncTeamThreads(serverUrl, initialTeamId);
|
||||
await fetchNewThreads(serverUrl, initialTeamId, false);
|
||||
}
|
||||
|
||||
if (teamData.teams?.length) {
|
||||
for await (const team of teamData.teams) {
|
||||
if (team.id !== initialTeamId) {
|
||||
// need to await here since GM/DM threads in different teams overlap
|
||||
await syncTeamThreads(serverUrl, team.id);
|
||||
await fetchNewThreads(serverUrl, team.id, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +82,7 @@ export async function deferredAppEntryGraphQLActions(
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
operator.batchRecords(models, 'deferredAppEntryActions');
|
||||
operator.batchRecords(models);
|
||||
|
||||
setTimeout(() => {
|
||||
if (result.chData?.channels?.length && result.chData.memberships?.length) {
|
||||
@@ -97,22 +95,8 @@ export async function deferredAppEntryGraphQLActions(
|
||||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, currentUserId);
|
||||
|
||||
updateCanJoinTeams(serverUrl);
|
||||
updateAllUsersSince(serverUrl, since);
|
||||
|
||||
// defer sidebar GM profiles
|
||||
setTimeout(async () => {
|
||||
const gmIds = chData?.channels?.reduce<Set<string>>((acc, v) => {
|
||||
if (v?.type === General.GM_CHANNEL) {
|
||||
acc.add(v.id);
|
||||
}
|
||||
return acc;
|
||||
}, new Set<string>());
|
||||
if (gmIds?.size) {
|
||||
fetchProfilesInGroupChannels(serverUrl, Array.from(gmIds));
|
||||
}
|
||||
}, FETCH_MISSING_GM_TIMEOUT);
|
||||
|
||||
return {error: undefined};
|
||||
}
|
||||
|
||||
@@ -196,22 +180,9 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
|
||||
user: gqlToClientUser(fetchedData.user!),
|
||||
};
|
||||
|
||||
const allTeams = getMemberTeamsFromGQLQuery(fetchedData);
|
||||
const allTeamMemberships = fetchedData.teamMembers.map((m) => gqlToClientTeamMembership(m, meData.user.id));
|
||||
|
||||
const [nonArchivedTeams, archivedTeamIds] = allTeams.reduce((acc, t) => {
|
||||
if (t.delete_at) {
|
||||
acc[1].add(t.id);
|
||||
return acc;
|
||||
}
|
||||
return [[...acc[0], t], acc[1]];
|
||||
}, [[], new Set<string>()]);
|
||||
|
||||
const nonArchivedTeamMemberships = allTeamMemberships.filter((m) => !archivedTeamIds.has(m.team_id));
|
||||
|
||||
const teamData = {
|
||||
teams: nonArchivedTeams,
|
||||
memberships: nonArchivedTeamMemberships,
|
||||
teams: getMemberTeamsFromGQLQuery(fetchedData),
|
||||
memberships: fetchedData.teamMembers.map((m) => gqlToClientTeamMembership(m, meData.user.id)),
|
||||
};
|
||||
|
||||
const prefData = {
|
||||
@@ -231,8 +202,8 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
|
||||
let initialTeamId = currentTeamId;
|
||||
if (!teamData.teams.length) {
|
||||
initialTeamId = '';
|
||||
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId && t.delete_at === 0)) {
|
||||
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
|
||||
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId)) {
|
||||
const teamOrderPreference = getPreferenceValue(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
|
||||
initialTeamId = selectDefaultTeam(teamData.teams, meData.user.locale, teamOrderPreference, config.ExperimentalPrimaryTeam)?.id || '';
|
||||
}
|
||||
const gqlRoles = [
|
||||
@@ -280,12 +251,6 @@ export const entry = async (serverUrl: string, teamId?: string, channelId?: stri
|
||||
result = entryRest(serverUrl, teamId, channelId, since);
|
||||
}
|
||||
|
||||
// Fetch data retention policies
|
||||
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
|
||||
if (isDataRetentionEnabled) {
|
||||
fetchDataRetentionPolicy(serverUrl);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,34 +1,77 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
|
||||
type AfterLoginArgs = {
|
||||
serverUrl: string;
|
||||
user: UserProfile;
|
||||
deviceToken?: string;
|
||||
}
|
||||
|
||||
export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?: any}> {
|
||||
export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs): Promise<{error?: any; hasTeams?: boolean; time?: number}> {
|
||||
const dt = Date.now();
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
if (deviceToken) {
|
||||
try {
|
||||
client.attachDevice(deviceToken);
|
||||
} catch {
|
||||
// do nothing, the token could've failed to attach to the session but is not a blocker
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const clData = await fetchConfigAndLicense(serverUrl, false);
|
||||
if (clData.error) {
|
||||
return {error: clData.error};
|
||||
}
|
||||
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials?.token) {
|
||||
WebsocketManager.createClient(serverUrl, credentials.token);
|
||||
await WebsocketManager.initializeClient(serverUrl);
|
||||
const entryData = await entry(serverUrl, '', '');
|
||||
|
||||
if ('error' in entryData) {
|
||||
return {error: entryData.error};
|
||||
}
|
||||
|
||||
return {};
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
|
||||
let switchToChannel = false;
|
||||
if (initialChannelId && isTabletDevice) {
|
||||
switchToChannel = true;
|
||||
switchToChannelById(serverUrl, initialChannelId, initialTeamId);
|
||||
} else {
|
||||
setCurrentTeamAndChannelId(operator, initialTeamId, '');
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
|
||||
const config = clData.config || {} as ClientConfig;
|
||||
const license = clData.license || {} as ClientLicense;
|
||||
deferredAppEntryActions(serverUrl, 0, user.id, user.locale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
|
||||
|
||||
return {time: Date.now() - dt, hasTeams: Boolean(teamData.teams?.length)};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
@@ -1,44 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchMyChannel, switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchPostById} from '@actions/remote/post';
|
||||
import {fetchMyTeam} from '@actions/remote/team';
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||
import {Preferences, Screens} from '@constants';
|
||||
import {getDefaultThemeByAppearance} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getMyChannel} from '@queries/servers/channel';
|
||||
import {getPostById} from '@queries/servers/post';
|
||||
import {queryThemePreferences} from '@queries/servers/preference';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getConfig, getCurrentTeamId, getLicense, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import {syncOtherServers} from './common';
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
|
||||
export async function pushNotificationEntry(serverUrl: string, notification: NotificationData) {
|
||||
export async function pushNotificationEntry(serverUrl: string, notification: NotificationWithData) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
// We only reach this point if we have a channel Id in the notification payload
|
||||
const channelId = notification.channel_id!;
|
||||
const rootId = notification.root_id!;
|
||||
const channelId = notification.payload!.channel_id!;
|
||||
const rootId = notification.payload!.root_id!;
|
||||
const {database} = operator;
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
|
||||
let teamId = notification.team_id;
|
||||
let isDirectChannel = false;
|
||||
|
||||
let teamId = notification.payload?.team_id;
|
||||
if (!teamId) {
|
||||
// If the notification payload does not have a teamId we assume is a DM/GM
|
||||
isDirectChannel = true;
|
||||
teamId = currentTeamId;
|
||||
}
|
||||
|
||||
@@ -50,7 +52,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
// When opening the app from a push notification the theme may not be set in the EphemeralStore
|
||||
// causing the goToScreen to use the Appearance theme instead and that causes the screen background color to potentially
|
||||
// not match the theme
|
||||
const themes = await queryThemePreferences(database, teamId).fetch();
|
||||
const themes = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_THEME, teamId).fetch();
|
||||
let theme = getDefaultThemeByAppearance();
|
||||
if (themes.length) {
|
||||
theme = setThemeDefaults(JSON.parse(themes[0].value) as Theme);
|
||||
@@ -58,62 +60,88 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
updateThemeIfNeeded(theme, true);
|
||||
}
|
||||
|
||||
await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME);
|
||||
|
||||
// To make the switch faster we determine if we already have the team & channel
|
||||
let myChannel: MyChannelModel | ChannelMembership | undefined = await getMyChannel(database, channelId);
|
||||
let myTeam: MyTeamModel | TeamMembership | undefined = await getMyTeamById(database, teamId);
|
||||
|
||||
if (!myTeam) {
|
||||
const resp = await fetchMyTeam(serverUrl, teamId);
|
||||
if (resp.error) {
|
||||
if ((resp.error as ClientError).status_code === 403) {
|
||||
emitNotificationError('Team');
|
||||
} else {
|
||||
emitNotificationError('Connection');
|
||||
}
|
||||
} else {
|
||||
myTeam = resp.memberships?.[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!myChannel) {
|
||||
const resp = await fetchMyChannel(serverUrl, teamId, channelId);
|
||||
if (resp.error) {
|
||||
if ((resp.error as ClientError).status_code === 403) {
|
||||
emitNotificationError('Channel');
|
||||
} else {
|
||||
emitNotificationError('Connection');
|
||||
}
|
||||
} else {
|
||||
myChannel = resp.memberships?.[0];
|
||||
}
|
||||
}
|
||||
const myChannel = await getMyChannel(database, channelId);
|
||||
const myTeam = await getMyTeamById(database, teamId);
|
||||
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(rootId);
|
||||
|
||||
let switchedToScreen = false;
|
||||
let switchedToChannel = false;
|
||||
if (myChannel && myTeam) {
|
||||
if (isThreadNotification) {
|
||||
let post: PostModel | Post | undefined = await getPostById(database, rootId);
|
||||
if (!post) {
|
||||
const resp = await fetchPostById(serverUrl, rootId);
|
||||
post = resp.post;
|
||||
}
|
||||
|
||||
const actualRootId = post && ('root_id' in post ? post.root_id : post.rootId);
|
||||
|
||||
if (actualRootId) {
|
||||
await fetchAndSwitchToThread(serverUrl, actualRootId, true);
|
||||
} else if (post) {
|
||||
await fetchAndSwitchToThread(serverUrl, rootId, true);
|
||||
} else {
|
||||
emitNotificationError('Post');
|
||||
}
|
||||
await fetchAndSwitchToThread(serverUrl, rootId, true);
|
||||
} else {
|
||||
switchedToChannel = true;
|
||||
await switchToChannelById(serverUrl, channelId, teamId);
|
||||
}
|
||||
switchedToScreen = true;
|
||||
}
|
||||
|
||||
const entryData = await entry(serverUrl, teamId, channelId);
|
||||
if ('error' in entryData) {
|
||||
return {error: entryData.error};
|
||||
}
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
|
||||
// There is a chance that after the above request returns
|
||||
// the user is no longer part of the team or channel
|
||||
// that triggered the notification (rare but possible)
|
||||
let selectedTeamId = teamId;
|
||||
let selectedChannelId = channelId;
|
||||
if (initialTeamId !== teamId) {
|
||||
// We are no longer a part of the team that the notification belongs to
|
||||
// Immediately set the new team as the current team in the database so that the UI
|
||||
// renders the correct team.
|
||||
selectedTeamId = initialTeamId;
|
||||
if (!isDirectChannel) {
|
||||
selectedChannelId = initialChannelId;
|
||||
}
|
||||
}
|
||||
|
||||
WebsocketManager.openAll();
|
||||
if (!switchedToScreen) {
|
||||
const isTabletDevice = await isTablet();
|
||||
if (isTabletDevice || (channelId === selectedChannelId)) {
|
||||
// Make switch again to get the missing data and make sure the team is the correct one
|
||||
switchedToScreen = true;
|
||||
if (isThreadNotification) {
|
||||
await fetchAndSwitchToThread(serverUrl, rootId, true);
|
||||
} else {
|
||||
switchedToChannel = true;
|
||||
await switchToChannelById(serverUrl, channelId, teamId);
|
||||
}
|
||||
} else if (teamId !== selectedTeamId || channelId !== selectedChannelId) {
|
||||
// If in the end the selected team or channel is different than the one from the notification
|
||||
// we switch again
|
||||
await setCurrentTeamAndChannelId(operator, selectedTeamId, selectedChannelId);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
if (teamId !== selectedTeamId) {
|
||||
emitNotificationError('Team');
|
||||
} else if (channelId !== selectedChannelId) {
|
||||
emitNotificationError('Channel');
|
||||
}
|
||||
|
||||
// Waiting for the screen to display fixes a race condition when fetching and storing data
|
||||
if (switchedToChannel) {
|
||||
await NavigationStore.waitUntilScreenHasLoaded(Screens.CHANNEL);
|
||||
} else if (switchedToScreen && isThreadNotification) {
|
||||
await NavigationStore.waitUntilScreenHasLoaded(Screens.THREAD);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId);
|
||||
|
||||
syncOtherServers(serverUrl);
|
||||
|
||||
return {userId: currentUserId};
|
||||
}
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
// 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 {logDebug} from '@utils/log';
|
||||
|
||||
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,
|
||||
@@ -33,11 +27,10 @@ export const uploadFile = (
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
return {cancel: client.uploadPostAttachment(file, channelId, onProgress, onComplete, onError, skipBytes)};
|
||||
} catch (error) {
|
||||
logDebug('uploadFile', error);
|
||||
return {error: error as ClientError};
|
||||
}
|
||||
return {cancel: client.uploadPostAttachment(file, channelId, onProgress, onComplete, onError, skipBytes)};
|
||||
};
|
||||
|
||||
export const fetchPublicLink = async (serverUrl: string, fileId: string) => {
|
||||
|
||||
@@ -27,7 +27,12 @@ async function getDeviceIdForPing(serverUrl: string, checkDeviceId: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
return getDeviceToken();
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return getDeviceToken(appDatabase);
|
||||
}
|
||||
|
||||
// Default timeout interval for ping is 5 seconds
|
||||
@@ -119,4 +124,3 @@ export const getRedirectLocation = async (serverUrl: string, link: string) => {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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';
|
||||
@@ -8,8 +9,6 @@ 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);
|
||||
@@ -83,7 +82,7 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string
|
||||
]);
|
||||
|
||||
if (!fetchOnly) {
|
||||
await operator.batchRecords([...groups, ...groupChannels], 'fetchGroupsForChannel');
|
||||
await operator.batchRecords([...groups, ...groupChannels]);
|
||||
}
|
||||
|
||||
return {groups, groupChannels};
|
||||
@@ -110,7 +109,7 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc
|
||||
]);
|
||||
|
||||
if (!fetchOnly) {
|
||||
await operator.batchRecords([...groups, ...groupTeams], 'fetchGroupsForTeam');
|
||||
await operator.batchRecords([...groups, ...groupTeams]);
|
||||
}
|
||||
|
||||
return {groups, groupTeams};
|
||||
@@ -136,7 +135,7 @@ export const fetchGroupsForMember = async (serverUrl: string, userId: string, fe
|
||||
]);
|
||||
|
||||
if (!fetchOnly) {
|
||||
await operator.batchRecords([...groups, ...groupMemberships], 'fetchGroupsForMember');
|
||||
await operator.batchRecords([...groups, ...groupMemberships]);
|
||||
}
|
||||
|
||||
return {groups, groupMemberships};
|
||||
|
||||
@@ -3,26 +3,18 @@
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
|
||||
import {storeMyChannelsForTeam} from '@actions/local/channel';
|
||||
import {storePostsForChannel} from '@actions/local/post';
|
||||
// import {updatePostSinceCache, updatePostsInThreadsSinceCache} from '@actions/local/notification';
|
||||
import {fetchDirectChannelsInfo, fetchMyChannel, switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {fetchMyTeam} from '@actions/remote/team';
|
||||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||
import {ActionType} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getMyChannel, getChannelById} from '@queries/servers/channel';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getMyTeamById, prepareMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentTeamId, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {logWarning} from '@utils/log';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
import {processPostsFetched} from '@utils/post';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
|
||||
const fetchNotificationData = async (serverUrl: string, notification: NotificationWithData, skipEvents = false) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -102,84 +94,24 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati
|
||||
};
|
||||
|
||||
export const backgroundNotification = async (serverUrl: string, notification: NotificationWithData) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const channelId = notification.payload?.channel_id;
|
||||
let teamId = notification.payload?.team_id;
|
||||
if (!channelId) {
|
||||
throw new Error('No chanel Id was specified');
|
||||
}
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
// If the notification payload does not have a teamId we assume is a DM/GM
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
teamId = currentTeamId;
|
||||
}
|
||||
if (notification.payload?.data) {
|
||||
const {data, isCRTEnabled} = notification.payload;
|
||||
const {channel, myChannel, team, myTeam, posts, users, threads} = data;
|
||||
const models: Model[] = [];
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
if (lastDisconnectedAt) {
|
||||
// if (Platform.OS === 'ios') {
|
||||
// const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
// const isThreadNotification = isCRTEnabled && Boolean(notification.payload?.root_id);
|
||||
// if (isThreadNotification) {
|
||||
// updatePostsInThreadsSinceCache(serverUrl, notification);
|
||||
// } else {
|
||||
// updatePostSinceCache(serverUrl, notification);
|
||||
// }
|
||||
// }
|
||||
|
||||
if (posts) {
|
||||
const postsData = processPostsFetched(posts);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(notification.payload.root_id);
|
||||
const actionType = isThreadNotification ? ActionType.POSTS.RECEIVED_IN_THREAD : ActionType.POSTS.RECEIVED_IN_CHANNEL;
|
||||
|
||||
if (team || myTeam) {
|
||||
const teamPromises = prepareMyTeams(operator, team ? [team] : [], myTeam ? [myTeam] : []);
|
||||
if (teamPromises.length) {
|
||||
const teamModels = await Promise.all(teamPromises);
|
||||
models.push(...teamModels.flat());
|
||||
}
|
||||
}
|
||||
|
||||
await storeMyChannelsForTeam(
|
||||
serverUrl, teamId,
|
||||
channel ? [channel] : [],
|
||||
myChannel ? [myChannel] : [],
|
||||
false, isCRTEnabled,
|
||||
);
|
||||
|
||||
if (data.categoryChannels?.length && channel) {
|
||||
const {models: categoryModels} = await addChannelToDefaultCategory(serverUrl, channel, true);
|
||||
if (categoryModels?.length) {
|
||||
models.push(...categoryModels);
|
||||
}
|
||||
} else if (data.categories?.categories) {
|
||||
const {models: categoryModels} = await storeCategories(serverUrl, data.categories.categories, false, true);
|
||||
if (categoryModels?.length) {
|
||||
models.push(...categoryModels);
|
||||
}
|
||||
}
|
||||
|
||||
await storePostsForChannel(
|
||||
serverUrl, channelId,
|
||||
postsData.posts, postsData.order, postsData.previousPostId ?? '',
|
||||
actionType, users || [],
|
||||
);
|
||||
|
||||
if (isThreadNotification && threads?.length) {
|
||||
const threadModels = await operator.handleThreads({
|
||||
threads: threads.map((t) => ({
|
||||
...t,
|
||||
lastFetchedAt: Math.max(t.post.create_at, t.post.update_at, t.post.delete_at),
|
||||
})),
|
||||
teamId,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
if (threadModels.length) {
|
||||
models.push(...threadModels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models, 'backgroundNotification');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logWarning('backgroundNotification', error);
|
||||
await fetchNotificationData(serverUrl, notification, true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -190,7 +122,6 @@ export const openNotification = async (serverUrl: string, notification: Notifica
|
||||
}
|
||||
|
||||
try {
|
||||
EphemeralStore.setNotificationTapped(true);
|
||||
const {database} = operator;
|
||||
const channelId = notification.payload!.channel_id!;
|
||||
const rootId = notification.payload!.root_id!;
|
||||
|
||||