Compare commits

..

24 Commits

Author SHA1 Message Date
Mattermost Build
5d653c4e19 Bump version 2.1 build 457 (#7120) (#7121)
* Bump app version number to 2.1.0

* Bump app build number to 457

(cherry picked from commit 1b94bbc0ad)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-14 23:13:13 +02:00
Mattermost Build
fd25ea163d Fix upload permissions and centralize download permissions (#7109) (#7119)
(cherry picked from commit f23960dea3)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2023-02-14 22:59:38 +02:00
Mattermost Build
55578a0dce Remove watermelondb limitation on updating an already updated model (#7067) (#7117)
* Remove watermelondb limitation on updating an already updated model

* Add logic to handle different prepare states and improve logging

* fix tests

---------

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
(cherry picked from commit 980c31f40f)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2023-02-14 19:42:11 +02:00
Mattermost Build
a9f325ef43 Fix iOS programmatically orientation crash on OS below 16 (#7112) (#7114)
(cherry picked from commit 76c8f844f9)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-14 17:24:29 +02:00
Elias Nahum
24ee8cc98e Bump app build number to 456 (#7102) 2023-02-08 17:07:08 +02:00
Mattermost Build
c27e1116cc Android fix (#7099) (#7101)
* Fix android notifications permission

* fix unsigned android build

(cherry picked from commit cb717aba0c)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-08 16:57:01 +02:00
Mattermost Build
2aaa366558 Replace package and imports for Kotlin files (#7090) (#7092)
(cherry picked from commit f37a9fbabb)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-07 11:36:54 +02:00
Mattermost Build
e72a142974 Fix (#7082) (#7083)
(cherry picked from commit fab5665773)

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
2023-02-03 15:26:48 +05:30
Jason Frerich
7165830fe0 [Gekidou - MM-47653] Implement Manage Members Screen (#6771)
* move user_list to component

* start the modal from create_direct_message

* initial commit

* Add managing options to user profile

* s/showManage/showManageMode/

* simplify

* use helper functions

* add dependency

* fix togglling manage/done button

* remove close button in navbar

* remove close button. The only exit from this screen is the back button

* utilize LeaveChannelLabel component actions

* nit

* nit

* slight refactor

* return earlier if not showManageMode

* use defineMessages

* don't modify leave channel component

* add manage_members_label component

* rename variables to imply manage actions

* remove user from channel on server and locally in channel membership

* prevent managing yourself. In V1, this is done by not allowing you to
select yourself for channel removal

* remove useReducer

* - fix typography
- fix icon size
- don't allow tapping on yourself in manage mode

* sort props

* sort props

* sort props

* - combine try blocks
- use getServerDatabaseAndOperator function to get the operator

* fetchChannelStats after removing users from a channel

* currently, the UI does not provide a need to remove multiple members
from a channel, only one member. modify the function to only accept and
remove one user

* no need to pass the entire channel object. only need the channelId which
is already passed into the screen

* do not pass the entire user model, only the userid and if user canManage
  (is sysadmin or channel admin)

* move members constants to its own file and out of general.ts file

* pass channel displayName instead of the entire channel object

* not need to store the user as it is already in the store from the
fetchProfilesInChannel call

* implement device emitter to notify the parent to remove to the user from
the user list

* rename constant in reveal removing a member from a channel.  Might need for another team removal later.

* add snackbar after user is removed

* remove unnessary filter

* remove paging. Server response is not paginated
deconstruct intl

* create EMPTY const

* simplify getProfiles function

* move constants to top of file

* add function to remove the user from the server

* clean up dependencies

* remove @app/ prefix from imports

* add comment describing reason for switch / case

* rename varaible to be more intention revealing

* calculate isDefaultChannel and pass in as prop so don't need to
query for each user

* if user cannot manage, do not show the manage nav button

* move options const into function that uses it

* have the caller of handeRemoveMemberFromChannel fetch channel stats, not
the action

* nit formatting

* s/canManage/canManageMembers/

* use existing observeCanManageChannelMembers function
function only requires channel id

* move userInfo and manage user options to their own components

* calculate bottom sheet snap points when in manage mode

* implement correct permissions for managing users.  For now, only channel
admins can manage users (including deleting members)

* working on section creation

* use map instead of arrays

* - handle user profile sections differently when in members are provided (manage mode)
- emit event when user role is changed
  - modify the channelMembers in manage members modal after changing
    user role

* remove commented code

* deconstruct options

* sort dependencies and add loading dependency

* - when removing a user, remove them from channelMembers state also
- don't add empty sections to the user list results

* user profile coming from ManageChannelMembers is UserProfile joined with
their ChannelMembership.  Can now check for scheme_admin to see if the
user is a channel admin

* deconstruct locale from intl and remove intl const

* Add SearchUserOptions type to provide type checking when creating options for searchProfile
action and searchUsers client api

* correct comment

* deconstruct MANAGE_OPTIONS

* Remove unused event constant

* nits

* Push header title in to the UserProfileTitle component

* Put constants back so Diff of file is smaller

* Combine switch statements
Remove isOptionItem.  These are always action items

* Wrap onAction in a usecallback

* Add help comments

* Add i18n to section titles

* Create RenderItemType for renderItem callback

* update testID
update snapshots

* CanManageMembers is deterimined by observeCanManageChannelMembers

* Add members chanenl option

* Update after merge

* Sort in order of options shown

* nit refactor

* Modify client getProfilesInChannel allow passing more options than sort.
- sort the profiles by admin
- do not show deactivated users in the manage members modal

* Profiles are now sorted by admin.  We can maintain the alphabetical sort
also by iterating over the profiles instead of members which are not alphabetical

* Type the get users Api object

* Add type.
Active option is a boolean, not a string

* only initialize if needed. Moved inside the check for members

* Create type for Manage Member Options

* Remove one liners and call directly in the switch block

* Keys to the map do not need to be translated. Only translate the title
Place the Admins section always on top

* Add removeFromChannel as a dependency

* Remove manageMode option from the title component
- add imageSize prop
- add headerText prop

* Do not show deactivated users in search

* When users are showing and not in manage mode, allow the user to tap and
open the profile for the user (in non-manage mode)

* Add fetchOnly to getMemberInChannel function
Add fetchOnly to updateChannelMembersSchemeRoles function
Remove getMemberInChannel from handleUserChangeRole in manage_channel_members because it is already called via updateChannelMembersSchemeRoles

* Remove todo from comment

* Don't use state for defining action text, icon, and isDestructive. just
set them based on the prop value manageOption

* Added correct permission check for can user manage member roles

* Add can manage member roles prop

* Calculate snap points based on manageMemberRoles prop

* Calculate snap point based on if user can remove other users

* Do not show options if you cannot remove or manage members

* Fix post merge issues

* No need to batch because only manipulating a single model

* Remove comment

* Rename variable

* Split and sort props into multiple lines for readability

* Nit

* Make dependency more specific

* Remove comment.  Doing this requires writing a custom search function in
the app that would need to guarantee the same results as a server call

* Add logError to functions with catch

* Add ticket reference

* Remove await from functions that are updating the database.  Components
that observe models these modify will get the update based from the
observable change.

* Keep track of which section is first so that the tutorial highlight
selects the first user profile of the first section

* Add a second user that creates a new section for testing tutorial

* Remove unused prop

* Update snapshot to include second user

* Use getServerDatabaseAndOperator

* remove testID change. Added a ticket to fix later

* Revert tests to only one user to test if previous tests worked

* Add new test that has 2 users

* Add ticket context as comment

* Add channelId as dependency

* Use useCallback for updateChannelMemberSchemeRole

* Remove async

* mounted.current should only be used in an effect that executes on the
first render

when user has permission to manage members changed, there is no need to
get the profiles again

* Add await for function

* Always reset loading to false after getting profiles

* use !text instead of const value using Boolean()

* add dependency

* Add manage members ids back

* When fetching users for the channel, always store them in the database.
Otherwise tapping a user might not be in the database and tapping on
them will cause a crash

* Fetch the user profile from the server when opening the user profile

* Checking management permissions should be based on the current user, not
the user of the profile being opened

---------

Co-authored-by: Avinash Lingaloo <avinashlng1080@gmail.com>
2023-02-03 10:47:22 +02:00
Mattermost Build
ce5d049a55 Update RN and deps to fix ANR issues (#7078) (#7079)
(cherry picked from commit 82f0b014f4)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-02 14:44:09 +02:00
Mattermost Build
8d9fab9b53 Use timeout defaults for iOS Share Extension and Notification Service (#7051) (#7074)
* Use timeout defaults for iOS Share Extension and Notification Service

* more logs

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

(cherry picked from commit 5aaff10664)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-01 21:28:53 +02:00
Mattermost Build
70cf8c5593 Only fetchMissingDirectChannelsInfo when no display name is set (#7060) (#7069)
(cherry picked from commit c9b56e55c4)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-31 22:10:24 +02:00
Mattermost Build
c9773d031d Request permissions for Android push notifications and refactor code to use network client (#7059) (#7068)
(cherry picked from commit 265b8b2193)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-31 21:41:09 +02:00
Mattermost Build
d75b854828 Fix add to default category code for dms and gms (#7057) (#7064)
(cherry picked from commit aa6c1ff058)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2023-01-31 16:40:46 +01:00
Mattermost Build
f1a06396c6 Filter unused preferences (#7015) (#7061)
* small preferences refactor

* filter unused preferences and fix removal of preferences in the db

* Feedback review

(cherry picked from commit 64a59aad55)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-30 21:12:26 +02:00
Mattermost Build
d1cbfe6659 Fix the animation that occurs in login flow (#7054) (#7056)
(cherry picked from commit 37bc95cf1e)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-30 12:16:02 +02:00
Mattermost Build
ff18feeac4 Bump app build number to 454 (#7042) (#7049)
Co-authored-by: Mattermost Build <build@mattermost.com>
(cherry picked from commit b2fb4d7ec2)

Co-authored-by: Avinash Lingaloo <avinashlng1080@gmail.com>
2023-01-27 22:26:46 +02:00
Mattermost Build
05984b7202 Fixes crashes and errors in iOS Share Extension and Notification Service (#7032) (#7048)
* Fix erros & crashes in iOS share extension

* Fix erros & crashes in iOS notification service

---------

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

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 22:20:33 +02:00
Mattermost Build
511525c9ed disable top domain level verification (#7045) (#7046)
(cherry picked from commit a535728d5c)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 22:13:18 +02:00
Mattermost Build
055c9109ef Fix CI to include postinstall script of react-native-webrtc (#7043) (#7044)
(cherry picked from commit 64ee37dfd4)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 20:55:49 +02:00
Mattermost Build
d484a4ff45 catch exceptions in Android Database helper (#7027) (#7041)
(cherry picked from commit 7ed2e73a91)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 18:58:42 +02:00
Mattermost Build
e6a1cbb2aa Allow user to mark post as unread that was posted by a webhook (#7016) (#7039)
* Allow user to mark post as unread that was posted by a webhook

* feedback review

(cherry picked from commit 34aef73ac1)

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

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

* ensure notification channels are created

(cherry picked from commit 9bae53b4ad)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-27 18:11:05 +02:00
999 changed files with 24881 additions and 40818 deletions

617
.circleci/config.yml Normal file
View File

@@ -0,0 +1,617 @@
version: 2.1
orbs:
owasp: entur/owasp@0.0.10
node: circleci/node@5.0.3
executors:
android:
parameters:
resource_class:
default: xlarge
type: string
environment:
NODE_OPTIONS: --max_old_space_size=12000
NODE_ENV: production
BABEL_ENV: production
docker:
- image: cimg/android:2022.09.2-node
working_directory: ~/mattermost-mobile
resource_class: <<parameters.resource_class>>
ios:
parameters:
resource_class:
default: medium
type: string
environment:
NODE_OPTIONS: --max_old_space_size=12000
NODE_ENV: production
BABEL_ENV: production
macos:
xcode: "14.0.0"
working_directory: ~/mattermost-mobile
shell: /bin/bash --login -o pipefail
resource_class: <<parameters.resource_class>>
commands:
checkout-private:
description: "Checkout the private repo with build env vars"
steps:
- add_ssh_keys:
fingerprints:
- "03:1c:a7:07:35:bc:57:e4:1d:6c:e1:2c:4b:be:09:6d"
- run:
name: Clone the mobile private repo
command: git clone git@github.com:mattermost/mattermost-mobile-private.git ~/mattermost-mobile-private
fastlane-dependencies:
description: "Get Fastlane dependencies"
parameters:
for:
type: string
steps:
- restore_cache:
name: Restore Fastlane cache
key: v1-gems-<< parameters.for >>-{{ checksum "fastlane/Gemfile.lock" }}-{{ arch }}
- run:
working_directory: fastlane
name: Download Fastlane dependencies
command: bundle install --path vendor/bundle
- save_cache:
name: Save Fastlane cache
key: v1-gems-<< parameters.for >>-{{ checksum "fastlane/Gemfile.lock" }}-{{ arch }}
paths:
- fastlane/vendor/bundle
gradle-dependencies:
description: "Get Gradle dependencies"
steps:
- restore_cache:
name: Restore Gradle cache
key: v1-gradle-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
- run:
working_directory: android
name: Download Gradle dependencies
command: ./gradlew dependencies
- save_cache:
name: Save Gradle cache
paths:
- ~/.gradle
key: v1-gradle-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
assets:
description: "Generate app assets"
steps:
- restore_cache:
name: Restore assets cache
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
- run:
name: Generate assets
command: node ./scripts/generate-assets.js
- run:
name: Compass Icons
environment:
COMPASS_ICONS: "node_modules/@mattermost/compass-icons/font/compass-icons.ttf"
command: |
cp "$COMPASS_ICONS" "assets/fonts/"
cp "$COMPASS_ICONS" "android/app/src/main/assets/fonts"
- save_cache:
name: Save assets cache
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
paths:
- dist
npm-dependencies:
description: "Get JavaScript dependencies"
steps:
- node/install:
node-version: '18.7.0'
- restore_cache:
name: Restore npm cache
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
- save_cache:
name: Save npm cache
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
paths:
- node_modules
- run:
name: "Patch dependencies"
command: npx patch-package
pods-dependencies:
description: "Get cocoapods dependencies"
steps:
- restore_cache:
name: Restore cocoapods specs and pods
key: v1-cocoapods-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
- run:
name: iOS gems
command: npm run ios-gems
- run:
name: Getting cocoapods dependencies
command: npm run pod-install
- save_cache:
name: Save cocoapods specs and pods cache
key: v1-cocoapods-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
paths:
- ios/Pods
- ~/.cocoapods
build-android:
description: "Build the android app"
steps:
- checkout:
path: ~/mattermost-mobile
- checkout-private
- npm-dependencies
- assets
- fastlane-dependencies:
for: android
- gradle-dependencies
- run:
name: Append Keystore to build Android
command: |
cp ~/mattermost-mobile-private/android/${STORE_FILE} android/app/${STORE_FILE}
echo "" | tee -a android/gradle.properties > /dev/null
echo MATTERMOST_RELEASE_STORE_FILE=${STORE_FILE} | tee -a android/gradle.properties > /dev/null
echo ${STORE_ALIAS} | tee -a android/gradle.properties > /dev/null
echo ${STORE_PASSWORD} | tee -a android/gradle.properties > /dev/null
- run:
name: Jetify android libraries
command: ./node_modules/.bin/jetify
- run:
working_directory: fastlane
name: Run fastlane to build android
no_output_timeout: 30m
command: export TERM=xterm && bundle exec fastlane android build
build-ios:
description: "Build the iOS app"
steps:
- checkout:
path: ~/mattermost-mobile
- npm-dependencies
- pods-dependencies
- assets
- fastlane-dependencies:
for: ios
- run:
working_directory: fastlane
name: Run fastlane to build iOS
no_output_timeout: 30m
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
export TERM=xterm && bundle exec fastlane ios build
deploy-to-store:
description: "Deploy build to store"
parameters:
task:
type: string
target:
type: string
file:
type: string
env:
type: string
steps:
- attach_workspace:
at: ~/
- run:
name: <<parameters.task>>
working_directory: fastlane
command: <<parameters.env>> bundle exec fastlane <<parameters.target>> deploy file:$HOME/mattermost-mobile/<<parameters.file>>
persist:
description: "Persist mattermost-mobile directory"
steps:
- persist_to_workspace:
root: ~/
paths:
- mattermost-mobile*
save:
description: "Save binaries artifacts"
parameters:
filename:
type: string
steps:
- run:
name: Copying artifacts
command: |
mkdir /tmp/artifacts;
cp ~/mattermost-mobile/<<parameters.filename>> /tmp/artifacts;
- store_artifacts:
path: /tmp/artifacts
jobs:
test:
working_directory: ~/mattermost-mobile
docker:
- image: cimg/node:16.14.2
steps:
- checkout:
path: ~/mattermost-mobile
- npm-dependencies
- assets
- run:
name: Check styles
command: npm run check
- run:
name: Running Tests
command: npm test
- run:
name: Check i18n
command: ./scripts/precommit/i18n.sh
check-deps:
parameters:
cve_data_directory:
type: string
default: "~/.owasp/dependency-check-data"
working_directory: ~/mattermost-mobile
executor: owasp/default
environment:
version_url: "https://jeremylong.github.io/DependencyCheck/current.txt"
executable_url: "https://dl.bintray.com/jeremy-long/owasp/dependency-check-VERSION-release.zip"
steps:
- checkout
- restore_cache:
name: Restore npm cache
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
- run:
name: Checkout config
command: cd .. && git clone https://github.com/mattermost/security-automation-config
- run:
name: Install Go
command: sudo apt-get update && sudo apt-get install golang
- owasp/with_commandline:
steps:
# Taken from https://github.com/entur/owasp-orb/blob/master/src/%40orb.yml#L349-L361
- owasp/generate_cache_keys:
cache_key: commmandline-default-cache-key-v7
- owasp/restore_owasp_cache
- run:
name: Update OWASP Dependency-Check Database
command: |
if ! ~/.owasp/dependency-check/bin/dependency-check.sh --data << parameters.cve_data_directory >> --updateonly; then
# Update failed, probably due to a bad DB version; delete cached DB and try again
rm -rv ~/.owasp/dependency-check-data/*.db
~/.owasp/dependency-check/bin/dependency-check.sh --data << parameters.cve_data_directory >> --updateonly
fi
- owasp/store_owasp_cache:
cve_data_directory: <<parameters.cve_data_directory>>
- run:
name: Run OWASP Dependency-Check Analyzer
command: |
~/.owasp/dependency-check/bin/dependency-check.sh \
--data << parameters.cve_data_directory >> --format ALL --noupdate --enableExperimental \
--propertyfile ../security-automation-config/dependency-check/dependencycheck.properties \
--suppression ../security-automation-config/dependency-check/suppression.xml \
--suppression ../security-automation-config/dependency-check/suppression.$CIRCLE_PROJECT_REPONAME.xml \
--scan './**/*' || true
- owasp/collect_reports:
persist_to_workspace: false
- run:
name: Post results to Mattermost
command: go run ../security-automation-config/dependency-check/post_results.go
build-android-beta:
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
environment:
BRANCH_TO_BUILD: ${CIRCLE_BRANCH}
steps:
- build-android
- 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:
name: ios
resource_class: large
steps:
- build-ios
- persist
- save:
filename: "*.ipa"
build-ios-release:
executor: ios
steps:
- build-ios
- persist
- save:
filename: "*.ipa"
build-ios-pr:
executor: ios
environment:
BRANCH_TO_BUILD: ${CIRCLE_BRANCH}
steps:
- build-ios
- 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"
deploy-android-release:
executor:
name: android
resource_class: medium
steps:
- deploy-to-store:
task: "Deploy to Google Play"
target: android
file: "*.apk"
env: "SUPPLY_TRACK=beta"
deploy-android-beta:
executor:
name: android
resource_class: medium
steps:
- deploy-to-store:
task: "Deploy to Google Play"
target: android
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-beta:
executor: ios
steps:
- deploy-to-store:
task: "Deploy to TestFlight"
target: ios
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
workflows:
version: 2
build:
jobs:
- test
# - check-deps:
# context: sast-webhook
# 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-beta:
context: mattermost-mobile-android-beta
filters:
branches:
only:
- /^build-\d+$/
- /^build-android-\d+$/
- /^build-android-beta-\d+$/
- deploy-android-beta:
context: mattermost-mobile-android-beta
requires:
- build-android-beta
filters:
branches:
only:
- /^build-\d+$/
- /^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-beta:
context: mattermost-mobile-ios-beta
filters:
branches:
only:
- /^build-\d+$/
- /^build-ios-\d+$/
- /^build-ios-beta-\d+$/
- deploy-ios-beta:
context: mattermost-mobile-ios-beta
requires:
- build-ios-beta
filters:
branches:
only:
- /^build-\d+$/
- /^build-ios-\d+$/
- /^build-ios-beta-\d+$/
- build-android-pr:
context: mattermost-mobile-android-pr
requires:
- test
filters:
branches:
only: /^(build|android)-pr-.*/
- build-ios-pr:
context: mattermost-mobile-ios-pr
requires:
- test
filters:
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

View File

@@ -1,38 +0,0 @@
kind: pipeline
name: default
steps:
- name: permissions
image: alpine/git
commands:
- chmod -R 777 .
- name: build
image: cimg/android:2023.06.1-node
environment:
CIRCLECI: true
COMMIT_CHANGES_TO_GIT: false
NODE_OPTIONS: --max_old_space_size=12000
NODE_ENV: production
BABEL_ENV: production
BUILD_FOR_RELEASE: true
#APP_NAME: "EXprojekt Team Beta"
#APP_SCHEME=exprojekt
#REPLACE_ASSETS=true
#MAIN_APP_IDENTIFIER=cz.exprojekt.team.beta
#SUPPLY_PACKAGE_NAME=cz.exprojekt.team.beta
MATTERMOST_RELEASE_STORE_FILE: /root/mattermost.keystore
MATTERMOST_RELEASE_KEY_ALIAS: mattermost-google-key
MATTERMOST_RELEASE_PASSWORD: 123456
commands:
- 'npm run build:android'
- name: gitea_release
image: plugins/gitea-release
settings:
api_key:
from_secret: drone_release
base_url: https://git.ivasoft.cz
files: '*.apk'
when:
event: tag

View File

@@ -1,49 +0,0 @@
name: prepare-android-build
description: Action to prepare environment for android build
inputs:
sign:
description: Flag to enable android package signing
default: "true"
runs:
using: composite
steps:
- name: ci/prepare-mobile-build
uses: ./.github/actions/prepare-mobile-build
# Disable this since we are not caching anything for now
# - name: ci/install-gradle-dependencies
# shell: bash
# working-directory: android
# run: |
# echo "::group::install-gradle-dependencies"
# ./gradlew dependencies
# echo "::endgroup::"
- name: ci/jetify-android-libraries
shell: bash
run: |
echo "::group::jetify-android-libraries"
./node_modules/.bin/jetify
echo "::endgroup::"
- name: ci/checkout-private-repo
if: ${{ inputs.sign == 'true' }}
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
with:
repository: mattermost/mattermost-mobile-private
token: ${{ env.MATTERMOST_BUILD_GH_TOKEN }}
path: ${{ github.workspace }}/mattermost-mobile-private
- name: ci/append-keystore-to-android-build-for-signing
if: ${{ inputs.sign == 'true' }}
shell: bash
run: |
echo "::group::append-keystore-to-android-build-for-signing"
cp ${{ github.workspace }}/mattermost-mobile-private/android/${STORE_FILE} android/app/${STORE_FILE}
echo "" | tee -a android/gradle.properties > /dev/null
echo MATTERMOST_RELEASE_STORE_FILE=${STORE_FILE} | tee -a android/gradle.properties > /dev/null
echo ${STORE_ALIAS} | tee -a android/gradle.properties > /dev/null
echo ${STORE_PASSWORD} | tee -a android/gradle.properties > /dev/null
echo "::endgroup::"

View File

@@ -1,25 +0,0 @@
name: prepare-ios-build
description: Action to prepare environment for ios build
runs:
using: composite
steps:
- name: ci/install-os-deps
env:
HOMEBREW_NO_AUTO_UPDATE: "1"
shell: bash
run: |
echo "::group::install-os-deps"
brew install watchman
echo "::endgroup::"
- name: ci/prepare-mobile-build
uses: ./.github/actions/prepare-mobile-build
- name: ci/install-pods-dependencies
shell: bash
run: |
echo "::group::install-pods-dependencies"
npm run ios-gems
npm run pod-install
echo "::endgroup::"

View File

@@ -1,20 +0,0 @@
name: prepare-mobile-build
description: Action to prepare environment for mobile build
runs:
using: composite
steps:
- uses: ruby/setup-ruby@9669f3ee51dc3f4eda8447ab696b3ab19a90d14b # v1.144.0
with:
ruby-version: "2.7.7"
- name: ci/setup-fastlane-dependencies
shell: bash
run: |
echo "::group::setup-fastlane-dependencies"
bundle install
echo "::endgroup::"
working-directory: ./fastlane
- name: ci/prepare-node-deps
uses: ./.github/actions/prepare-node-deps

View File

@@ -1,46 +0,0 @@
name: deps
description: Common deps for mobile repo
runs:
using: composite
steps:
- name: ci/setup-node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
with:
node-version: "${{ env.NODE_VERSION }}"
cache: "npm"
cache-dependency-path: package-lock.json
- name: ci/install-npm-dependencies
shell: bash
env:
NODE_ENV: development
run: |
echo "::group::install-npm-dependencies"
npm ci --ignore-scripts
node node_modules/\@sentry/cli/scripts/install.js
echo "::endgroup::"
- name: ci/patch-npm-dependencies
shell: bash
run: |
echo "::group::patch-npm-dependencies"
npx patch-package
echo "::endgroup::"
- name: ci/generate-assets
shell: bash
run: |
echo "::group::generate-assets"
node ./scripts/generate-assets.js
echo "::endgroup::"
- name: ci/import-compass-icon
shell: bash
env:
COMPASS_ICONS: "node_modules/@mattermost/compass-icons/font/compass-icons.ttf"
run: |
echo "::group::import-compass-icon"
cp "$COMPASS_ICONS" "assets/fonts/"
cp "$COMPASS_ICONS" "android/app/src/main/assets/fonts"
echo "::endgroup::"

View File

@@ -1,27 +0,0 @@
name: test
description: Common tests for mobile repo
runs:
using: composite
steps:
- name: ci/prepare-node-deps
uses: ./.github/actions/prepare-node-deps
- name: ci/check-styles
shell: bash
run: |
echo "::group::check-styles"
npm run check
echo "::endgroup::"
- name: ci/run-tests
shell: bash
run: |
echo "::group::run-tests"
npm test
echo "::endgroup::"
- name: ci/check-i18n
shell: bash
run: |
echo "::group::check-i18n"
./scripts/precommit/i18n.sh
echo "::endgroup::"

View File

@@ -1,61 +0,0 @@
---
name: build-android-beta
on:
push:
branches:
- build-beta-[0-9]+
- build-android-[0-9]+
- build-android-beta-[0-9]+
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test
build-and-deploy-android-beta:
runs-on: ubuntu-22.04
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
env:
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
- name: ci/build-and-deploy-android-beta
env:
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
SENTRY_AUTH_TOKEN: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
SENTRY_DSN_ANDROID: ${{ secrets.MM_MOBILE_BETA_SENTRY_DSN_ANDROID }}
SUPPLY_JSON_KEY: ${{ github.workspace }}/mattermost-mobile-private/android/mattermost-credentials.json
run: |
echo "::group::Build"
bundle exec fastlane android build --env android.beta
echo "::endgroup::"
echo "::group::Deploy to Play Store"
bundle exec fastlane android deploy file:"${{ github.workspace }}/*.apk" --env android.beta
echo "::endgroup::"
working-directory: ./fastlane
- name: ci/upload-android-beta-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: android-build-beta-${{ github.run_id }}
path: "*.apk"

View File

@@ -1,60 +0,0 @@
---
name: build-android-release
on:
push:
branches:
- build-release-[0-9]+
- build-release-android-[0-9]+
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test
build-and-deploy-android-release:
runs-on: ubuntu-22.04
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
env:
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
- name: ci/build-and-deploy-android-release
env:
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_RELEASE_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_RELEASE_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_RELEASE_MATTERMOST_WEBHOOK_URL }}"
SENTRY_AUTH_TOKEN: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
SENTRY_DSN_ANDROID: ${{ secrets.MM_MOBILE_RELEASE_SENTRY_DSN_ANDROID }}
SUPPLY_JSON_KEY: ${{ github.workspace }}/mattermost-mobile-private/android/mattermost-credentials.json
run: |
echo "::group::Build"
bundle exec fastlane android build --env android.release
echo "::endgroup::"
echo "::group::Deploy to Play Store"
bundle exec fastlane android deploy file:"${{ github.workspace }}/*.apk" --env android.release
echo "::endgroup::"
working-directory: ./fastlane
- name: ci/upload-android-release-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: android-build-release-${{ github.run_id }}
path: "*.apk"

View File

@@ -1,99 +0,0 @@
---
name: build-ios-beta
on:
push:
branches:
- build-beta-[0-9]+
- build-beta-ios-[0-9]+
- build-beta-sim-[0-9]+
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test
build-ios-simulator:
runs-on: macos-12
if: ${{ !contains(github.ref_name, 'beta-ios') }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
- name: ci/build-ios-simulator
env:
TAG: "${{ github.ref_name }}"
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
run: bundle exec fastlane ios simulator --env ios.simulator
working-directory: ./fastlane
- name: ci/upload-ios-pr-simulator
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-x86_64.app.zip
build-and-deploy-ios-beta:
runs-on: macos-12
if: ${{ !contains(github.ref_name, 'beta-sim') }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/output-ssh-private-key
shell: bash
run: |
SSH_KEY_PATH=~/.ssh/id_ed25519
mkdir -p ~/.ssh
echo -e '${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}' > ${SSH_KEY_PATH}
chmod 0600 ${SSH_KEY_PATH}
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
- name: ci/build-and-deploy-ios-beta
env:
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
FASTLANE_TEAM_ID: "${{ secrets.MM_MOBILE_FASTLANE_TEAM_ID }}"
IOS_API_ISSUER_ID: "${{ secrets.MM_MOBILE_IOS_API_ISSUER_ID }}"
IOS_API_KEY: "${{ secrets.MM_MOBILE_IOS_API_KEY }}"
IOS_API_KEY_ID: "${{ secrets.MM_MOBILE_IOS_API_KEY_ID }}"
MATCH_GIT_URL: "${{ secrets.MM_MOBILE_MATCH_GIT_URL }}"
MATCH_PASSWORD: "${{ secrets.MM_MOBILE_MATCH_PASSWORD }}"
SENTRY_AUTH_TOKEN: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
SENTRY_DSN_IOS: "${{ secrets.MM_MOBILE_BETA_SENTRY_DSN_IOS }}"
run: |
echo "::group::Build"
bundle exec fastlane ios build --env ios.beta
echo "::endgroup::"
echo "::group::Deploy to TestFlight"
bundle exec fastlane ios deploy file:"${{ github.workspace }}/*.ipa" --env ios.beta
echo "::endgroup::"
working-directory: ./fastlane
- name: ci/upload-ios-beta-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ios-build-beta-${{ github.run_id }}
path: "*.ipa"

View File

@@ -1,99 +0,0 @@
---
name: build-ios-release
on:
push:
branches:
- build-release-[0-9]+
- build-release-ios-[0-9]+
- build-release-sim-[0-9]+
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test
build-and-deploy-ios-release:
runs-on: macos-12
if: ${{ !contains(github.ref_name, 'release-sim') }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
- name: ci/output-ssh-private-key
shell: bash
run: |
SSH_KEY_PATH=~/.ssh/id_ed25519
mkdir -p ~/.ssh
echo -e '${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}' > ${SSH_KEY_PATH}
chmod 0600 ${SSH_KEY_PATH}
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
- name: ci/build-and-deploy-ios-release
env:
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_RELEASE_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_RELEASE_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_RELEASE_MATTERMOST_WEBHOOK_URL }}"
FASTLANE_TEAM_ID: "${{ secrets.MM_MOBILE_FASTLANE_TEAM_ID }}"
IOS_API_ISSUER_ID: "${{ secrets.MM_MOBILE_IOS_API_ISSUER_ID }}"
IOS_API_KEY: "${{ secrets.MM_MOBILE_IOS_API_KEY }}"
IOS_API_KEY_ID: "${{ secrets.MM_MOBILE_IOS_API_KEY_ID }}"
MATCH_GIT_URL: "${{ secrets.MM_MOBILE_MATCH_GIT_URL }}"
MATCH_PASSWORD: "${{ secrets.MM_MOBILE_MATCH_PASSWORD }}"
SENTRY_AUTH_TOKEN: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
SENTRY_DSN_IOS: ${{ secrets.MM_MOBILE_RELEASE_SENTRY_DSN_IOS }}
run: |
echo "::group::Build"
bundle exec fastlane ios build --env ios.release
echo "::endgroup::"
echo "::group::Deploy to TestFlight"
bundle exec fastlane ios deploy file:"${{ github.workspace }}/*.ipa" --env ios.release
echo "::endgroup::"
working-directory: ./fastlane
- name: ci/upload-ios-release-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ios-build-release-${{ github.run_id }}
path: "*.ipa"
build-ios-simulator:
runs-on: macos-12
if: ${{ !contains(github.ref_name , 'release-ios') }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
- name: ci/build-ios-simulator
env:
TAG: "${{ github.ref_name }}"
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
run: bundle exec fastlane ios simulator --env ios.simulator
working-directory: ./fastlane
- name: ci/upload-ios-pr-simulator
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-x86_64.app.zip

View File

@@ -1,95 +0,0 @@
---
name: build-pr
on:
push:
branches:
- build-pr-*
- build-pr-android-*
- build-pr-ios-*
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test
build-ios-pr:
runs-on: macos-12
if: ${{ !contains(github.ref_name, 'android') }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
- name: ci/output-ssh-private-key
shell: bash
run: |
SSH_KEY_PATH=~/.ssh/id_ed25519
mkdir -p ~/.ssh
echo -e '${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}' > ${SSH_KEY_PATH}
chmod 0600 ${SSH_KEY_PATH}
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
- name: ci/build-ios-pr
env:
BRANCH_TO_BUILD: "${{ github.ref_name }}"
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_PR_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_PR_AWS_SECRET_ACCESS_KEY }}"
FASTLANE_TEAM_ID: "${{ secrets.MM_MOBILE_FASTLANE_TEAM_ID }}"
IOS_API_ISSUER_ID: "${{ secrets.MM_MOBILE_IOS_API_ISSUER_ID }}"
IOS_API_KEY: "${{ secrets.MM_MOBILE_IOS_API_KEY }}"
IOS_API_KEY_ID: "${{ secrets.MM_MOBILE_IOS_API_KEY_ID }}"
MATCH_GIT_URL: "${{ secrets.MM_MOBILE_MATCH_GIT_URL }}"
MATCH_PASSWORD: "${{ secrets.MM_MOBILE_MATCH_PASSWORD }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_PR_MATTERMOST_WEBHOOK_URL }}"
run: bundle exec fastlane ios build --env ios.pr
working-directory: ./fastlane
- name: ci/upload-ios-pr-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ios-build-pr-${{ github.run_id }}
path: "*.ipa"
build-android-pr:
runs-on: ubuntu-22.04
if: ${{ !contains(github.ref_name, 'ios') }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
env:
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
- name: ci/build-android-pr
env:
BRANCH_TO_BUILD: "${{ github.ref_name }}"
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_PR_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_PR_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_PR_MATTERMOST_WEBHOOK_URL }}"
run: bundle exec fastlane android build --env android.pr
working-directory: ./fastlane
- name: ci/upload-android-pr-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: android-build-pr-${{ github.run_id }}
path: "*.apk"

View File

@@ -1,17 +0,0 @@
---
name: ci
on:
pull_request:
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test

View File

@@ -1,100 +0,0 @@
---
name: github-release
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+*
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test
build-ios-unsigned:
runs-on: macos-12
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
- name: ci/output-ssh-private-key
shell: bash
run: |
SSH_KEY_PATH=~/.ssh/id_ed25519
mkdir -p ~/.ssh
echo -e '${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}' > ${SSH_KEY_PATH}
chmod 0600 ${SSH_KEY_PATH}
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
- name: ci/build-ios-unsigned
env:
TAG: "${{ github.ref_name }}"
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
run: bundle exec fastlane ios unsigned
working-directory: ./fastlane
- name: ci/upload-ios-unsigned
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
path: Mattermost-unsigned.ipa
name: Mattermost-unsigned.ipa
build-android-unsigned:
runs-on: ubuntu-22.04
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
with:
sign: false
- name: ci/build-android-beta
env:
TAG: "${{ github.ref_name }}"
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
run: bundle exec fastlane android unsigned
working-directory: ./fastlane
- name: ci/upload-android-unsigned-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
path: Mattermost-unsigned.apk
name: Mattermost-unsigned.apk
release:
runs-on: ubuntu-22.04
needs:
- build-ios-unsigned
- build-android-unsigned
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- uses: ruby/setup-ruby@9669f3ee51dc3f4eda8447ab696b3ab19a90d14b # v1.144.0
with:
ruby-version: "2.7"
- name: release/setup-fastlane-dependencies
run: bundle install
working-directory: ./fastlane
- name: ci/download-artifacts
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
- name: release/create-github-release
env:
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
run: bundle exec fastlane github
working-directory: ./fastlane

6
.gitignore vendored
View File

@@ -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
@@ -103,7 +102,6 @@ detox/detox_pixel_*
# Bundle artifact
*.jsbundle
.bundle
#editor-settings
.vscode

View File

@@ -1 +0,0 @@
18.7.0

1
.nvmrc
View File

@@ -1 +0,0 @@
18.7.0

View File

@@ -1 +0,0 @@
2.7.8

View File

@@ -14,7 +14,7 @@
{
"rule": "cli",
"binary": "npm",
"semver": ">=7.24.0 <9.0.0",
"semver": ">=8.5.5 <9.0.0",
"error": "install npm 8.5.5 `npm i -g npm@8.5.5"
}
],

View File

@@ -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 https://portal.productboard.com/mattermost/33-what-matters-to-you. File non-security related bugs here in the following format:
#### Summary
Issue in one concise sentence.

View File

@@ -124,19 +124,6 @@ SOFTWARE.
---
## @mattermost/calls
This product contains '@mattermost/calls' by Mattermost.
Calls enables voice calling and screen sharing functionality in Mattermost channels.
* HOMEPAGE:
* https://github.com/mattermost/calls-common
* LICENSE: Apache License
---
## @mattermost/compass-icons
This product contains '@mattermost/compass-icons' by Mattermost.
@@ -246,6 +233,41 @@ 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
@@ -1511,6 +1533,42 @@ Open Android settings from your React Native app
* LICENSE: ISC
---
## react-native-animated-numbers
This product contains 'react-native-animated-numbers' by Lake (Yeongsu Han).
Library showing animation of number changes in react-native
* HOMEPAGE:
* https://github.com/heyman333/react-native-animated-numbers
* LICENSE: MIT
MIT License
Copyright (c) 2020 Yeongsu Han
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-background-timer

View File

@@ -1,12 +1,12 @@
# Mattermost Mobile v2
- **Minimum Server versions:** Current ESR version (7.8.0+)
- **Minimum Server versions:** Current ESR version (7.1.0+)
- **Supported iOS versions:** 12.1+
- **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://mattermost.com](https://mattermost.com).
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).
You can download our apps from the [App Store](https://mattermost.com/mattermost-ios-app/) or [Google Play Store](https://mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
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/).
We plan on releasing monthly updates with new features - check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for what features are currently supported!
@@ -27,8 +27,8 @@ To help with testing app updates before they're released, you can:
- Repro steps
- Observed behavior (including screenshot / video when possible)
- Expected behavior
4. (Optional) [Sign up for our team site](https://community.mattermost.com/signup_user_complete/?id=codoy5s743rq5mk18i7u5ksz7e&md=link&sbr=su)
- Join the [Native Mobile Apps channel](https://community.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
4. (Optional) [Sign up for our team site](https://pre-release.mattermost.com/signup_user_complete/?id=f1924a8db44ff3bb41c96424cdc20676)
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
You can leave the Beta testing program at any time:
- On Android, [click this link](https://play.google.com/apps/testing/com.mattermost.rnbeta) while logged in with your Google Play email address used to opt-in for the Beta program, then click **Leave the program**.
@@ -39,7 +39,7 @@ You can leave the Beta testing program at any time:
1. Look in [GitHub issues](https://mattermost.com/pl/help-wanted-mattermost-mobile) for issues marked as [Help Wanted]
2. Comment to let people know youre working on it
3. Follow [these instructions](https://developers.mattermost.com/contribute/mobile/developer-setup/) to set up your developer environment
4. Join the [Native Mobile Apps channel](https://community.mattermost.com/core/channels/native-mobile-apps) on our team site to ask questions
4. Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) on our team site to ask questions

View File

@@ -1,6 +1,8 @@
apply plugin: "com.android.application"
apply plugin: "com.facebook.react"
apply plugin: 'kotlin-android'
import com.android.build.OutputFile
/**
* This is the configuration block to customize your React Native Android app.
@@ -110,8 +112,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 479
versionName "2.6.0"
versionCode 457
versionName "2.1.0"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
@@ -145,7 +147,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 +172,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
}
}
}
@@ -190,7 +191,7 @@ dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
@@ -204,19 +205,18 @@ dependencies {
implementation jscFlavor
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'androidx.window:window-rxjava3:1.0.0'
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.appcompat:appcompat:1.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 "com.google.firebase:firebase-messaging:$firebaseVersion"
androidTestImplementation('com.wix:detox:+')
implementation project(':reactnativenotifications')
implementation project(':watermelondb')
implementation project(':watermelondb-jsi')
}
@@ -224,16 +224,16 @@ configurations.all {
resolutionStrategy {
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'

Binary file not shown.

View File

@@ -22,7 +22,7 @@ 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;
import okhttp3.OkHttpClient;
/**
* Class responsible of loading Flipper inside your React Native application. This is the debug
@@ -37,9 +37,13 @@ public class ReactNativeFlipper {
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 +56,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 {

View File

@@ -5,7 +5,6 @@ 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()
@@ -15,35 +14,15 @@ class BitmapCache {
return bitmap.byteCount / 1024
}
}
keysCache = LruCache<String, String>(50)
}
fun bitmap(userId: String, updatedAt: Double, serverUrl: String): Bitmap? {
val key = "$serverUrl-$userId-$updatedAt"
fun getBitmapFromMemCache(key: String): Bitmap? {
return memoryCache.get(key)
}
fun insertBitmap(bitmap: Bitmap?, userId: String, updatedAt: Double, serverUrl: String) {
if (bitmap == null) {
removeBitmap(userId, serverUrl)
fun addBitmapToMemoryCache(key: String, bitmap: Bitmap) {
if (getBitmapFromMemCache(key) == null) {
memoryCache.put(key, bitmap)
}
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()
}
}

View File

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

View File

@@ -21,7 +21,6 @@ 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;
@@ -29,7 +28,6 @@ import androidx.core.app.RemoteInput;
import androidx.core.graphics.drawable.IconCompat;
import com.mattermost.rnbeta.*;
import com.nozbe.watermelondb.Database;
import java.io.IOException;
import java.util.Date;
@@ -39,9 +37,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";
@@ -60,7 +55,7 @@ public class CustomPushNotificationHelper {
private static final BitmapCache bitmapCache = new BitmapCache();
private static void addMessagingStyleMessages(Context context, NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
private static void addMessagingStyleMessages(NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
String message = bundle.getString("message", bundle.getString("body"));
String senderId = bundle.getString("sender_id");
String serverUrl = bundle.getString("server_url");
@@ -82,7 +77,7 @@ public class CustomPushNotificationHelper {
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
try {
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
Bitmap avatar = userAvatar(serverUrl, senderId, urlOverride);
if (avatar != null) {
sender.setIcon(IconCompat.createWithBitmap(avatar));
}
@@ -128,7 +123,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");
@@ -185,8 +179,8 @@ public class CustomPushNotificationHelper {
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
addNotificationExtras(notification, bundle);
setNotificationIcons(context, notification, bundle);
setNotificationMessagingStyle(context, notification, bundle);
setNotificationIcons(notification, bundle);
setNotificationMessagingStyle(notification, bundle);
setNotificationGroup(notification, groupId, createSummary);
setNotificationBadgeType(notification);
@@ -262,7 +256,7 @@ public class CustomPushNotificationHelper {
return title;
}
private static NotificationCompat.MessagingStyle getMessagingStyle(Context context, Bundle bundle) {
private static NotificationCompat.MessagingStyle getMessagingStyle(Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle;
final String senderId = "me";
final String serverUrl = bundle.getString("server_url");
@@ -275,7 +269,7 @@ public class CustomPushNotificationHelper {
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
try {
Bitmap avatar = userAvatar(context, serverUrl, "me", urlOverride);
Bitmap avatar = userAvatar(serverUrl, "me", urlOverride);
if (avatar != null) {
sender.setIcon(IconCompat.createWithBitmap(avatar));
}
@@ -288,7 +282,7 @@ public class CustomPushNotificationHelper {
String conversationTitle = getConversationTitle(bundle);
setMessagingStyleConversationTitle(messagingStyle, conversationTitle, bundle);
addMessagingStyleMessages(context, messagingStyle, conversationTitle, bundle);
addMessagingStyleMessages(messagingStyle, conversationTitle, bundle);
return messagingStyle;
}
@@ -370,8 +364,8 @@ public class CustomPushNotificationHelper {
notification.setDeleteIntent(deleteIntent);
}
private static void setNotificationMessagingStyle(Context context, NotificationCompat.Builder notification, Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(context, bundle);
private static void setNotificationMessagingStyle(NotificationCompat.Builder notification, Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(bundle);
notification.setStyle(messagingStyle);
}
@@ -384,7 +378,7 @@ public class CustomPushNotificationHelper {
}
}
private static void setNotificationIcons(Context context, NotificationCompat.Builder notification, Bundle bundle) {
private static void setNotificationIcons(NotificationCompat.Builder notification, Bundle bundle) {
String channelName = getConversationTitle(bundle);
String senderName = bundle.getString("sender_name");
String serverUrl = bundle.getString("server_url");
@@ -395,7 +389,7 @@ public class CustomPushNotificationHelper {
if (serverUrl != null && channelName.equals(senderName)) {
try {
String senderId = bundle.getString("sender_id");
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
Bitmap avatar = userAvatar(serverUrl, senderId, urlOverride);
if (avatar != null) {
notification.setLargeIcon(avatar);
}
@@ -405,33 +399,19 @@ public class CustomPushNotificationHelper {
}
}
private static Bitmap userAvatar(final Context context, @NonNull final String serverUrl, final String userId, final String urlOverride) throws IOException {
private static Bitmap userAvatar(final String serverUrl, final String userId, final String urlOverride) throws IOException {
try {
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);
Bitmap cached = bitmapCache.getBitmapFromMemCache(userId);
if (cached != null) {
Bitmap bitmap = cached.copy(cached.getConfig(), false);
return getCircleBitmap(bitmap);
}
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);
@@ -442,7 +422,7 @@ public class CustomPushNotificationHelper {
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);
bitmapCache.addBitmapToMemoryCache(userId, bitmap.copy(bitmap.getConfig(), false));
}
return getCircleBitmap(bitmap);
}

View File

@@ -2,26 +2,32 @@ 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)
}
val cursor = defaultDatabase!!.rawQuery(query)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
} catch (e: Exception) {
e.printStackTrace()
@@ -36,13 +42,640 @@ class DatabaseHelper {
}
}
fun getServerUrlForIdentifier(identifier: String): String? {
try {
val args: Array<Any?> = arrayOf(identifier)
val query = "SELECT url FROM Servers WHERE identifier=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
}
return null
}
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) {
e.printStackTrace()
return null
}
}
fun getDatabaseForServer(context: Context?, serverUrl: String): Database? {
try {
val args: Array<Any?> = arrayOf(serverUrl)
val query = "SELECT db_path FROM Servers WHERE url=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val databasePath = cursor.getString(0)
cursor.close()
return Database(databasePath, context!!)
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
}
return null
}
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? {
try {
if (db != null) {
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
if (cursor1.count == 1) {
cursor1.moveToFirst()
val lastFetchedAt = cursor1.getDouble(0)
cursor1.close()
if (lastFetchedAt == 0.0) {
return queryLastPostCreateAt(db, channelId)
}
return lastFetchedAt
}
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
}
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.getInstance(databasePath, context)
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 +683,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
}
}

View File

@@ -21,7 +21,7 @@ public class Network {
private static final Promise emptyPromise = new ResolvePromise();
public static void init(Context context) {
final ReactApplicationContext reactContext = (APIClientModule.context == null) ? new ReactApplicationContext(context) : APIClientModule.context;
final ReactApplicationContext reactContext = new ReactApplicationContext(context);
clientModule = new APIClientModule(reactContext);
createClientOptions();
}

View File

@@ -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")))
}
})
}
}
}
}

View File

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

View File

@@ -122,37 +122,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,
@@ -258,9 +245,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 +256,22 @@ public class RealPathUtil {
File f = new File(filename);
return f.getName();
}
public static File createDirIfNotExists(String path) {
File dir = new File(path);
if (dir.exists()) {
return dir;
}
try {
dir.mkdirs();
// Add .nomedia to hide the thumbnail directory from gallery
File noMedia = new File(path, ".nomedia");
noMedia.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
return dir;
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}

View File

@@ -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.getInstance(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
}
}

View File

@@ -1,258 +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 messageSource = try { post.getString("message_source") } 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, message_source, 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, messageSource, 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 messageSource = try { post.getString("message_source") } 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 = ?, message_source = ?, 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, messageSource, 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()
}
}

View File

@@ -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()
}
}

View File

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

View File

@@ -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()
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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()
}
}
}

View File

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

View File

@@ -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()
}

View File

@@ -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")))
}
})
}
}

View File

@@ -1,201 +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) }
}
}
fun findNeededUsernames(text: String?) {
if (text == null) {
return
}
val matchResults = regex.findAll(text)
matchResults.iterator().forEach {
val username = it.value.removePrefix("@")
if (!usernames.contains(username) && currentUsername != username && !specialMentions.contains(username)) {
usernames.add(username)
}
}
}
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")
findNeededUsernames(message)
val props = post?.getMap("props")
val attachments = props?.getArray("attachments")
if (attachments != null) {
for (i in 0 until attachments.size()) {
val attachment = attachments.getMap(i)
val pretext = attachment.getString("pretext")
val text = attachment.getString("text")
findNeededUsernames(pretext)
findNeededUsernames(text)
}
}
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
}
}

View File

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

View File

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

View File

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

View File

@@ -7,29 +7,23 @@ import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
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.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;
@@ -57,6 +51,7 @@ 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);
@@ -70,7 +65,7 @@ public class CustomPushNotification extends PushNotification {
}
}
finishProcessingNotification(serverUrl, type, channelId, notificationId);
finishProcessingNotification(serverUrl, type, channelId, notificationId, isReactInit);
}
@Override
@@ -83,9 +78,7 @@ public class CustomPushNotification extends PushNotification {
}
}
private void finishProcessingNotification(final String serverUrl, @NonNull final String type, final String channelId, final int notificationId) {
final boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
private void finishProcessingNotification(String serverUrl, String type, String channelId, int notificationId, Boolean isReactInit) {
switch (type) {
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
case CustomPushNotificationHelper.PUSH_TYPE_SESSION:
@@ -97,17 +90,13 @@ public class CustomPushNotification extends PushNotification {
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,
@@ -156,20 +145,17 @@ public class CustomPushNotification extends PushNotification {
}
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;

View File

@@ -13,12 +13,12 @@ class FoldableObserver(private val activity: Activity) {
private var disposable: Disposable? = null
private lateinit var observable: Observable<WindowLayoutInfo>
fun onCreate() {
public fun onCreate() {
observable = WindowInfoTracker.getOrCreate(activity)
.windowLayoutInfoObservable(activity)
}
fun onStart() {
public fun onStart() {
if (disposable?.isDisposed == true) {
onCreate()
}
@@ -42,7 +42,7 @@ class FoldableObserver(private val activity: Activity) {
}
}
fun onStop() {
public fun onStop() {
disposable?.dispose()
}

View File

@@ -1,8 +1,6 @@
package com.mattermost.rnbeta;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.KeyEvent;
@@ -14,11 +12,9 @@ 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);
private FoldableObserver foldableObserver = new FoldableObserver(this);
@Override
protected String getMainComponentName() {
@@ -34,7 +30,7 @@ public class MainActivity extends NavigationActivity {
protected ReactActivityDelegate createReactActivityDelegate() {
return new DefaultReactActivityDelegate(
this,
Objects.requireNonNull(getMainComponentName()),
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).
@@ -63,7 +59,7 @@ public class MainActivity extends NavigationActivity {
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
@@ -101,7 +97,7 @@ public class MainActivity extends NavigationActivity {
}
}
return super.dispatchKeyEvent(event);
}
};
private void setHWKeyboardConnected() {
HWKeyboardConnected = getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY;

View File

@@ -32,10 +32,10 @@ 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 +46,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;
@@ -147,7 +149,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 +176,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);
@@ -209,7 +211,7 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
if (!TextUtils.isEmpty(token)) {
WritableMap headers = Arguments.createMap();
if (optionsMap.hasKey("headers")) {
headers.merge(Objects.requireNonNull(optionsMap.getMap("headers")));
headers.merge(optionsMap.getMap("headers"));
}
headers.putString("Authorization", "Bearer " + token);
optionsMap.putMap("headers", headers);
@@ -221,11 +223,6 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
}
}
@ReactMethod
public void invalidateKeychainCache(String serverUrl) {
// Not using cache
}
private static class SaveDataTask extends GuardedResultAsyncTask<Object> {
private final WeakReference<Context> weakContext;
private final String fromFile;
@@ -240,21 +237,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;

View File

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

View File

@@ -29,10 +29,11 @@ class SplitViewModule(private var reactContext: ReactApplicationContext) : React
override fun getName() = "SplitView"
private fun sendEvent(params: WritableMap?) {
fun sendEvent(eventName: String,
params: WritableMap?) {
reactContext
.getJSModule(RCTDeviceEventEmitter::class.java)
.emit("SplitViewChanged", params)
.emit(eventName, params)
}
private fun getSplitViewResults(folded: Boolean) : WritableMap? {
@@ -50,7 +51,7 @@ class SplitViewModule(private var reactContext: ReactApplicationContext) : React
fun setDeviceFolded(folded: Boolean) {
val map = getSplitViewResults(folded)
if (listenerCount > 0 && isDeviceFolded != folded) {
sendEvent(map)
sendEvent("SplitViewChanged", map)
}
isDeviceFolded = folded
}

View File

@@ -29,7 +29,6 @@ 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;
@@ -76,8 +75,8 @@ public class ShareModule extends ReactContextBaseJavaModule {
public String getCurrentActivityName() {
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
String activityName = currentActivity.getComponentName().getClassName();
String[] components = activityName.split("\\.");
String actvName = currentActivity.getComponentName().getClassName();
String[] components = actvName.split("\\.");
return components[components.length - 1];
}
@@ -116,7 +115,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
if (data != null && data.hasKey("serverUrl")) {
ReadableArray files = data.getArray("files");
String serverUrl = data.getString("serverUrl");
final String token = Credentials.getCredentialsForServerSync(mReactContext, serverUrl);
final String token = Credentials.getCredentialsForServerSync(this.getReactApplicationContext(), serverUrl);
JSONObject postData = buildPostObject(data);
if (files != null && files.size() > 0) {
@@ -237,7 +236,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String responseData = Objects.requireNonNull(response.body()).string();
String responseData = response.body().string();
JSONObject responseJson = new JSONObject(responseData);
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
JSONArray file_ids = new JSONArray();

View File

@@ -7,9 +7,9 @@ buildscript {
compileSdkVersion = 33
targetSdkVersion = 33
supportLibVersion = "33.0.0"
kotlinVersion = "1.7.21"
kotlin_version = "1.7.21"
firebaseVersion = "23.1.1"
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.
@@ -23,7 +23,7 @@ buildscript {
dependencies {
classpath("com.android.tools.build:gradle:7.3.1")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath('com.google.gms:google-services:4.3.15')
classpath('com.google.gms:google-services:4.3.14')
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
// NOTE: Do not place your application dependencies here; they belong

View File

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

View File

@@ -5,8 +5,6 @@ project(':reactnativenotifications').projectDir = new File(rootProject.projectDi
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')

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getActiveServerUrl} from '@app/init/credentials';
import {Tutorial} from '@constants';
import {GLOBAL_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
@@ -51,29 +50,3 @@ export const storeLastAskForReview = async (prepareRecordsOnly = false) => {
export const storeFirstLaunch = async (prepareRecordsOnly = false) => {
return storeGlobal(GLOBAL_IDENTIFIERS.FIRST_LAUNCH, Date.now(), prepareRecordsOnly);
};
export const storeLastViewedChannelIdAndServer = async (channelId: string) => {
const currentServerUrl = await getActiveServerUrl();
return storeGlobal(GLOBAL_IDENTIFIERS.LAST_VIEWED_CHANNEL, {
server_url: currentServerUrl,
channel_id: channelId,
}, false);
};
export const storeLastViewedThreadIdAndServer = async (threadId: string) => {
const currentServerUrl = await getActiveServerUrl();
return storeGlobal(GLOBAL_IDENTIFIERS.LAST_VIEWED_THREAD, {
server_url: currentServerUrl,
thread_id: threadId,
}, false);
};
export const removeLastViewedChannelIdAndServer = async () => {
return storeGlobal(GLOBAL_IDENTIFIERS.LAST_VIEWED_CHANNEL, null, false);
};
export const removeLastViewedThreadIdAndServer = async () => {
return storeGlobal(GLOBAL_IDENTIFIERS.LAST_VIEWED_THREAD, null, false);
};

View File

@@ -14,7 +14,7 @@ import {
getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes,
} from '@queries/servers/channel';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {prepareCommonSystemValues, type PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId, getConfig, getLicense} from '@queries/servers/system';
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';
import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation';

View File

@@ -155,37 +155,3 @@ export const removeDraft = async (serverUrl: string, channelId: string, rootId =
return {error};
}
};
export async function updateDraftPriority(serverUrl: string, channelId: string, rootId: string, postPriority: PostPriority, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const draft = await getDraft(database, channelId, rootId);
if (!draft) {
const newDraft: Draft = {
channel_id: channelId,
root_id: rootId,
metadata: {
priority: postPriority,
},
};
return operator.handleDraft({drafts: [newDraft], prepareRecordsOnly});
}
draft.prepareUpdate((d) => {
d.metadata = {
...d.metadata,
priority: postPriority,
};
});
if (!prepareRecordsOnly) {
await operator.batchRecords([draft], 'updateDraftPriority');
}
return {draft};
} catch (error) {
logError('Failed updateDraftPriority', error);
return {error};
}
}

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

View File

@@ -5,7 +5,7 @@ import {fetchPostAuthors} from '@actions/remote/post';
import {ActionType, Post} from '@constants';
import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
import {countUsersFromMentions, getPostById, prepareDeletePost, queryPostsById} from '@queries/servers/post';
import {getPostById, prepareDeletePost, queryPostsById} from '@queries/servers/post';
import {getCurrentUserId} from '@queries/servers/system';
import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers/thread';
import {generateId} from '@utils/general';
@@ -15,7 +15,6 @@ import {getPostIdsForCombinedUserActivityPost} from '@utils/post_list';
import {updateLastPostAt, updateMyChannelLastFetchedAt} from './channel';
import type {Q} from '@nozbe/watermelondb';
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';
@@ -158,7 +157,6 @@ export async function markPostAsDeleted(serverUrl: string, post: Post, prepareRe
const model = dbPost.prepareUpdate((p) => {
p.deleteAt = Date.now();
p.message = '';
p.messageSource = '';
p.metadata = null;
p.props = undefined;
});
@@ -241,81 +239,15 @@ export async function storePostsForChannel(
}
}
export async function getPosts(serverUrl: string, ids: string[], sort?: Q.SortOrder) {
export async function getPosts(serverUrl: string, ids: string[]) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
return queryPostsById(database, ids, sort).fetch();
return queryPostsById(database, ids).fetch();
} catch (error) {
return [];
}
}
export async function addPostAcknowledgement(serverUrl: string, postId: string, userId: string, acknowledgedAt: number, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const post = await getPostById(database, postId);
if (!post) {
throw new Error('Post not found');
}
// Check if the post has already been acknowledged by the user
const isAckd = post.metadata?.acknowledgements?.find((a) => a.user_id === userId);
if (isAckd) {
return {error: false};
}
const acknowledgements = [...(post.metadata?.acknowledgements || []), {
user_id: userId,
acknowledged_at: acknowledgedAt,
post_id: postId,
}];
const model = post.prepareUpdate((p) => {
p.metadata = {
...p.metadata,
acknowledgements,
};
});
if (!prepareRecordsOnly) {
await operator.batchRecords([model], 'addPostAcknowledgement');
}
return {model};
} catch (error) {
logError('Failed addPostAcknowledgement', error);
return {error};
}
}
export async function removePostAcknowledgement(serverUrl: string, postId: string, userId: string, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const post = await getPostById(database, postId);
if (!post) {
throw new Error('Post not found');
}
const model = post.prepareUpdate((record) => {
record.metadata = {
...post.metadata,
acknowledgements: post.metadata?.acknowledgements?.filter(
(a) => a.user_id !== userId,
) || [],
};
});
if (!prepareRecordsOnly) {
await operator.batchRecords([model], 'removePostAcknowledgement');
}
return {model};
} catch (error) {
logError('Failed removePostAcknowledgement', error);
return {error};
}
}
export async function deletePosts(serverUrl: string, postIds: string[]) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@@ -343,12 +275,3 @@ export async function deletePosts(serverUrl: string, postIds: string[]) {
return {error};
}
}
export function getUsersCountFromMentions(serverUrl: string, mentions: string[]): Promise<number> {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
return countUsersFromMentions(database, mentions);
} catch (error) {
return Promise.resolve(0);
}
}

View File

@@ -9,12 +9,12 @@ 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 PostModel from '@typings/database/models/servers/post';
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;
@@ -127,6 +127,11 @@ 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();
@@ -135,25 +140,6 @@ export async function dataRetentionCleanup(serverUrl: string) {
return {error: undefined};
}
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
const result = await (isDataRetentionEnabled ? dataRetentionPolicyCleanup(serverUrl) : dataRetentionWithoutPolicyCleanup(serverUrl));
if (!result.error) {
await updateLastDataRetentionRun(serverUrl);
}
await database.unsafeVacuum();
return result;
} catch (error) {
logError('An error occurred while performing data retention cleanup', error);
return {error};
}
}
async function dataRetentionPolicyCleanup(serverUrl: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const globalPolicy = await getGlobalDataRetentionPolicy(database);
const granularPoliciesData = await getGranularDataRetentionPolicies(database);
@@ -210,48 +196,30 @@ async function dataRetentionPolicyCleanup(serverUrl: string) {
Q.unsafeSqlQuery(`SELECT * FROM ${POST} where ${conditions.join(' OR ')}`),
).fetchIds();
return dataRetentionCleanPosts(serverUrl, postIds);
} catch (error) {
logError('An error occurred while performing data retention policy cleanup', error);
return {error};
}
}
async function dataRetentionWithoutPolicyCleanup(serverUrl: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const cutoff = getDataRetentionPolicyCutoff(14); // 14 days
const postIds = await database.get<PostModel>(POST).query(
Q.where('create_at', Q.lt(cutoff)),
).fetchIds();
return dataRetentionCleanPosts(serverUrl, postIds);
} catch (error) {
logError('An error occurred while performing data retention without policy cleanup', error);
return {error};
}
}
async function dataRetentionCleanPosts(serverUrl: string, postIds: string[]) {
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};
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};
}
}
}
}
return {error: undefined};
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

View File

@@ -8,11 +8,11 @@ import DatabaseManager from '@database/manager';
import {getTranslations, t} from '@i18n';
import {getChannelById} from '@queries/servers/channel';
import {getPostById} from '@queries/servers/post';
import {getCurrentTeamId, getCurrentUserId, prepareCommonSystemValues, type PrepareCommonSystemValuesArgs, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {getCurrentTeamId, getCurrentUserId, prepareCommonSystemValues, PrepareCommonSystemValuesArgs, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {addChannelToTeamHistory, addTeamToTeamHistory} from '@queries/servers/team';
import {getThreadById, prepareThreadsFromReceivedPosts, queryThreadsInTeam} from '@queries/servers/thread';
import {getIsCRTEnabled, getThreadById, prepareThreadsFromReceivedPosts, queryThreadsInTeam} from '@queries/servers/thread';
import {getCurrentUser} from '@queries/servers/user';
import {dismissAllModals, dismissAllModalsAndPopToRoot, dismissAllOverlays, goToScreen} from '@screens/navigation';
import {dismissAllModalsAndPopToRoot, goToScreen} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
import NavigationStore from '@store/navigation_store';
import {isTablet} from '@utils/helpers';
@@ -77,23 +77,8 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
const currentTeamId = await getCurrentTeamId(database);
const isTabletDevice = await isTablet();
const teamId = channel.teamId || currentTeamId;
const currentThreadId = EphemeralStore.getCurrentThreadId();
EphemeralStore.setCurrentThreadId(rootId);
if (isFromNotification) {
if (currentThreadId && currentThreadId === rootId && NavigationStore.getScreensInStack().includes(Screens.THREAD)) {
await dismissAllModals();
await dismissAllOverlays();
return {};
}
await dismissAllModalsAndPopToRoot();
await NavigationStore.waitUntilScreenIsTop(Screens.HOME);
if (currentTeamId !== teamId && isTabletDevice) {
DeviceEventEmitter.emit(Navigation.NAVIGATION_HOME, Screens.GLOBAL_THREADS);
}
}
let switchingTeams = false;
if (currentTeamId === teamId) {
const models = await prepareCommonSystemValues(operator, {
currentChannelId: channel.id,
@@ -103,6 +88,7 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
}
} else {
const modelPromises: Array<Promise<Model[]>> = [];
switchingTeams = true;
modelPromises.push(addTeamToTeamHistory(operator, teamId, true));
const commonValues: PrepareCommonSystemValuesArgs = {
currentChannelId: channel.id,
@@ -115,6 +101,25 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
}
}
// Modal right buttons
const rightButtons = [];
const isCRTEnabled = await getIsCRTEnabled(database);
if (isCRTEnabled) {
// CRT: Add follow/following button
rightButtons.push({
id: 'thread-follow-button',
component: {
id: post.id,
name: Screens.THREAD_FOLLOW_BUTTON,
passProps: {
teamId: channel.teamId,
threadId: post.id,
},
},
});
}
// Get translation by user locale
const translations = getTranslations(user.locale);
@@ -130,6 +135,15 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
subtitle = subtitle.replace('{channelName}', channel.displayName);
}
EphemeralStore.setCurrentThreadId(rootId);
if (isFromNotification) {
await dismissAllModalsAndPopToRoot();
await NavigationStore.waitUntilScreenIsTop(Screens.HOME);
if (switchingTeams && isTabletDevice) {
DeviceEventEmitter.emit(Navigation.NAVIGATION_HOME, Screens.GLOBAL_THREADS);
}
}
goToScreen(Screens.THREAD, '', {rootId}, {
topBar: {
title: {
@@ -142,15 +156,14 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
noBorder: true,
scrollEdgeAppearance: {
noBorder: true,
active: true,
},
rightButtons,
},
});
return {};
} catch (error) {
logError('Failed switchToThread', error);
EphemeralStore.setCurrentThreadId('');
return {error};
}
};

View File

@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import General from '@constants/general';
import DatabaseManager from '@database/manager';
import {getRecentCustomStatuses} from '@queries/servers/system';
import {getCurrentUser, getUserById} from '@queries/servers/user';
@@ -12,7 +13,7 @@ import {addRecentReaction} from './reactions';
import type Model from '@nozbe/watermelondb/Model';
import type UserModel from '@typings/database/models/servers/user';
export async function setCurrentUserStatus(serverUrl: string, status: string) {
export async function setCurrentUserStatusOffline(serverUrl: string) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const user = await getCurrentUser(database);
@@ -20,7 +21,7 @@ export async function setCurrentUserStatus(serverUrl: string, status: string) {
throw new Error(`No current user for ${serverUrl}`);
}
user.prepareStatus(status);
user.prepareStatus(General.OFFLINE);
await operator.batchRecords([user], 'setCurrentUserStatusOffline');
return null;
} catch (error) {

View File

@@ -5,10 +5,9 @@ import {sendEphemeralPost} from '@actions/local/post';
import {AppCallResponseTypes} from '@constants/apps';
import NetworkManager from '@managers/network_manager';
import {cleanForm, createCallRequest, makeCallErrorResponse} from '@utils/apps';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
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';
@@ -59,9 +58,15 @@ export async function handleBindingClick<Res=unknown>(serverUrl: string, binding
return doAppSubmit<Res>(serverUrl, callRequest, intl);
}
export async function doAppSubmit<Res=unknown>(serverUrl: string, inCall: AppCallRequest, intl: IntlShape): Promise<{data: AppCallResponse<Res>} | {error: AppCallResponse<Res>}> {
export async function doAppSubmit<Res=unknown>(serverUrl: string, inCall: AppCallRequest, intl: IntlShape) {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error: makeCallErrorResponse((error as ClientError).message)};
}
try {
const client = NetworkManager.getClient(serverUrl);
const call: AppCallRequest = {
...inCall,
context: {
@@ -111,18 +116,23 @@ export async function doAppSubmit<Res=unknown>(serverUrl: string, inCall: AppCal
}
}
} catch (error) {
const errMsg = getFullErrorMessage(error) || intl.formatMessage({
const errMsg = (error as ClientError).message || intl.formatMessage({
id: 'apps.error.responses.unexpected_error',
defaultMessage: 'Received an unexpected error.',
});
logDebug('error on doAppSubmit', getFullErrorMessage(error));
return {error: makeCallErrorResponse(errMsg)};
}
}
export async function doAppFetchForm<Res=unknown>(serverUrl: string, call: AppCallRequest, intl: IntlShape) {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error: makeCallErrorResponse((error as ClientError).message)};
}
try {
const client = NetworkManager.getClient(serverUrl);
const res = await client.executeAppCall<Res>(call, false);
const responseType = res.type || AppCallResponseTypes.OK;
@@ -147,12 +157,11 @@ export async function doAppFetchForm<Res=unknown>(serverUrl: string, call: AppCa
return {error: makeCallErrorResponse(errMsg)};
}
}
} catch (error) {
const errMsg = getFullErrorMessage(error) || intl.formatMessage({
} catch (error: any) {
const errMsg = error.message || intl.formatMessage({
id: 'apps.error.responses.unexpected_error',
defaultMessage: 'Received an unexpected error.',
});
logDebug('error on doAppFetchForm', getFullErrorMessage(error));
return {error: makeCallErrorResponse(errMsg)};
}
}
@@ -161,6 +170,11 @@ export async function doAppLookup<Res=unknown>(serverUrl: string, call: AppCallR
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error: makeCallErrorResponse((error as ClientError).message)};
}
try {
const res = await client.executeAppCall<Res>(call, false);
const responseType = res.type || AppCallResponseTypes.OK;
@@ -179,11 +193,10 @@ export async function doAppLookup<Res=unknown>(serverUrl: string, call: AppCallR
}
}
} catch (error: any) {
const errMsg = getFullErrorMessage(error) || intl.formatMessage({
const errMsg = error.message || intl.formatMessage({
id: 'apps.error.responses.unexpected_error',
defaultMessage: 'Received an unexpected error.',
});
logDebug('error on doAppLookup', getFullErrorMessage(error));
return {error: makeCallErrorResponse(errMsg)};
}
}

View File

@@ -9,20 +9,26 @@ import NetworkManager from '@managers/network_manager';
import {getChannelCategory, queryCategoriesByTeamIds} from '@queries/servers/categories';
import {getChannelById} from '@queries/servers/channel';
import {getCurrentTeamId} from '@queries/servers/system';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {showFavoriteChannelSnackbar} from '@utils/snack_bar';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
export type CategoriesRequest = {
categories?: CategoryWithChannels[];
error?: unknown;
}
export const fetchCategories = async (serverUrl: string, teamId: string, prune = false, fetchOnly = false): Promise<CategoriesRequest> => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const client = NetworkManager.getClient(serverUrl);
const {categories} = await client.getCategories('me', teamId);
if (!fetchOnly) {
@@ -31,17 +37,26 @@ export const fetchCategories = async (serverUrl: string, teamId: string, prune =
return {categories};
} catch (error) {
logDebug('error on fetchCategories', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const toggleFavoriteChannel = async (serverUrl: string, channelId: string, showSnackBar = false) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
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};
}
try {
const {database} = operator;
const channel = await getChannelById(database, channelId);
if (!channel) {
return {error: 'channel not found'};
@@ -94,8 +109,7 @@ export const toggleFavoriteChannel = async (serverUrl: string, channelId: string
return {data: true};
} catch (error) {
logDebug('error on toggleFavoriteChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -14,29 +14,26 @@ 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 {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {tryOpenURL} from '@utils/url';
import type {Client} from '@client/rest';
import type {IntlShape} from 'react-intl';
export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string): Promise<{data?: CommandResponse; error?: unknown}> => {
export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string): Promise<{data?: CommandResponse; error?: string | {message: string}}> => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
return {error: error as ClientErrorProps};
}
const channel = await getChannelById(database, channelId);
const teamId = channel?.teamId || (await getCurrentTeamId(database));
const channel = await getChannelById(operator.database, channelId);
const teamId = channel?.teamId || (await getCurrentTeamId(operator.database));
const args: CommandArgs = {
channel_id: channelId,
@@ -71,8 +68,7 @@ export const executeCommand = async (serverUrl: string, intl: IntlShape, message
try {
data = await client.executeCommand(msg, args);
} catch (error) {
logDebug('error on executeCommand', getFullErrorMessage(error));
return {error};
return {error: error as ClientErrorProps};
}
if (data?.trigger_id) { //eslint-disable-line camelcase
@@ -93,14 +89,14 @@ const executeAppCommand = async (serverUrl: string, intl: IntlShape, parser: App
}
const res = await doAppSubmit(serverUrl, creq, intl);
if ('error' in res) {
const errorResponse = res.error;
if (res.error) {
const errorResponse = res.error as AppCallResponse;
return createErrorMessage(errorResponse.text || intl.formatMessage({
id: 'apps.error.unknown',
defaultMessage: 'Unknown error.',
}));
}
const callResp = res.data;
const callResp = res.data as AppCallResponse;
switch (callResp.type) {
case AppCallResponseTypes.OK:
@@ -162,22 +158,31 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc
return {data: true};
};
export const fetchCommands = async (serverUrl: string, teamId: string): Promise<{commands: Command[]} | {error: unknown}> => {
export const fetchCommands = async (serverUrl: string, teamId: string) => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error: error as ClientErrorProps};
}
try {
const client = NetworkManager.getClient(serverUrl);
return {commands: await client.getCommandsList(teamId)};
} catch (error) {
logDebug('error on fetchCommands', getFullErrorMessage(error));
return {error};
return {error: error as ClientErrorProps};
}
};
export const fetchSuggestions = async (serverUrl: string, term: string, teamId: string, channelId: string, rootId?: string): Promise<{suggestions: AutocompleteSuggestion[]} | {error: unknown}> => {
export const fetchSuggestions = async (serverUrl: string, term: string, teamId: string, channelId: string, rootId?: string) => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error: error as ClientErrorProps};
}
try {
const client = NetworkManager.getClient(serverUrl);
return {suggestions: await client.getCommandAutocompleteSuggestionsList(term, teamId, channelId, rootId)};
} catch (error) {
logDebug('error on fetchSuggestions', getFullErrorMessage(error));
return {error};
return {error: error as ClientErrorProps};
}
};

View File

@@ -7,14 +7,23 @@ 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 {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
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 {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
const data = await client.getCustomEmojis(page, perPage, sort);
await operator.handleCustomEmojis({
emojis: data,
@@ -23,20 +32,29 @@ export const fetchCustomEmojis = async (serverUrl: string, page = 0, perPage = G
return {data};
} catch (error) {
logDebug('error on fetchCustomEmojis', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const searchCustomEmojis = async (serverUrl: string, term: string) => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const data = await client.searchCustomEmoji(term);
if (data.length) {
const names = data.map((c) => c.name);
const exist = await queryCustomEmojisByName(database, names).fetch();
const exist = await queryCustomEmojisByName(operator.database, names).fetch();
const existingNames = new Set(exist.map((e) => e.name));
const emojis = data.filter((d) => !existingNames.has(d.name));
await operator.handleCustomEmojis({
@@ -46,22 +64,31 @@ export const searchCustomEmojis = async (serverUrl: string, term: string) => {
}
return {data};
} catch (error) {
logDebug('error on searchCustomEmojis', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
const names = new Set<string>();
const debouncedFetchEmojiByNames = debounce(async (serverUrl: string) => {
let client: Client;
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const promises: Array<Promise<CustomEmoji>> = [];
for (const name of names) {
promises.push(client.getCustomEmojiByName(name));
}
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const promises: Array<Promise<CustomEmoji>> = [];
for (const name of names) {
promises.push(client.getCustomEmojiByName(name));
}
try {
const emojisResult = await Promise.allSettled(promises);
const emojis = emojisResult.reduce<CustomEmoji[]>((result, e) => {
if (e.status === 'fulfilled') {
@@ -72,9 +99,8 @@ const debouncedFetchEmojiByNames = debounce(async (serverUrl: string) => {
if (emojis.length) {
await operator.handleCustomEmojis({emojis, prepareRecordsOnly: false});
}
return {};
return {error: undefined};
} catch (error) {
logDebug('error on debouncedFetchEmojiByNames', getFullErrorMessage(error));
return {error};
}
}, 200, false, () => {

View File

@@ -1,38 +1,87 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {setLastServerVersionCheck} from '@actions/local/systems';
import {dataRetentionCleanup, setLastServerVersionCheck} from '@actions/local/systems';
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, getCurrentChannelId, getConfig, getLicense} from '@queries/servers/system';
import {getCurrentUser} from '@queries/servers/user';
import {setTeamLoading} from '@store/team_load_store';
import {deleteV1Data} from '@utils/file';
import {isTablet} from '@utils/helpers';
import {logInfo} from '@utils/log';
import {verifyPushProxy} from './common';
import {handleEntryAfterLoadNavigation, 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) {
registerDeviceToken(serverUrl);
if (Object.keys(DatabaseManager.serverDatabases).length === 1) {
await setLastServerVersionCheck(serverUrl, true);
}
}
// Run data retention cleanup
await dataRetentionCleanup(serverUrl);
// clear lastUnreadChannelId
const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''});
if (removeLastUnreadChannelId) {
await operator.batchRecords(removeLastUnreadChannelId, 'appEntry - removeLastUnreadChannelId');
}
WebsocketManager.openAll();
const {database} = operator;
const currentTeamId = await getCurrentTeamId(database);
const currentChannelId = await getCurrentChannelId(database);
const lastDisconnectedAt = (await getWebSocketLastDisconnected(database)) || since;
setTeamLoading(serverUrl, true);
const entryData = await entry(serverUrl, currentTeamId, currentChannelId, since);
if ('error' in entryData) {
setTeamLoading(serverUrl, false);
return {error: entryData.error};
}
const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData} = entryData;
if (isUpgrade && meData?.user) {
const isTabletDevice = await isTablet();
const me = await prepareCommonSystemValues(operator, {
currentUserId: meData.user.id,
currentTeamId: initialTeamId,
currentChannelId: isTabletDevice ? initialChannelId : undefined,
});
if (me?.length) {
await operator.batchRecords(me, 'appEntry - upgrade store me');
}
}
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId, currentChannelId, initialTeamId, initialChannelId);
const dt = Date.now();
await operator.batchRecords(models, 'appEntry');
logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
setTeamLoading(serverUrl, false);
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);
if (!since) {
// Load data from other servers
syncOtherServers(serverUrl);
}
verifyPushProxy(serverUrl);
return {};
return {userId: currentUserId};
}
export async function upgradeEntry(serverUrl: string) {
@@ -40,7 +89,7 @@ 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) {

View File

@@ -1,15 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, type MyChannelsRequest} from '@actions/remote/channel';
import {dataRetentionCleanup} from '@actions/local/systems';
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, MyChannelsRequest} from '@actions/remote/channel';
import {fetchGroupsForMember} from '@actions/remote/groups';
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
import {type MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
import {fetchRoles} from '@actions/remote/role';
import {fetchConfigAndLicense} from '@actions/remote/systems';
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, type MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
import {syncTeamThreads} from '@actions/remote/thread';
import {fetchMe, type MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
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,18 +21,21 @@ 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 {getAllServers} 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, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
import NavigationStore from '@store/navigation_store';
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
import {getFullErrorMessage, isErrorWithStatusCode} from '@utils/errors';
import {getMemberChannelsFromGQLQuery, gqlToClientChannelMembership} from '@utils/graphql';
import {isTablet} from '@utils/helpers';
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 = {
@@ -45,7 +50,7 @@ export type AppEntryData = {
}
export type AppEntryError = {
error: unknown;
error: Error | ClientError | string;
}
export type EntryResponse = {
@@ -74,8 +79,8 @@ export const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[])
if (!operator) {
return [];
}
const {database} = operator;
const {database} = operator;
if (removeTeamIds?.length) {
// Immediately delete myTeams so that the UI renders only teams the user is a member of.
const removeMyTeams = await queryMyTeamsByIds(database, removeTeamIds).fetch();
@@ -95,6 +100,7 @@ export const entryRest = async (serverUrl: string, teamId?: string, channelId?:
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
const lastDisconnectedAt = since || await getWebSocketLastDisconnected(database);
@@ -105,12 +111,6 @@ export const entryRest = async (serverUrl: string, teamId?: string, channelId?:
}
const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds, isCRTEnabled} = fetchedData;
const chError = chData?.error;
if (isErrorWithStatusCode(chError) && 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};
@@ -208,8 +208,8 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
}
const inTeam = teamData.teams?.find((t) => t.id === initialTeamId);
const chError = chData?.error;
if ((!inTeam && !teamData.error) || (isErrorWithStatusCode(chError) && chError.status_code === 403)) {
const chError = chData?.error as ClientError | undefined;
if ((!inTeam && !teamData.error) || chError?.status_code === 403) {
// User is no longer a member of the current team
if (!removeTeamIds.includes(initialTeamId)) {
removeTeamIds.push(initialTeamId);
@@ -253,8 +253,8 @@ export const fetchAlternateTeamData = async (
for (const teamId of availableTeamIds) {
// eslint-disable-next-line no-await-in-loop
chData = await fetchMyChannelsForTeam(serverUrl, teamId, includeDeleted, since, fetchOnly, false, isCRTEnabled);
const chError = chData.error;
if (isErrorWithStatusCode(chError) && chError.status_code === 403) {
const chError = chData.error as ClientError | undefined;
if (chError?.status_code === 403) {
removeTeamIds.push(teamId);
} else {
initialTeamId = teamId;
@@ -311,8 +311,13 @@ 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);
}
@@ -344,11 +349,8 @@ export async function restDeferredAppEntryActions(
// 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);
}
@@ -356,38 +358,151 @@ export async function restDeferredAppEntryActions(
}
export const registerDeviceToken = async (serverUrl: string) => {
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const deviceToken = await getDeviceToken();
if (deviceToken) {
client.attachDevice(deviceToken);
}
return {};
client = NetworkManager.getClient(serverUrl);
} catch (error) {
logDebug('error on registerDeviceToken', getFullErrorMessage(error));
return {error};
}
const deviceToken = await getDeviceToken();
if (deviceToken) {
client.attachDevice(deviceToken);
}
return {error: undefined};
};
export const syncOtherServers = async (serverUrl: string) => {
const servers = await getAllServers();
for (const server of servers) {
if (server.url !== serverUrl && server.lastActiveAt > 0) {
registerDeviceToken(server.url);
syncAllChannelMembersAndThreads(server.url).then(() => {
dataRetentionCleanup(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 {database} = operator;
const response = await gqlAllChannels(serverUrl);
if ('error' in response) {
return response.error;
}
if (response.errors) {
return response.errors[0].message;
}
const userId = await getCurrentUserId(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) {
await operator.batchRecords(models, 'graphQLSyncAllChannelMembers');
}
}
const isCRTEnabled = await getIsCRTEnabled(database);
if (isCRTEnabled) {
const myTeams = await queryMyTeams(operator.database).fetch();
for await (const myTeam of myTeams) {
// need to await here since GM/DM threads in different teams overlap
await syncTeamThreads(serverUrl, myTeam.id);
}
}
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 await (const myTeam of myTeams) {
fetchMyChannelsForTeam(serverUrl, myTeam.id, false, 0, false, excludeDirect);
excludeDirect = true;
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
// need to await here since GM/DM threads in different teams overlap
await syncTeamThreads(serverUrl, myTeam.id);
}
}
} catch {
// Do nothing
}
};
export async function verifyPushProxy(serverUrl: string) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
const {database} = operator;
const ppVerification = await getPushVerificationStatus(database);
if (
ppVerification !== PUSH_PROXY_STATUS_UNKNOWN &&
ppVerification !== ''
) {
return;
}
const deviceId = await getDeviceToken();
if (!deviceId) {
return;
}
let client;
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (err) {
return;
}
const ppVerification = await getPushVerificationStatus(database);
if (
ppVerification !== PUSH_PROXY_STATUS_UNKNOWN &&
ppVerification !== ''
) {
return;
}
const client = NetworkManager.getClient(serverUrl);
try {
const response = await client.ping(deviceId);
const canReceiveNotifications = response?.data?.CanReceiveNotifications;
switch (canReceiveNotifications) {
@@ -399,9 +514,7 @@ export async function verifyPushProxy(serverUrl: string) {
default:
operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_VERIFIED}], prepareRecordsOnly: false});
}
} catch (error) {
logDebug('error on verifyPushProxy', getFullErrorMessage(error));
} catch (err) {
// Do nothing
}
}
@@ -425,14 +538,7 @@ export async function handleEntryAfterLoadNavigation(
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) {
if (currentTeamIdAfterLoad !== currentTeamId) {
// Switched teams while loading
if (!teamMembers.find((t) => t.team_id === currentTeamIdAfterLoad && t.delete_at === 0)) {
await handleKickFromTeam(serverUrl, currentTeamIdAfterLoad);

View File

@@ -5,11 +5,11 @@ import {storeConfigAndLicense} from '@actions/local/systems';
import {fetchGroupsForMember} from '@actions/remote/groups';
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
import {fetchDataRetentionPolicy} from '@actions/remote/systems';
import {type MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
import {MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
import {syncTeamThreads} from '@actions/remote/thread';
import {autoUpdateTimezone, fetchProfilesInGroupChannels, updateAllUsersSince} from '@actions/remote/user';
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';
@@ -17,19 +17,17 @@ import {queryAllChannels, queryAllChannelsForTeam} from '@queries/servers/channe
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
import {getHasCRTChanged} from '@queries/servers/preference';
import {getConfig, getIsDataRetentionEnabled} from '@queries/servers/system';
import {getFullErrorMessage} from '@utils/errors';
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, type EntryResponse, entryInitialChannelId, restDeferredAppEntryActions, getRemoveTeamIds} from './common';
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,
@@ -100,19 +98,6 @@ export async function deferredAppEntryGraphQLActions(
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};
}
@@ -141,7 +126,7 @@ const getChannelData = async (serverUrl: string, initialTeamId: string, userId:
const request = exclude ? gqlOtherChannels : gqlEntryChannels;
response = await request(serverUrl, initialTeamId);
} catch (error) {
return {error: getFullErrorMessage(error)};
return {error: (error as ClientError).message};
}
if ('error' in response) {
@@ -175,7 +160,7 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
try {
response = await gqlEntry(serverUrl);
} catch (error) {
return {error: getFullErrorMessage(error)};
return {error: (error as ClientError).message};
}
if ('error' in response) {

View File

@@ -1,34 +1,81 @@
// 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 {setTeamLoading} from '@store/team_load_store';
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?: unknown}> {
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);
setTeamLoading(serverUrl, true);
const entryData = await entry(serverUrl, '', '');
if ('error' in entryData) {
setTeamLoading(serverUrl, false);
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, 'loginEntry');
setTeamLoading(serverUrl, false);
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};
}

View File

@@ -1,45 +1,47 @@
// 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 {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 {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 {isErrorWithStatusCode} from '@utils/errors';
import NavigationStore from '@store/navigation_store';
import {setTeamLoading} from '@store/team_load_store';
import {isTablet} from '@utils/helpers';
import {emitNotificationError} from '@utils/notification';
import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme';
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';
export async function pushNotificationEntry(serverUrl: string, notification: NotificationData) {
// 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!;
import {syncOtherServers} from './common';
import {deferredAppEntryActions, entry} from './gql_common';
export async function pushNotificationEntry(serverUrl: string, notification: NotificationWithData) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
// We only reach this point if we have a channel Id in the notification payload
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;
}
@@ -59,62 +61,91 @@ 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 (isErrorWithStatusCode(resp.error) && resp.error.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 (isErrorWithStatusCode(resp.error) && resp.error.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;
}
setTeamLoading(serverUrl, true);
const entryData = await entry(serverUrl, teamId, channelId);
if ('error' in entryData) {
setTeamLoading(serverUrl, false);
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, 'pushNotificationEntry');
setTeamLoading(serverUrl, false);
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};
}

View File

@@ -3,12 +3,12 @@
import {DOWNLOAD_TIMEOUT} from '@constants/network';
import NetworkManager from '@managers/network_manager';
import {getFullErrorMessage} from '@utils/errors';
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
@@ -30,23 +30,29 @@ export const uploadFile = (
onError: (response: ClientResponseError) => void = () => {/*Do Nothing*/},
skipBytes = 0,
) => {
let client: Client;
try {
const client = NetworkManager.getClient(serverUrl);
client = NetworkManager.getClient(serverUrl);
return {cancel: client.uploadPostAttachment(file, channelId, onProgress, onComplete, onError, skipBytes)};
} catch (error) {
logDebug('error on uploadFile', getFullErrorMessage(error));
return {error};
logDebug('uploadFile', error);
return {error: error as ClientError};
}
};
export const fetchPublicLink = async (serverUrl: string, fileId: string) => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error: error as ClientError};
}
try {
const client = NetworkManager.getClient(serverUrl);
const publicLink = await client!.getFilePublicLink(fileId);
return publicLink;
} catch (error) {
logDebug('error on fetchPublicLink', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};

View File

@@ -8,8 +8,6 @@ import {t} from '@i18n';
import NetworkManager from '@managers/network_manager';
import {getDeviceToken} from '@queries/app/global';
import {getExpandedLinks, getPushVerificationStatus} from '@queries/servers/system';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {forceLogoutIfNecessary} from './session';
@@ -65,12 +63,10 @@ export const doPing = async (serverUrl: string, verifyPushProxy: boolean, timeou
}
if (!response.ok) {
logDebug('Server ping returned not ok response', response);
NetworkManager.invalidateClient(serverUrl);
return {error: {intl: pingError}};
}
} catch (error) {
logDebug('Server ping threw an exception', getFullErrorMessage(error));
NetworkManager.invalidateClient(serverUrl);
return {error: {intl: pingError}};
}
@@ -83,19 +79,29 @@ export const doPing = async (serverUrl: string, verifyPushProxy: boolean, timeou
canReceiveNotifications = PUSH_PROXY_RESPONSE_VERIFIED;
}
return {canReceiveNotifications};
return {canReceiveNotifications, error: undefined};
}
return {};
return {error: undefined};
};
export const getRedirectLocation = async (serverUrl: string, link: string) => {
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};
}
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const expandedLink = await client.getRedirectLocation(link);
if (expandedLink?.location) {
const storedLinks = await getExpandedLinks(database);
const storedLinks = await getExpandedLinks(operator.database);
storedLinks[link] = expandedLink.location;
const expanded: IdValue = {
id: SYSTEM_IDENTIFIERS.EXPANDED_LINKS,
@@ -109,8 +115,7 @@ export const getRedirectLocation = async (serverUrl: string, link: string) => {
return {expandedLink};
} catch (error) {
logDebug('error on getRedirectLocation', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};

View File

@@ -4,23 +4,30 @@
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {getChannelById} from '@queries/servers/channel';
import {getLicense} from '@queries/servers/system';
import {getTeamById} from '@queries/servers/team';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
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);
const client: Client = NetworkManager.getClient(serverUrl);
const group = await client.getGroup(id);
// Save locally
return operator.handleGroups({groups: [group], prepareRecordsOnly: fetchOnly});
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const fetchGroupsForAutocomplete = async (serverUrl: string, query: string, fetchOnly = false) => {
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || license.IsLicensed !== 'true') {
return [];
}
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client: Client = NetworkManager.getClient(serverUrl);
const response = await client.getGroups({query, includeMemberCount: true});
@@ -30,19 +37,14 @@ export const fetchGroupsForAutocomplete = async (serverUrl: string, query: strin
return operator.handleGroups({groups: response, prepareRecordsOnly: fetchOnly});
} catch (error) {
logDebug('error on fetchGroupsForAutocomplete', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const fetchGroupsByNames = async (serverUrl: string, names: string[], fetchOnly = false) => {
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || license.IsLicensed !== 'true') {
return [];
}
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client: Client = NetworkManager.getClient(serverUrl);
const promises: Array <Promise<Group[]>> = [];
@@ -60,20 +62,14 @@ export const fetchGroupsByNames = async (serverUrl: string, names: string[], fet
return operator.handleGroups({groups, prepareRecordsOnly: fetchOnly});
} catch (error) {
logDebug('error on fetchGroupsByNames', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const fetchGroupsForChannel = async (serverUrl: string, channelId: string, fetchOnly = false) => {
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || license.IsLicensed !== 'true') {
return {groups: [], groupChannels: []};
}
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
const response = await client.getAllGroupsAssociatedToChannel(channelId);
@@ -92,19 +88,14 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string
return {groups, groupChannels};
} catch (error) {
logDebug('error on fetchGroupsForChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetchOnly = false) => {
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || license.IsLicensed !== 'true') {
return {groups: [], groupTeams: []};
}
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client: Client = NetworkManager.getClient(serverUrl);
const response = await client.getAllGroupsAssociatedToTeam(teamId);
@@ -124,18 +115,13 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc
return {groups, groupTeams};
} catch (error) {
logDebug('error on fetchGroupsForTeam', getFullErrorMessage(error));
return {error};
}
};
export const fetchGroupsForMember = async (serverUrl: string, userId: string, fetchOnly = false) => {
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || license.IsLicensed !== 'true') {
return {groups: [], groupMemberships: []};
}
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client: Client = NetworkManager.getClient(serverUrl);
const response = await client.getAllGroupsAssociatedToMembership(userId);
@@ -155,25 +141,36 @@ export const fetchGroupsForMember = async (serverUrl: string, userId: string, fe
return {groups, groupMemberships};
} catch (error) {
logDebug('error on fetchGroupsForMember', getFullErrorMessage(error));
return {error};
}
};
export const fetchFilteredTeamGroups = async (serverUrl: string, searchTerm: string, teamId: string) => {
const res = await fetchGroupsForTeam(serverUrl, teamId);
if ('error' in res) {
return {error: res.error};
try {
const groups = await fetchGroupsForTeam(serverUrl, teamId);
if (groups && Array.isArray(groups)) {
return groups.filter((g) => g.name.toLowerCase().includes(searchTerm.toLowerCase()));
}
throw groups.error;
} catch (error) {
return {error};
}
return res.groups.filter((g) => g.name.toLowerCase().includes(searchTerm.toLowerCase()));
};
export const fetchFilteredChannelGroups = async (serverUrl: string, searchTerm: string, channelId: string) => {
const res = await fetchGroupsForChannel(serverUrl, channelId);
if ('error' in res) {
return {error: res.error};
try {
const groups = await fetchGroupsForChannel(serverUrl, channelId);
if (groups && Array.isArray(groups)) {
return groups.filter((g) => g.name.toLowerCase().includes(searchTerm.toLowerCase()));
}
throw groups.error;
} catch (error) {
return {error};
}
return res.groups.filter((g) => g.name.toLowerCase().includes(searchTerm.toLowerCase()));
};
export const fetchGroupsForTeamIfConstrained = async (serverUrl: string, teamId: string, fetchOnly = false) => {

View File

@@ -6,31 +6,45 @@ import DatabaseManager from '@database/manager';
import IntegrationsMananger from '@managers/integrations_manager';
import NetworkManager from '@managers/network_manager';
import {getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {forceLogoutIfNecessary} from './session';
export const submitInteractiveDialog = async (serverUrl: string, submission: DialogSubmission) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
import type {Client} from '@client/rest';
submission.channel_id = await getCurrentChannelId(database);
submission.team_id = await getCurrentTeamId(database);
export const submitInteractiveDialog = async (serverUrl: string, submission: DialogSubmission) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
submission.channel_id = await getCurrentChannelId(database);
submission.team_id = await getCurrentTeamId(database);
try {
const data = await client.submitInteractiveDialog(submission);
return {data};
} catch (error) {
logDebug('error on submitInteractiveDialog', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const postActionWithCookie = async (serverUrl: string, postId: string, actionId: string, actionCookie: string, selectedOption = '') => {
let client: Client;
try {
const client = NetworkManager.getClient(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const data = await client.doPostActionWithCookie(postId, actionId, actionCookie, selectedOption);
if (data?.trigger_id) {
IntegrationsMananger.getManager(serverUrl)?.setTriggerId(data.trigger_id);
@@ -38,8 +52,7 @@ export const postActionWithCookie = async (serverUrl: string, postId: string, ac
return {data};
} catch (error) {
logDebug('error on postActionWithCookie', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};

View File

@@ -3,37 +3,34 @@
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 channelId = notification.payload?.channel_id;
if (!channelId) {
return {error: 'No chanel Id was specified'};
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channelId = notification.payload?.channel_id;
if (!channelId) {
return {error: 'No chanel Id was specified'};
}
const {database} = operator;
const currentTeamId = await getCurrentTeamId(database);
let teamId = notification.payload?.team_id;
let isDirectChannel = false;
@@ -92,107 +89,44 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati
}
return {};
} catch (error) {
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
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);
}
};
export const openNotification = async (serverUrl: string, notification: NotificationWithData) => {
// Wait for initial launch to kick in if needed
await new Promise((r) => setTimeout(r, 500));
if (EphemeralStore.getProcessingNotification() === notification.identifier) {
return {};
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
EphemeralStore.setNotificationTapped(true);
const channelId = notification.payload!.channel_id!;
const rootId = notification.payload!.root_id!;
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
EphemeralStore.setNotificationTapped(true);
const {database} = operator;
const channelId = notification.payload!.channel_id!;
const rootId = notification.payload!.root_id!;
const isCRTEnabled = await getIsCRTEnabled(database);
const isThreadNotification = isCRTEnabled && Boolean(rootId);
@@ -231,7 +165,7 @@ export const openNotification = async (serverUrl: string, notification: Notifica
}
return switchToChannelById(serverUrl, channelId, teamId);
} catch (error) {
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};

View File

@@ -3,8 +3,6 @@
import {General} from '@constants';
import NetworkManager from '@managers/network_manager';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
export const isNPSEnabled = async (serverUrl: string) => {
try {
@@ -17,7 +15,6 @@ export const isNPSEnabled = async (serverUrl: string) => {
}
return false;
} catch (error) {
logDebug('error on isNPSEnabled', getFullErrorMessage(error));
return false;
}
};
@@ -28,7 +25,6 @@ export const giveFeedbackAction = async (serverUrl: string) => {
const post = await client.npsGiveFeedbackAction();
return {post};
} catch (error) {
logDebug('error on giveFeedbackAction', getFullErrorMessage(error));
return {error};
}
};

View File

@@ -9,9 +9,12 @@ import {displayPermalink} from '@utils/permalink';
import type TeamModel from '@typings/database/models/servers/team';
export const showPermalink = async (serverUrl: string, teamName: string, postId: string, openAsPermalink = true) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
try {
let name = teamName;
let team: TeamModel | undefined;
if (!name || name === DeepLink.Redirect) {
@@ -23,7 +26,7 @@ export const showPermalink = async (serverUrl: string, teamName: string, postId:
await displayPermalink(name, postId, openAsPermalink);
return {};
return {error: undefined};
} catch (error) {
return {error};
}

View File

@@ -4,11 +4,13 @@
/* eslint-disable max-lines */
import {DeviceEventEmitter} from 'react-native';
import {markChannelAsUnread, updateLastPostAt} from '@actions/local/channel';
import {addPostAcknowledgement, removePost, removePostAcknowledgement, storePostsForChannel} from '@actions/local/post';
import {removePost, storePostsForChannel} from '@actions/local/post';
import {addRecentReaction} from '@actions/local/reactions';
import {createThreadFromNewPost} from '@actions/local/thread';
import {ActionType, General, Post, ServerErrors} from '@constants';
import {ActionType, Events, General, Post, ServerErrors} from '@constants';
import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
import {filterPostsInOrderedArray} from '@helpers/api/post';
@@ -21,11 +23,10 @@ import {getPostById, getRecentPostsInChannel} from '@queries/servers/post';
import {getCurrentUserId, getCurrentChannelId} from '@queries/servers/system';
import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers/thread';
import {queryAllUsers} from '@queries/servers/user';
import EphemeralStore from '@store/ephemeral_store';
import {setFetchingThreadState} from '@store/fetching_thread_store';
import {getValidEmojis, matchEmoticons} from '@utils/emoji/helpers';
import {getFullErrorMessage, isServerError} from '@utils/errors';
import {logDebug, logError} from '@utils/log';
import {isServerError} from '@utils/errors';
import {logError} from '@utils/log';
import {processPostsFetched} from '@utils/post';
import {getPostIdsForCombinedUserActivityPost} from '@utils/post_list';
@@ -59,7 +60,6 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
let client: Client;
try {
@@ -68,6 +68,8 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
return {error};
}
const {database} = operator;
const currentUserId = await getCurrentUserId(database);
const timestamp = Date.now();
const pendingPostId = post.pending_post_id || `${currentUserId}:${timestamp}`;
@@ -132,9 +134,8 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
let created;
try {
created = await client.createPost({...newPost, create_at: 0});
created = await client.createPost(newPost);
} catch (error) {
logDebug('Error sending a post', getFullErrorMessage(error));
const errorPost = {
...newPost,
id: pendingPostId,
@@ -203,7 +204,6 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
let client: Client;
try {
@@ -212,6 +212,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
return {error};
}
const {database} = operator;
const isCRTEnabled = await getIsCRTEnabled(database);
try {
@@ -254,7 +255,6 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
}
await operator.batchRecords(models, 'retryFailedPost - success update');
} catch (error) {
logDebug('error on retryFailedPost', getFullErrorMessage(error));
if (isServerError(error) && (
error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
error.server_error_id === ServerErrors.TOWN_SQUARE_READ_ONLY_ERROR ||
@@ -274,7 +274,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
return {error};
}
return {};
return {error: undefined};
};
export const fetchPostsForCurrentChannel = async (serverUrl: string) => {
@@ -289,9 +289,6 @@ export const fetchPostsForCurrentChannel = async (serverUrl: string) => {
export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false) {
try {
if (!fetchOnly) {
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
}
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
let postAction: Promise<PostsRequest>|undefined;
let actionType: string|undefined;
@@ -310,6 +307,7 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
if (data.error) {
throw data.error;
}
let authors: UserProfile[] = [];
if (data.posts?.length && data.order?.length) {
const {authors: fetchedAuthors} = await fetchPostAuthors(serverUrl, data.posts, true);
@@ -326,40 +324,39 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
return {posts: data.posts, order: data.order, authors, actionType, previousPostId: data.previousPostId};
} catch (error) {
logDebug('error on fetchPostsForChannel', getFullErrorMessage(error));
logError('FetchPostsForChannel', error);
return {error};
} finally {
if (!fetchOnly) {
EphemeralStore.stopLoadingMessagesForChannel(serverUrl, channelId);
}
}
}
export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string) => {
const promises = [];
for (const member of memberships) {
const channel = channels.find((c) => c.id === member.channel_id);
if (channel && !channel.delete_at && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) {
promises.push(fetchPostsForChannel(serverUrl, channel.id));
try {
const promises = [];
for (const member of memberships) {
const channel = channels.find((c) => c.id === member.channel_id);
if (channel && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) {
promises.push(fetchPostsForChannel(serverUrl, channel.id));
}
}
await Promise.all(promises);
} catch (error) {
return {error};
}
await Promise.all(promises);
return {error: undefined};
};
export async function fetchPosts(serverUrl: string, channelId: string, page = 0, perPage = General.POST_CHUNK_SIZE, fetchOnly = false): Promise<PostsRequest> {
try {
if (!fetchOnly) {
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
}
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
const isCRTEnabled = await getIsCRTEnabled(database);
const isCRTEnabled = await getIsCRTEnabled(operator.database);
const data = await client.getPosts(channelId, page, perPage, isCRTEnabled, isCRTEnabled);
const result = processPostsFetched(data);
if (!fetchOnly && result.posts.length) {
if (!fetchOnly) {
const models = await operator.handlePosts({
...result,
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
actionType: ActionType.POSTS.RECEIVED_SINCE,
prepareRecordsOnly: true,
});
@@ -382,27 +379,38 @@ export async function fetchPosts(serverUrl: string, channelId: string, page = 0,
}
return result;
} catch (error) {
logDebug('error on fetchPosts', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
} finally {
if (!fetchOnly) {
EphemeralStore.stopLoadingMessagesForChannel(serverUrl, channelId);
}
}
}
export async function fetchPostsBefore(serverUrl: string, channelId: string, postId: string, perPage = General.POST_CHUNK_SIZE, fetchOnly = false) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
let client: Client;
try {
if (!fetchOnly) {
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
try {
if (activeServerUrl === serverUrl) {
DeviceEventEmitter.emit(Events.LOADING_CHANNEL_POSTS, true);
}
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const isCRTEnabled = await getIsCRTEnabled(database);
const isCRTEnabled = await getIsCRTEnabled(operator.database);
const data = await client.getPostsBefore(channelId, postId, 0, perPage, isCRTEnabled, isCRTEnabled);
const result = processPostsFetched(data);
if (activeServerUrl === serverUrl) {
DeviceEventEmitter.emit(Events.LOADING_CHANNEL_POSTS, false);
}
if (result.posts.length && !fetchOnly) {
try {
const models = await operator.handlePosts({
@@ -431,27 +439,32 @@ export async function fetchPostsBefore(serverUrl: string, channelId: string, pos
logError('FETCH POSTS BEFORE ERROR', error);
}
}
return result;
} catch (error) {
logDebug('error on fetchPostsBefore', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
} finally {
if (!fetchOnly) {
EphemeralStore.stopLoadingMessagesForChannel(serverUrl, channelId);
if (activeServerUrl === serverUrl) {
DeviceEventEmitter.emit(Events.LOADING_CHANNEL_POSTS, false);
}
return {error};
}
}
export async function fetchPostsSince(serverUrl: string, channelId: string, since: number, fetchOnly = false): Promise<PostsRequest> {
try {
if (!fetchOnly) {
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
}
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const isCRTEnabled = await getIsCRTEnabled(database);
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const isCRTEnabled = await getIsCRTEnabled(operator.database);
const data = await client.getPostsSince(channelId, since, isCRTEnabled, isCRTEnabled);
const result = await processPostsFetched(data);
if (!fetchOnly) {
@@ -480,42 +493,47 @@ export async function fetchPostsSince(serverUrl: string, channelId: string, sinc
}
return result;
} catch (error) {
logDebug('error on fetchPostsSince', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
} finally {
if (!fetchOnly) {
EphemeralStore.stopLoadingMessagesForChannel(serverUrl, channelId);
}
}
}
export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOnly = false): Promise<AuthorsRequest> => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
let client: Client;
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const currentUserId = await getCurrentUserId(database);
const users = await queryAllUsers(database).fetch();
const existingUserIds = new Set<string>();
const existingUserNames = new Set<string>();
let excludeUsername;
users.forEach((u) => {
existingUserIds.add(u.id);
existingUserNames.add(u.username);
if (u.id === currentUserId) {
excludeUsername = u.username;
}
});
const usernamesToLoad = getNeededAtMentionedUsernames(existingUserNames, posts, excludeUsername);
const userIdsToLoad = new Set<string>();
for (const p of posts) {
const {user_id} = p;
if (user_id !== currentUserId) {
userIdsToLoad.add(user_id);
}
const currentUserId = await getCurrentUserId(operator.database);
const users = await queryAllUsers(operator.database).fetch();
const existingUserIds = new Set<string>();
const existingUserNames = new Set<string>();
let excludeUsername;
users.forEach((u) => {
existingUserIds.add(u.id);
existingUserNames.add(u.username);
if (u.id === currentUserId) {
excludeUsername = u.username;
}
});
const usernamesToLoad = getNeededAtMentionedUsernames(existingUserNames, posts, excludeUsername);
const userIdsToLoad = new Set<string>();
for (const p of posts) {
const {user_id} = p;
if (user_id !== currentUserId) {
userIdsToLoad.add(user_id);
}
}
try {
const promises: Array<Promise<UserProfile[]>> = [];
if (userIdsToLoad.size) {
promises.push(client.getProfilesByIds(Array.from(userIdsToLoad)));
@@ -547,20 +565,29 @@ export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOn
return {authors: [] as UserProfile[]};
} catch (error) {
logDebug('error on fetchPostAuthors', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
logError('FETCH AUTHORS ERROR', error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export async function fetchPostThread(serverUrl: string, postId: string, options?: FetchPaginatedThreadOptions, fetchOnly = false) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
let client: Client;
try {
setFetchingThreadState(postId, true);
const client = NetworkManager.getClient(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
setFetchingThreadState(postId, true);
const isCRTEnabled = await getIsCRTEnabled(database);
try {
const isCRTEnabled = await getIsCRTEnabled(operator.database);
// Not doing any version check as server versions below 6.7 will ignore the additional params from the client.
const data = await client.getPostThread(postId, {
@@ -599,18 +626,26 @@ export async function fetchPostThread(serverUrl: string, postId: string, options
setFetchingThreadState(postId, false);
return {posts: extractRecordsForTable<PostModel>(posts, MM_TABLES.SERVER.POST)};
} catch (error) {
logDebug('error on fetchPostThread', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
setFetchingThreadState(postId, false);
return {error};
}
}
export async function fetchPostsAround(serverUrl: string, channelId: string, postId: string, perPage = General.POST_AROUND_CHUNK_SIZE, isCRTEnabled = false) {
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
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};
}
try {
const [after, post, before] = await Promise.all<PostsObjectsRequest>([
client.getPostsAfter(channelId, postId, 0, perPage, isCRTEnabled, isCRTEnabled),
client.getPostThread(postId, {
@@ -667,18 +702,27 @@ export async function fetchPostsAround(serverUrl: string, channelId: string, pos
return {posts: extractRecordsForTable<PostModel>(posts, MM_TABLES.SERVER.POST)};
} catch (error) {
logDebug('error on fetchPostsAround', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
logError('FETCH POSTS AROUND ERROR', error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
}
export async function fetchMissingChannelsFromPosts(serverUrl: string, posts: Post[], fetchOnly = false) {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const channelIds = new Set(await queryAllMyChannel(database).fetchIds());
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const channelIds = new Set(await queryAllMyChannel(operator.database).fetchIds());
const channelPromises: Array<Promise<Channel>> = [];
const userPromises: Array<Promise<ChannelMembership>> = [];
@@ -695,7 +739,7 @@ export async function fetchMissingChannelsFromPosts(serverUrl: string, posts: Po
const channelMemberships = await Promise.all(userPromises);
if (!fetchOnly && channels.length && channelMemberships.length) {
const isCRTEnabled = await getIsCRTEnabled(database);
const isCRTEnabled = await getIsCRTEnabled(operator.database);
const modelPromises = prepareMissingChannelsForAllTeams(operator, channels, channelMemberships, isCRTEnabled);
if (modelPromises.length) {
const channelModelsArray = await Promise.all(modelPromises);
@@ -718,16 +762,25 @@ export async function fetchMissingChannelsFromPosts(serverUrl: string, posts: Po
channelMemberships,
};
} catch (error) {
logDebug('error on fetchMissingChannelsFromPosts', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
}
export async function fetchPostById(serverUrl: string, postId: string, fetchOnly = false) {
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};
}
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const post = await client.getPost(postId);
if (!fetchOnly) {
const models: Model[] = [];
@@ -748,7 +801,7 @@ export async function fetchPostById(serverUrl: string, postId: string, fetchOnly
models.push(...users);
}
const isCRTEnabled = await getIsCRTEnabled(database);
const isCRTEnabled = await getIsCRTEnabled(operator.database);
if (isCRTEnabled) {
const threadModels = await prepareThreadsFromReceivedPosts(operator, [post], false);
if (threadModels?.length) {
@@ -761,17 +814,25 @@ export async function fetchPostById(serverUrl: string, postId: string, fetchOnly
return {post};
} catch (error) {
logDebug('error on fetchPostById', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
}
export const togglePinPost = async (serverUrl: string, postId: string) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const post = await getPostById(database, postId);
if (post) {
const isPinned = post.isPinned;
@@ -786,15 +847,25 @@ export const togglePinPost = async (serverUrl: string, postId: string) => {
}
return {post};
} catch (error) {
logDebug('error on togglePinPost', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const deletePost = async (serverUrl: string, postToDelete: PostModel | Post) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const client = NetworkManager.getClient(serverUrl);
if (postToDelete.type === Post.POST_TYPES.COMBINED_USER_ACTIVITY && postToDelete.props?.system_post_ids) {
const systemPostIds = getPostIdsForCombinedUserActivityPost(postToDelete.id);
const promises = systemPostIds.map((id) => client.deletePost(id));
@@ -806,17 +877,24 @@ export const deletePost = async (serverUrl: string, postToDelete: PostModel | Po
const post = await removePost(serverUrl, postToDelete);
return {post};
} catch (error) {
logDebug('error on deletePost', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const markPostAsUnread = async (serverUrl: string, postId: string) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const [userId, post] = await Promise.all([getCurrentUserId(database), getPostById(database, postId)]);
if (post && userId) {
await client.markPostAsUnread(userId, postId);
@@ -840,48 +918,67 @@ export const markPostAsUnread = async (serverUrl: string, postId: string) => {
const messageCount = totalMessages - messages;
await markChannelAsUnread(serverUrl, channelId, messageCount, mentionCount, post.createAt);
return {post};
return {
post,
};
}
}
return {post};
return {
post,
};
} catch (error) {
logDebug('error on markPostAsUnread', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const editPost = async (serverUrl: string, postId: string, postMessage: string) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const post = await getPostById(database, postId);
if (post) {
const {update_at, edit_at, message: updatedMessage, message_source} = await client.patchPost({message: postMessage, id: postId});
const {update_at, edit_at, message: updatedMessage} = await client.patchPost({message: postMessage, id: postId});
await database.write(async () => {
await post.update((p) => {
p.updateAt = update_at;
p.editAt = edit_at;
p.message = updatedMessage;
p.messageSource = message_source || '';
});
});
}
return {post};
return {
post,
};
} catch (error) {
logDebug('error on editPost', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export async function fetchSavedPosts(serverUrl: string, teamId?: string, channelId?: string, page?: number, perPage?: number) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const userId = await getCurrentUserId(database);
try {
const userId = await getCurrentUserId(operator.database);
const data = await client.getSavedPosts(userId, channelId, teamId, page, perPage);
const posts = data.posts || {};
const order = data.order || [];
@@ -909,7 +1006,7 @@ export async function fetchSavedPosts(serverUrl: string, teamId?: string, channe
}
if (channels?.length && channelMemberships?.length) {
const isCRTEnabled = await getIsCRTEnabled(database);
const isCRTEnabled = await getIsCRTEnabled(operator.database);
const channelPromises = prepareMissingChannelsForAllTeams(operator, channels, channelMemberships, isCRTEnabled);
if (channelPromises.length) {
promises.push(...channelPromises);
@@ -926,7 +1023,7 @@ export async function fetchSavedPosts(serverUrl: string, teamId?: string, channe
}),
);
const isCRTEnabled = await getIsCRTEnabled(database);
const isCRTEnabled = await getIsCRTEnabled(operator.database);
if (isCRTEnabled) {
promises.push(prepareThreadsFromReceivedPosts(operator, postsArray, false));
}
@@ -946,17 +1043,24 @@ export async function fetchSavedPosts(serverUrl: string, teamId?: string, channe
posts: postsArray,
};
} catch (error) {
logDebug('error on fetchSavedPosts', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
}
export async function fetchPinnedPosts(serverUrl: string, channelId: string) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const data = await client.getPinnedPosts(channelId);
const posts = data.posts || {};
const order = data.order || [];
@@ -970,6 +1074,7 @@ export async function fetchPinnedPosts(serverUrl: string, channelId: string) {
}
const promises: Array<Promise<Model[]>> = [];
const {database} = operator;
const isCRTEnabled = await getIsCRTEnabled(database);
const {authors} = await fetchPostAuthors(serverUrl, postsArray, true);
@@ -1019,44 +1124,8 @@ export async function fetchPinnedPosts(serverUrl: string, channelId: string) {
order,
posts: postsArray,
};
} catch (error) {
logDebug('error on fetchPinnedPosts', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function acknowledgePost(serverUrl: string, postId: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
EphemeralStore.setAcknowledgingPost(postId);
const userId = await getCurrentUserId(database);
const {acknowledged_at: acknowledgedAt} = await client.acknowledgePost(postId, userId);
return addPostAcknowledgement(serverUrl, postId, userId, acknowledgedAt, false);
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
} finally {
EphemeralStore.unsetAcknowledgingPost(postId);
}
}
export async function unacknowledgePost(serverUrl: string, postId: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
EphemeralStore.setUnacknowledgingPost(postId);
const userId = await getCurrentUserId(database);
await client.unacknowledgePost(postId, userId);
return removePostAcknowledgement(serverUrl, postId, userId, false);
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
} finally {
EphemeralStore.unsetUnacknowledgingPost(postId);
}
}

View File

@@ -1,19 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DeviceEventEmitter} from 'react-native';
import {handleReconnect} from '@actions/websocket';
import {Events, General, Preferences} from '@constants';
import {General, Preferences} from '@constants';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {getChannelById} from '@queries/servers/channel';
import {truncateCrtRelatedTables} from '@queries/servers/entry';
import {querySavedPostsPreferences} from '@queries/servers/preference';
import {getCurrentUserId} from '@queries/servers/system';
import EphemeralStore from '@store/ephemeral_store';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {getUserIdFromChannelName} from '@utils/user';
import {forceLogoutIfNecessary} from './session';
@@ -24,32 +17,42 @@ export type MyPreferencesRequest = {
};
export const fetchMyPreferences = async (serverUrl: string, fetchOnly = false): Promise<MyPreferencesRequest> => {
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const preferences = await client.getMyPreferences();
if (!fetchOnly) {
await operator.handlePreferences({
prepareRecordsOnly: false,
preferences,
sync: true,
});
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (operator) {
await operator.handlePreferences({
prepareRecordsOnly: false,
preferences,
sync: true,
});
}
}
return {preferences};
} catch (error) {
logDebug('error on fetchMyPreferences', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const saveFavoriteChannel = async (serverUrl: string, channelId: string, isFavorite: boolean) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const userId = await getCurrentUserId(database);
const userId = await getCurrentUserId(operator.database);
const favPref: PreferenceType = {
category: Preferences.CATEGORIES.FAVORITE_CHANNEL,
name: channelId,
@@ -63,10 +66,13 @@ export const saveFavoriteChannel = async (serverUrl: string, channelId: string,
};
export const savePostPreference = async (serverUrl: string, postId: string) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const userId = await getCurrentUserId(database);
try {
const userId = await getCurrentUserId(operator.database);
const pref: PreferenceType = {
user_id: userId,
category: Preferences.CATEGORIES.SAVED_POST,
@@ -80,11 +86,20 @@ export const savePostPreference = async (serverUrl: string, postId: string) => {
};
export const savePreference = async (serverUrl: string, preferences: PreferenceType[]) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const userId = await getCurrentUserId(database);
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const userId = await getCurrentUserId(operator.database);
client.savePreferences(userId, preferences);
await operator.handlePreferences({
preferences,
@@ -93,8 +108,7 @@ export const savePreference = async (serverUrl: string, preferences: PreferenceT
return {preferences};
} catch (error) {
logDebug('error on savePreference', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
@@ -122,15 +136,18 @@ export const deleteSavedPost = async (serverUrl: string, postId: string) => {
preference: pref,
};
} catch (error) {
logDebug('error on deleteSavedPost', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const setDirectChannelVisible = async (serverUrl: string, channelId: string, visible = true) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channel = await getChannelById(database, channelId);
if (channel?.type === General.DM_CHANNEL || channel?.type === General.GM_CHANNEL) {
const userId = await getCurrentUserId(database);
@@ -146,9 +163,9 @@ export const setDirectChannelVisible = async (serverUrl: string, channelId: stri
return savePreference(serverUrl, [pref]);
}
return {};
return {error: undefined};
} catch (error) {
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
@@ -168,11 +185,3 @@ export const savePreferredSkinTone = async (serverUrl: string, skinCode: string)
return {error};
}
};
export const handleCRTToggled = async (serverUrl: string) => {
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
await truncateCrtRelatedTables(serverUrl);
await handleReconnect(serverUrl);
EphemeralStore.setEnablingCRT(false);
DeviceEventEmitter.emit(Events.CRT_TOGGLED, serverUrl === currentServerUrl);
};

View File

@@ -8,22 +8,30 @@ import {getRecentPostsInChannel, getRecentPostsInThread} from '@queries/servers/
import {queryReaction} from '@queries/servers/reaction';
import {getCurrentChannelId, getCurrentUserId} from '@queries/servers/system';
import {getEmojiFirstAlias} from '@utils/emoji/helpers';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
import type {Model} from '@nozbe/watermelondb';
import type PostModel from '@typings/database/models/servers/post';
export async function addReaction(serverUrl: string, postId: string, emojiName: string) {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const currentUserId = await getCurrentUserId(database);
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const currentUserId = await getCurrentUserId(operator.database);
const emojiAlias = getEmojiFirstAlias(emojiName);
const reacted = await queryReaction(database, emojiAlias, postId, currentUserId).fetchCount() > 0;
const reacted = await queryReaction(operator.database, emojiAlias, postId, currentUserId).fetchCount() > 0;
if (!reacted) {
const reaction = await client.addReaction(currentUserId, postId, emojiAlias);
const models: Model[] = [];
@@ -56,17 +64,25 @@ export async function addReaction(serverUrl: string, postId: string, emojiName:
} as Reaction,
};
} catch (error) {
logDebug('error on addReaction', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
}
export const removeReaction = async (serverUrl: string, postId: string, emojiName: string) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const currentUserId = await getCurrentUserId(database);
const emojiAlias = getEmojiFirstAlias(emojiName);
await client.removeReaction(currentUserId, postId, emojiAlias);
@@ -82,21 +98,24 @@ export const removeReaction = async (serverUrl: string, postId: string, emojiNam
return {reaction};
} catch (error) {
logDebug('error on removeReaction', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const handleReactionToLatestPost = async (serverUrl: string, emojiName: string, add: boolean, rootId?: string) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
let posts: PostModel[];
if (rootId) {
posts = await getRecentPostsInThread(database, rootId);
posts = await getRecentPostsInThread(operator.database, rootId);
} else {
const channelId = await getCurrentChannelId(database);
posts = await getRecentPostsInChannel(database, channelId);
const channelId = await getCurrentChannelId(operator.database);
posts = await getRecentPostsInChannel(operator.database, channelId);
}
if (add) {

View File

@@ -15,12 +15,12 @@ import {prepareMyTeams} from '@queries/servers/team';
import {getCurrentUser} from '@queries/servers/user';
import {isDMorGM, selectDefaultChannelForTeam} from '@utils/channel';
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, type MyChannelsRequest} from './channel';
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, MyChannelsRequest} from './channel';
import {fetchPostsForChannel} from './post';
import {fetchMyPreferences, type MyPreferencesRequest} from './preference';
import {fetchMyPreferences, MyPreferencesRequest} from './preference';
import {fetchRolesIfNeeded} from './role';
import {type ConfigAndLicenseRequest, fetchConfigAndLicense} from './systems';
import {fetchMyTeams, type MyTeamsRequest} from './team';
import {ConfigAndLicenseRequest, fetchConfigAndLicense} from './systems';
import {fetchMyTeams, MyTeamsRequest} from './team';
import type {Model} from '@nozbe/watermelondb';
import type TeamModel from '@typings/database/models/servers/team';
@@ -30,6 +30,7 @@ export async function retryInitialTeamAndChannel(serverUrl: string) {
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
try {
@@ -138,6 +139,7 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) {
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
try {

View File

@@ -4,8 +4,6 @@
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {queryRoles} from '@queries/servers/role';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {forceLogoutIfNecessary} from './session';
@@ -19,29 +17,43 @@ export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string
return {roles: []};
}
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
let newRoles;
if (force) {
newRoles = updatedRoles;
} else {
const existingRoles = await queryRoles(database).fetch();
let database;
let operator;
try {
const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = result.database;
operator = result.operator;
} catch (e) {
return {error: `${serverUrl} database not found`};
}
const roleNames = new Set(existingRoles.map((role) => {
return role.name;
}));
let newRoles;
if (force) {
newRoles = updatedRoles;
} else {
const existingRoles = await queryRoles(database).fetch();
newRoles = updatedRoles.filter((newRole) => {
return !roleNames.has(newRole);
});
}
const roleNames = new Set(existingRoles.map((role) => {
return role.name;
}));
if (!newRoles.length) {
return {roles: []};
}
newRoles = updatedRoles.filter((newRole) => {
return !roleNames.has(newRole);
});
}
if (!newRoles.length) {
return {roles: []};
}
try {
const roles = await client.getRolesByNames(newRoles);
if (!fetchOnly) {
await operator.handleRole({
@@ -52,8 +64,7 @@ export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string
return {roles};
} catch (error) {
logDebug('error on fetchRolesIfNeeded', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};

View File

@@ -5,13 +5,9 @@ import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {prepareMissingChannelsForAllTeams} from '@queries/servers/channel';
import {getConfigValue} from '@queries/servers/system';
import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers/thread';
import {getCurrentUser} from '@queries/servers/user';
import {getFullErrorMessage} from '@utils/errors';
import {getUtcOffsetForTimeZone} from '@utils/helpers';
import {logDebug} from '@utils/log';
import {getUserTimezone} from '@utils/user';
import {logError} from '@utils/log';
import {fetchPostAuthors, fetchMissingChannelsFromPosts} from './post';
import {forceLogoutIfNecessary} from './session';
@@ -52,18 +48,11 @@ export async function fetchRecentMentions(serverUrl: string): Promise<PostSearch
export const searchPosts = async (serverUrl: string, teamId: string, params: PostSearchParams): Promise<PostSearchRequest> => {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
const viewArchivedChannels = await getConfigValue(database, 'ExperimentalViewArchivedChannels');
const user = await getCurrentUser(database);
const timezoneOffset = getUtcOffsetForTimeZone(getUserTimezone(user)) * 60;
let postsArray: Post[] = [];
const data = await client.searchPostsWithParams(teamId, {
...params,
include_deleted_channels: Boolean(viewArchivedChannels),
time_zone_offset: timezoneOffset,
});
const data = await client.searchPosts(teamId, params.terms, params.is_or_search);
const posts = data.posts || {};
const order = data.order || [];
@@ -71,7 +60,7 @@ export const searchPosts = async (serverUrl: string, teamId: string, params: Pos
const promises: Array<Promise<Model[]>> = [];
postsArray = order.map((id) => posts[id]);
if (postsArray.length) {
const isCRTEnabled = await getIsCRTEnabled(database);
const isCRTEnabled = await getIsCRTEnabled(operator.database);
if (isCRTEnabled) {
promises.push(prepareThreadsFromReceivedPosts(operator, postsArray, false));
}
@@ -118,11 +107,10 @@ export const searchPosts = async (serverUrl: string, teamId: string, params: Pos
return {
order,
posts: postsArray,
matches: data.matches,
};
} catch (error) {
logDebug('error on searchPosts', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
logError('Failed: searchPosts', error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
@@ -141,8 +129,7 @@ export const searchFiles = async (serverUrl: string, teamId: string, params: Fil
const channels = [...new Set(allChannelIds)];
return {files, channels};
} catch (error) {
logDebug('error on searchFiles', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};

View File

@@ -7,44 +7,68 @@ import {DeviceEventEmitter, Platform} from 'react-native';
import {Database, Events} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {getServerCredentials} from '@init/credentials';
import PushNotifications from '@init/push_notifications';
import NetworkManager from '@managers/network_manager';
import WebsocketManager from '@managers/websocket_manager';
import {getDeviceToken} from '@queries/app/global';
import {getServerDisplayName} from '@queries/app/servers';
import {getCurrentUserId, getExpiredSession} from '@queries/servers/system';
import {getCurrentUserId, getExpiredSession, getConfig, getLicense} from '@queries/servers/system';
import {getCurrentUser} from '@queries/servers/user';
import EphemeralStore from '@store/ephemeral_store';
import {getFullErrorMessage, isErrorWithStatusCode, isErrorWithUrl} from '@utils/errors';
import {logWarning, logError, logDebug} from '@utils/log';
import {logWarning, logError} from '@utils/log';
import {scheduleExpiredNotification} from '@utils/notification';
import {getCSRFFromCookie} from '@utils/security';
import {loginEntry} from './entry';
import type ClientError from '@client/rest/error';
import type {LoginArgs} from '@typings/database/database';
const HTTP_UNAUTHORIZED = 401;
export const addPushProxyVerificationStateFromLogin = async (serverUrl: string) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const systems: IdValue[] = [];
// Set push proxy verification
const ppVerification = EphemeralStore.getPushProxyVerificationState(serverUrl);
if (ppVerification) {
systems.push({id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: ppVerification});
}
if (systems.length) {
await operator.handleSystem({systems, prepareRecordsOnly: false});
}
} catch (error) {
logDebug('error setting the push proxy verification state on login', error);
export const completeLogin = async (serverUrl: string) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
const license = await getLicense(database);
const config = await getConfig(database);
if (!Object.keys(config)?.length || !license || !Object.keys(license)?.length) {
return null;
}
await DatabaseManager.setActiveServerDatabase(serverUrl);
const systems: IdValue[] = [];
// Set push proxy verification
const ppVerification = EphemeralStore.getPushProxyVerificationState(serverUrl);
if (ppVerification) {
systems.push({id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: ppVerification});
}
// Start websocket
const credentials = await getServerCredentials(serverUrl);
if (credentials?.token) {
WebsocketManager.createClient(serverUrl, credentials.token);
systems.push({
id: SYSTEM_IDENTIFIERS.WEBSOCKET,
value: 0,
});
}
if (systems.length) {
operator.handleSystem({systems, prepareRecordsOnly: false});
}
return null;
};
export const forceLogoutIfNecessary = async (serverUrl: string, err: unknown) => {
export const forceLogoutIfNecessary = async (serverUrl: string, err: ClientErrorProps) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
@@ -52,7 +76,7 @@ export const forceLogoutIfNecessary = async (serverUrl: string, err: unknown) =>
const currentUserId = await getCurrentUserId(database);
if (isErrorWithStatusCode(err) && err.status_code === HTTP_UNAUTHORIZED && isErrorWithUrl(err) && err.url?.indexOf('/login') === -1 && currentUserId) {
if ('status_code' in err && err.status_code === HTTP_UNAUTHORIZED && err?.url?.indexOf('/login') === -1 && currentUserId) {
await logout(serverUrl);
}
@@ -69,9 +93,9 @@ export const fetchSessions = async (serverUrl: string, currentUserId: string) =>
try {
return await client.getSessions(currentUserId);
} catch (error) {
logDebug('error on fetchSessions', getFullErrorMessage(error));
await forceLogoutIfNecessary(serverUrl, error);
} catch (e) {
logError('fetchSessions', e);
await forceLogoutIfNecessary(serverUrl, e as ClientError);
}
return undefined;
@@ -86,8 +110,14 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
return {error: 'App database not found.', failed: true};
}
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error: error as Error, failed: true};
}
try {
const client = NetworkManager.getClient(serverUrl);
deviceToken = await getDeviceToken();
user = await client.login(
loginId,
@@ -117,17 +147,15 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
const csrfToken = await getCSRFFromCookie(serverUrl);
client.setCSRFToken(csrfToken);
} catch (error) {
logDebug('error on login', getFullErrorMessage(error));
return {error, failed: true};
return {error: error as Error, failed: true};
}
try {
await addPushProxyVerificationStateFromLogin(serverUrl);
const {error} = await loginEntry({serverUrl});
await DatabaseManager.setActiveServerDatabase(serverUrl);
return {error, failed: false};
const {error, hasTeams, time} = await loginEntry({serverUrl, user});
completeLogin(serverUrl);
return {error: error as ClientError, failed: false, hasTeams, time};
} catch (error) {
return {error, failed: false};
return {error: error as ClientError, failed: false, time: 0};
}
};
@@ -138,7 +166,7 @@ export const logout = async (serverUrl: string, skipServerLogout = false, remove
await client.logout();
} catch (error) {
// We want to log the user even if logging out from the server failed
logWarning('An error occurred logging out from the server', serverUrl, getFullErrorMessage(error));
logWarning('An error occurred logging out from the server', serverUrl, error);
}
}
@@ -198,34 +226,51 @@ export const scheduleSessionNotification = async (serverUrl: string) => {
}
} catch (e) {
logError('scheduleExpiredNotification', e);
await forceLogoutIfNecessary(serverUrl, e);
await forceLogoutIfNecessary(serverUrl, e as ClientError);
}
};
export const sendPasswordResetEmail = async (serverUrl: string, email: string) => {
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const response = await client.sendPasswordResetEmail(email);
return {status: response.status};
client = NetworkManager.getClient(serverUrl);
} catch (error) {
logDebug('error on sendPasswordResetEmail', getFullErrorMessage(error));
return {error};
}
let response;
try {
response = await client.sendPasswordResetEmail(email);
} catch (error) {
return {error};
}
return {
data: response.data,
error: undefined,
};
};
export const ssoLogin = async (serverUrl: string, serverDisplayName: string, serverIdentifier: string, bearerToken: string, csrfToken: string): Promise<LoginActionResponse> => {
let deviceToken;
let user;
const database = DatabaseManager.appDatabase?.database;
if (!database) {
return {error: 'App database not found', failed: true};
}
let client;
try {
const client = NetworkManager.getClient(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error: error as Error, failed: true};
}
client.setBearerToken(bearerToken);
client.setCSRFToken(csrfToken);
client.setBearerToken(bearerToken);
client.setCSRFToken(csrfToken);
// Setting up active database for this SSO login flow
// Setting up active database for this SSO login flow
try {
const server = await DatabaseManager.createServerDatabase({
config: {
dbName: serverUrl,
@@ -234,7 +279,8 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
displayName: serverDisplayName,
},
});
const user = await client.getMe();
deviceToken = await getDeviceToken();
user = await client.getMe();
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
await server?.operator.handleSystem({
systems: [{
@@ -243,18 +289,16 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
}],
prepareRecordsOnly: false,
});
} catch (error) {
logDebug('error on ssoLogin', getFullErrorMessage(error));
return {error, failed: true};
} catch (e) {
return {error: e as ClientError, failed: true};
}
try {
await addPushProxyVerificationStateFromLogin(serverUrl);
const {error} = await loginEntry({serverUrl});
await DatabaseManager.setActiveServerDatabase(serverUrl);
return {error, failed: false};
const {error, hasTeams, time} = await loginEntry({serverUrl, user, deviceToken});
completeLogin(serverUrl);
return {error: error as ClientError, failed: false, hasTeams, time};
} catch (error) {
return {error, failed: false};
return {error: error as ClientError, failed: false, time: 0};
}
};

View File

@@ -6,8 +6,8 @@ import {forceLogoutIfNecessary} from '@actions/remote/session';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {getCurrentUserId} from '@queries/servers/system';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import type ClientError from '@client/rest/error';
export type ConfigAndLicenseRequest = {
config?: ClientConfig;
@@ -23,36 +23,51 @@ export type DataRetentionPoliciesRequest = {
}
export const fetchDataRetentionPolicy = async (serverUrl: string, fetchOnly = false): Promise<DataRetentionPoliciesRequest> => {
const {data: globalPolicy, error: globalPolicyError} = await fetchGlobalDataRetentionPolicy(serverUrl);
const {data: teamPolicies, error: teamPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl);
const {data: channelPolicies, error: channelPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl, true);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const error = globalPolicyError || teamPoliciesError || channelPoliciesError;
if (error) {
try {
const {data: globalPolicy, error: globalPolicyError} = await fetchGlobalDataRetentionPolicy(serverUrl);
const {data: teamPolicies, error: teamPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl);
const {data: channelPolicies, error: channelPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl, true);
const hasError = globalPolicyError || teamPoliciesError || channelPoliciesError;
if (hasError) {
return hasError;
}
const data = {
globalPolicy,
teamPolicies: teamPolicies as TeamDataRetentionPolicy[],
channelPolicies: channelPolicies as ChannelDataRetentionPolicy[],
};
if (!fetchOnly) {
await storeDataRetentionPolicies(serverUrl, data);
}
return data;
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
const data = {
globalPolicy,
teamPolicies: teamPolicies as TeamDataRetentionPolicy[],
channelPolicies: channelPolicies as ChannelDataRetentionPolicy[],
};
if (!fetchOnly) {
await storeDataRetentionPolicies(serverUrl, data);
}
return data;
};
export const fetchGlobalDataRetentionPolicy = async (serverUrl: string): Promise<{data?: GlobalDataRetentionPolicy; error?: unknown}> => {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const client = NetworkManager.getClient(serverUrl);
const data = await client.getGlobalDataRetentionPolicy();
return {data};
} catch (error) {
logDebug('error on fetchGlobalDataRetentionPolicy', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
};
@@ -63,31 +78,43 @@ export const fetchAllGranularDataRetentionPolicies = async (
page = 0,
policies: Array<TeamDataRetentionPolicy | ChannelDataRetentionPolicy> = [],
): Promise<{data?: Array<TeamDataRetentionPolicy | ChannelDataRetentionPolicy>; error?: unknown}> => {
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUserId = await getCurrentUserId(database);
let data;
if (isChannel) {
data = await client.getChannelDataRetentionPolicies(currentUserId, page);
} else {
data = await client.getTeamDataRetentionPolicies(currentUserId, page);
}
policies.push(...data.policies);
if (policies.length < data.total_count) {
await fetchAllGranularDataRetentionPolicies(serverUrl, isChannel, page + 1, policies);
}
return {data: policies};
client = NetworkManager.getClient(serverUrl);
} catch (error) {
logDebug('error on fetchAllGranularDataRetentionPolicies', getFullErrorMessage(error));
return {error};
}
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
const currentUserId = await getCurrentUserId(database);
let data;
if (isChannel) {
data = await client.getChannelDataRetentionPolicies(currentUserId, page);
} else {
data = await client.getTeamDataRetentionPolicies(currentUserId, page);
}
policies.push(...data.policies);
if (policies.length < data.total_count) {
await fetchAllGranularDataRetentionPolicies(serverUrl, isChannel, page + 1, policies);
}
return {data: policies};
};
export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false): Promise<ConfigAndLicenseRequest> => {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const client = NetworkManager.getClient(serverUrl);
const [config, license]: [ClientConfig, ClientLicense] = await Promise.all([
client.getClientConfigOld(),
client.getClientLicenseOld(),
@@ -99,8 +126,7 @@ export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false
return {config, license};
} catch (error) {
logDebug('error on fetchConfigAndLicense', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
};

View File

@@ -13,10 +13,9 @@ import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categorie
import {prepareMyChannelsForTeam, getDefaultChannelForTeam} from '@queries/servers/channel';
import {prepareCommonSystemValues, getCurrentTeamId, getCurrentUserId} from '@queries/servers/system';
import {addTeamToTeamHistory, prepareDeleteTeam, prepareMyTeams, getNthLastChannelFromTeam, queryTeamsById, getLastTeam, getTeamById, removeTeamFromTeamHistory, queryMyTeams} from '@queries/servers/team';
import {dismissAllModalsAndPopToRoot} from '@screens/navigation';
import {dismissAllModals, popToRoot} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
import {setTeamLoading} from '@store/team_load_store';
import {getFullErrorMessage} from '@utils/errors';
import {isTablet} from '@utils/helpers';
import {logDebug} from '@utils/log';
@@ -27,6 +26,7 @@ import {fetchRolesIfNeeded} from './role';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
import type ClientError from '@client/rest/error';
import type {Model} from '@nozbe/watermelondb';
export type MyTeamsRequest = {
@@ -52,11 +52,15 @@ export async function addCurrentUserToTeam(serverUrl: string, teamId: string, fe
}
export async function addUserToTeam(serverUrl: string, teamId: string, userId: string, fetchOnly = false) {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
let loadEventSent = false;
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
EphemeralStore.startAddingToTeam(teamId);
const team = await client.getTeam(teamId);
const member = await client.addToTeam(teamId, userId);
@@ -67,40 +71,45 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
const {channels, memberships: channelMembers, categories} = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
const myTeams: MyTeam[] = [{
id: member.team_id,
roles: member.roles,
}];
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (operator) {
const myTeams: MyTeam[] = [{
id: member.team_id,
roles: member.roles,
}];
const models: Model[] = (await Promise.all([
operator.handleTeam({teams: [team], prepareRecordsOnly: true}),
operator.handleMyTeam({myTeams, prepareRecordsOnly: true}),
operator.handleTeamMemberships({teamMemberships: [member], prepareRecordsOnly: true}),
...await prepareMyChannelsForTeam(operator, teamId, channels || [], channelMembers || []),
prepareCategoriesAndCategoriesChannels(operator, categories || [], true),
])).flat();
const models: Model[] = (await Promise.all([
operator.handleTeam({teams: [team], prepareRecordsOnly: true}),
operator.handleMyTeam({myTeams, prepareRecordsOnly: true}),
operator.handleTeamMemberships({teamMemberships: [member], prepareRecordsOnly: true}),
...await prepareMyChannelsForTeam(operator, teamId, channels || [], channelMembers || []),
prepareCategoriesAndCategoriesChannels(operator, categories || [], true),
])).flat();
await operator.batchRecords(models, 'addUserToTeam');
setTeamLoading(serverUrl, false);
loadEventSent = false;
await operator.batchRecords(models, 'addUserToTeam');
setTeamLoading(serverUrl, false);
loadEventSent = false;
if (await isTablet()) {
const channel = await getDefaultChannelForTeam(database, teamId);
if (channel) {
fetchPostsForChannel(serverUrl, channel.id);
if (await isTablet()) {
const channel = await getDefaultChannelForTeam(operator.database, teamId);
if (channel) {
fetchPostsForChannel(serverUrl, channel.id);
}
}
} else {
setTeamLoading(serverUrl, false);
loadEventSent = false;
}
}
EphemeralStore.finishAddingToTeam(teamId);
updateCanJoinTeams(serverUrl);
return {member};
} catch (error) {
logDebug('error on addUserToTeam', getFullErrorMessage(error));
if (loadEventSent) {
setTeamLoading(serverUrl, false);
}
EphemeralStore.finishAddingToTeam(teamId);
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
}
@@ -132,12 +141,11 @@ export async function addUsersToTeam(serverUrl: string, teamId: string, userIds:
EphemeralStore.finishAddingToTeam(teamId);
return {members};
} catch (error) {
logDebug('error on addUsersToTeam', getFullErrorMessage(error));
if (EphemeralStore.isAddingToTeam(teamId)) {
EphemeralStore.finishAddingToTeam(teamId);
}
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
}
@@ -149,23 +157,27 @@ export async function sendEmailInvitesToTeam(serverUrl: string, teamId: string,
return {members};
} catch (error) {
logDebug('error on sendEmailInvitesToTeam', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
}
export async function fetchMyTeams(serverUrl: string, fetchOnly = false): Promise<MyTeamsRequest> {
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const [teams, memberships]: [Team[], TeamMembership[]] = await Promise.all([
client.getMyTeams(),
client.getMyTeamMembers(),
]);
if (!fetchOnly) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
const modelPromises: Array<Promise<Model[]>> = [];
if (operator) {
const removeTeamIds = new Set(memberships.filter((m) => m.delete_at > 0).map((m) => m.team_id));
@@ -177,7 +189,7 @@ export async function fetchMyTeams(serverUrl: string, fetchOnly = false): Promis
if (removeTeamIds.size) {
// Immediately delete myTeams so that the UI renders only teams the user is a member of.
const removeTeams = await queryTeamsById(database, Array.from(removeTeamIds)).fetch();
const removeTeams = await queryTeamsById(operator.database, Array.from(removeTeamIds)).fetch();
removeTeams.forEach((team) => {
modelPromises.push(prepareDeleteTeam(team));
});
@@ -195,36 +207,41 @@ export async function fetchMyTeams(serverUrl: string, fetchOnly = false): Promis
return {teams, memberships};
} catch (error) {
logDebug('error on fetchMyTeams', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
}
export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly = false): Promise<MyTeamsRequest> {
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const [team, membership] = await Promise.all([
client.getTeam(teamId),
client.getTeamMember(teamId, 'me'),
]);
if (!fetchOnly) {
const modelPromises = prepareMyTeams(operator, [team], [membership]);
if (modelPromises.length) {
const models = await Promise.all(modelPromises);
const flattenedModels = models.flat();
if (flattenedModels?.length > 0) {
await operator.batchRecords(flattenedModels, 'fetchMyTeam');
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (operator) {
const modelPromises = prepareMyTeams(operator, [team], [membership]);
if (modelPromises.length) {
const models = await Promise.all(modelPromises);
const flattenedModels = models.flat();
if (flattenedModels?.length > 0) {
await operator.batchRecords(flattenedModels, 'fetchMyTeam');
}
}
}
}
return {teams: [team], memberships: [membership]};
} catch (error) {
logDebug('error on fetchMyTeam', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
}
@@ -235,8 +252,7 @@ export const fetchAllTeams = async (serverUrl: string, page = 0, perPage = PER_P
const teams = await client.getTeams(page, perPage);
return {teams};
} catch (error) {
logDebug('error on fetchAllTeams', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
};
@@ -305,9 +321,8 @@ export const updateCanJoinTeams = async (serverUrl: string) => {
EphemeralStore.setCanJoinOtherTeams(serverUrl, canJoin);
return {};
} catch (error) {
logDebug('error on updateCanJoinTeams', getFullErrorMessage(error));
EphemeralStore.setCanJoinOtherTeams(serverUrl, false);
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
};
@@ -329,25 +344,31 @@ export const fetchTeamsChannelsAndUnreadPosts = async (serverUrl: string, since:
}
}
return {};
return {error: undefined};
};
export async function fetchTeamByName(serverUrl: string, teamName: string, fetchOnly = false) {
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const team = await client.getTeamByName(teamName);
if (!fetchOnly) {
const models = await operator.handleTeam({teams: [team], prepareRecordsOnly: true});
await operator.batchRecords(models, 'fetchTeamByName');
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (operator) {
const models = await operator.handleTeam({teams: [team], prepareRecordsOnly: true});
await operator.batchRecords(models, 'fetchTeamByName');
}
}
return {team};
} catch (error) {
logDebug('error on fetchTeamByName', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
}
@@ -363,8 +384,14 @@ export const removeCurrentUserFromTeam = async (serverUrl: string, teamId: strin
};
export const removeUserFromTeam = async (serverUrl: string, teamId: string, userId: string, fetchOnly = false) => {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const client = NetworkManager.getClient(serverUrl);
await client.removeFromTeam(teamId, userId);
if (!fetchOnly) {
@@ -372,10 +399,9 @@ export const removeUserFromTeam = async (serverUrl: string, teamId: string, user
updateCanJoinTeams(serverUrl);
}
return {};
return {error: undefined};
} catch (error) {
logDebug('error on removeUserFromTeam', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
};
@@ -385,8 +411,8 @@ export async function handleTeamChange(serverUrl: string, teamId: string) {
if (!operator) {
return;
}
const {database} = operator;
const {database} = operator;
const currentTeamId = await getCurrentTeamId(database);
if (currentTeamId === teamId) {
@@ -435,7 +461,8 @@ export async function handleKickFromTeam(serverUrl: string, teamId: string) {
if (currentServer === serverUrl) {
const team = await getTeamById(database, teamId);
DeviceEventEmitter.emit(Events.LEAVE_TEAM, team?.displayName);
await dismissAllModalsAndPopToRoot();
await dismissAllModals();
await popToRoot();
}
await removeTeamFromTeamHistory(operator, teamId);
@@ -470,8 +497,7 @@ export async function getTeamMembersByIds(serverUrl: string, teamId: string, use
return {members};
} catch (error) {
logDebug('error on getTeamMembersByIds', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
}

View File

@@ -4,19 +4,24 @@
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {getCurrentUser} from '@queries/servers/user';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {forceLogoutIfNecessary} from './session';
import type ClientError from '@client/rest/error';
export async function fetchTermsOfService(serverUrl: string): Promise<{terms?: TermsOfService; error?: any}> {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const client = NetworkManager.getClient(serverUrl);
const terms = await client.getTermsOfService();
return {terms};
} catch (error) {
logDebug('error on fetchTermsOfService', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
}
@@ -42,8 +47,7 @@ export async function updateTermsOfServiceStatus(serverUrl: string, id: string,
}
return {resp};
} catch (error) {
logDebug('error on updateTermsOfServiceStatus', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
}

View File

@@ -12,9 +12,6 @@ import {getPostById} from '@queries/servers/post';
import {getConfigValue, getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system';
import {getIsCRTEnabled, getThreadById, getTeamThreadsSyncData} from '@queries/servers/thread';
import {getCurrentUser} from '@queries/servers/user';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {showThreadFollowingSnackbar} from '@utils/snack_bar';
import {getThreadsListEdges} from '@utils/thread';
import {forceLogoutIfNecessary} from './session';
@@ -78,23 +75,34 @@ export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string,
};
export const fetchThread = async (serverUrl: string, teamId: string, threadId: string, extended?: boolean) => {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const client = NetworkManager.getClient(serverUrl);
const thread = await client.getThread('me', teamId, threadId, extended);
await processReceivedThreads(serverUrl, [thread], teamId);
return {data: thread};
} catch (error) {
logDebug('error on fetchThread', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const updateTeamThreadsAsRead = async (serverUrl: string, teamId: string) => {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const client = NetworkManager.getClient(serverUrl);
const data = await client.updateTeamThreadsAsRead('me', teamId);
// Update locally
@@ -102,17 +110,26 @@ export const updateTeamThreadsAsRead = async (serverUrl: string, teamId: string)
return {data};
} catch (error) {
logDebug('error on updateTeamThreadsAsRead', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const markThreadAsRead = async (serverUrl: string, teamId: string | undefined, threadId: string, updateLastViewed = true) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
export const markThreadAsRead = async (serverUrl: string, teamId: string | undefined, threadId: string) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const timestamp = Date.now();
// DM/GM doesn't have a teamId, so we pass the current team id
@@ -124,7 +141,7 @@ export const markThreadAsRead = async (serverUrl: string, teamId: string | undef
// Update locally
await updateThread(serverUrl, threadId, {
last_viewed_at: updateLastViewed ? timestamp : undefined,
last_viewed_at: timestamp,
unread_replies: 0,
unread_mentions: 0,
});
@@ -141,17 +158,26 @@ export const markThreadAsRead = async (serverUrl: string, teamId: string | undef
return {data};
} catch (error) {
logDebug('error on markThreadAsRead', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const markThreadAsUnread = async (serverUrl: string, teamId: string, threadId: string, postId: string) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
// DM/GM doesn't have a teamId, so we pass the current team id
let threadTeamId = teamId;
if (!threadTeamId) {
@@ -171,37 +197,40 @@ export const markThreadAsUnread = async (serverUrl: string, teamId: string, thre
return {data};
} catch (error) {
logDebug('error on markThreadAsUnread', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const updateThreadFollowing = async (serverUrl: string, teamId: string, threadId: string, state: boolean, showSnackBar: boolean) => {
export const updateThreadFollowing = async (serverUrl: string, teamId: string, threadId: string, state: boolean) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
// DM/GM doesn't have a teamId, so we pass the current team id
let threadTeamId = teamId;
if (!threadTeamId) {
threadTeamId = await getCurrentTeamId(database);
}
// DM/GM doesn't have a teamId, so we pass the current team id
let threadTeamId = teamId;
if (!threadTeamId) {
threadTeamId = await getCurrentTeamId(database);
}
try {
const data = await client.updateThreadFollow('me', threadTeamId, threadId, state);
// Update locally
await updateThread(serverUrl, threadId, {is_following: state});
if (showSnackBar) {
const onUndo = () => updateThreadFollowing(serverUrl, teamId, threadId, !state, false);
showThreadFollowingSnackbar(state, onUndo);
}
return {data};
} catch (error) {
logDebug('error on updateThreadFollowing', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
@@ -214,10 +243,10 @@ export const fetchThreads = async (
pages?: number,
) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
let client: Client;
try {
@@ -228,12 +257,12 @@ export const fetchThreads = async (
const fetchDirection = direction ?? Direction.Up;
const currentUser = await getCurrentUser(database);
const currentUser = await getCurrentUser(operator.database);
if (!currentUser) {
return {error: 'currentUser not found'};
}
const version = await getConfigValue(database, 'Version');
const version = await getConfigValue(operator.database, 'Version');
const threadsData: Thread[] = [];
let currentPage = 0;
@@ -267,7 +296,6 @@ export const fetchThreads = async (
try {
await fetchThreadsFunc(options);
} catch (error) {
logDebug('error on fetchThreads', getFullErrorMessage(error));
if (__DEV__) {
throw error;
}
@@ -278,9 +306,13 @@ export const fetchThreads = async (
};
export const syncTeamThreads = async (serverUrl: string, teamId: string, prepareRecordsOnly = false) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const syncData = await getTeamThreadsSyncData(database, teamId);
const syncData = await getTeamThreadsSyncData(operator.database, teamId);
const syncDataUpdate = {
id: teamId,
} as TeamThreadsSync;
@@ -380,9 +412,12 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
};
export const loadEarlierThreads = async (serverUrl: string, teamId: string, lastThreadId: string, prepareRecordsOnly = false) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
/*
* - We will fetch one page of old threads
* - Update the sync data with the earliest thread last_reply_at timestamp
@@ -435,7 +470,7 @@ export const loadEarlierThreads = async (serverUrl: string, teamId: string, last
}
}
return {models, threads};
return {error: false, models, threads};
} catch (error) {
return {error};
}

View File

@@ -14,16 +14,17 @@ import {debounce} from '@helpers/api/general';
import NetworkManager from '@managers/network_manager';
import {getMembersCountByChannelsId, queryChannelsByTypes} from '@queries/servers/channel';
import {queryGroupsByNames} from '@queries/servers/group';
import {getConfig, getCurrentUserId, setCurrentUserId} from '@queries/servers/system';
import {getConfig, getCurrentUserId} from '@queries/servers/system';
import {getCurrentUser, prepareUsers, queryAllUsers, queryUsersById, queryUsersByIdsOrUsernames, queryUsersByUsername} from '@queries/servers/user';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {logError} from '@utils/log';
import {getDeviceTimezone, isTimezoneEnabled} from '@utils/timezone';
import {getUserTimezoneProps, removeUserFromList} from '@utils/user';
import {fetchGroupsByNames} from './groups';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
import type ClientError from '@client/rest/error';
import type {Model} from '@nozbe/watermelondb';
import type UserModel from '@typings/database/models/servers/user';
@@ -44,68 +45,50 @@ export type ProfilesInChannelRequest = {
}
export const fetchMe = async (serverUrl: string, fetchOnly = false): Promise<MyUserRequest> => {
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const resultSettled = await Promise.allSettled([client.getMe(), client.getStatus('me')]);
let user: UserProfile|undefined;
let userStatus: UserStatus|undefined;
for (const result of resultSettled) {
if (result.status === 'fulfilled') {
const {value} = result;
if ('email' in value) {
user = value;
} else {
userStatus = value;
}
}
}
try {
const [user, userStatus] = await Promise.all<[Promise<UserProfile>, Promise<UserStatus>]>([
client.getMe(),
client.getStatus('me'),
]);
if (!user) {
throw new Error('User not found');
}
user.status = userStatus?.status;
user.status = userStatus.status;
if (!fetchOnly) {
await operator.handleUsers({users: [user], prepareRecordsOnly: false});
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (operator) {
await operator.handleUsers({users: [user], prepareRecordsOnly: false});
}
}
return {user};
} catch (error) {
logDebug('error on fetchMe', getFullErrorMessage(error));
await forceLogoutIfNecessary(serverUrl, error);
await forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const refetchCurrentUser = async (serverUrl: string, currentUserId: string | undefined) => {
logDebug('re-fetching self');
const {user} = await fetchMe(serverUrl);
if (!user || currentUserId) {
return;
}
logDebug('missing currentUserId');
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
logDebug('missing operator');
return;
}
setCurrentUserId(operator, user.id);
};
export async function fetchProfilesInChannel(serverUrl: string, channelId: string, excludeUserId?: string, options?: GetUsersOptions, fetchOnly = false): Promise<ProfilesInChannelRequest> {
let client: Client;
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {channelId, error};
}
try {
const users = await client.getProfilesInChannel(channelId, options);
const uniqueUsers = Array.from(new Set(users));
const filteredUsers = uniqueUsers.filter((u) => u.id !== excludeUserId);
if (!fetchOnly) {
if (filteredUsers.length) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (operator && filteredUsers.length) {
const modelPromises: Array<Promise<Model[]>> = [];
const membership = filteredUsers.map((u) => ({
channel_id: channelId,
@@ -125,17 +108,28 @@ export async function fetchProfilesInChannel(serverUrl: string, channelId: strin
return {channelId, users: filteredUsers};
} catch (error) {
logDebug('error on fetchProfilesInChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
logError('fetchProfilesInChannel', error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {channelId, error};
}
}
export async function fetchProfilesInGroupChannels(serverUrl: string, groupChannelIds: string[], fetchOnly = false): Promise<ProfilesPerChannelRequest> {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
// let's filter those channels that we already have the users
const membersCount = await getMembersCountByChannelsId(database, groupChannelIds);
const channelsToFetch = groupChannelIds.filter((c) => membersCount[c] <= 1);
@@ -189,14 +183,16 @@ export async function fetchProfilesInGroupChannels(serverUrl: string, groupChann
return {data};
} catch (error) {
logDebug('error on fetchProfilesInGroupChannels', getFullErrorMessage(error));
return {error};
}
}
export async function fetchProfilesPerChannels(serverUrl: string, channelIds: string[], excludeUserId?: string, fetchOnly = false): Promise<ProfilesPerChannelRequest> {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
// Batch fetching profiles per channel by chunks of 250
const channels = chunk(channelIds, 250);
@@ -239,31 +235,39 @@ export async function fetchProfilesPerChannels(serverUrl: string, channelIds: st
return {data};
} catch (error) {
logDebug('error on fetchProfilesPerChannels', getFullErrorMessage(error));
return {error};
}
}
export const updateMe = async (serverUrl: string, user: Partial<UserProfile>) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const data = await client.patchMe(user);
if (data) {
operator.handleUsers({prepareRecordsOnly: false, users: [data]});
const updatedRoles: string[] = data.roles.split(' ');
await fetchRolesIfNeeded(serverUrl, updatedRoles);
}
return {data};
client = NetworkManager.getClient(serverUrl);
} catch (error) {
logDebug('error on updateMe', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
let data: UserProfile;
try {
data = await client.patchMe(user);
} catch (e) {
forceLogoutIfNecessary(serverUrl, e as ClientError);
return {error: e};
}
if (data) {
operator.handleUsers({prepareRecordsOnly: false, users: [data]});
const updatedRoles: string[] = data.roles.split(' ');
await fetchRolesIfNeeded(serverUrl, updatedRoles);
}
return {data};
};
let ids: string[] = [];
@@ -319,58 +323,79 @@ const fetchUserOrGroupsByMentionNames = async (serverUrl: string, mentions: stri
if (groupsToFetch.length) {
await fetchGroupsByNames(serverUrl, groupsToFetch, false);
}
return {};
} catch (error) {
logDebug('error on fetchUserOrGroupsByMentionNames', getFullErrorMessage(error));
return {error};
return {data: true};
} catch (e) {
return {error: e};
}
};
export async function fetchStatusByIds(serverUrl: string, userIds: string[], fetchOnly = false) {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
if (!userIds.length) {
return {statuses: []};
}
let database;
let operator;
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = result.database;
operator = result.operator;
} catch (e) {
return {error: `${serverUrl} database not found`};
}
try {
const statuses = await client.getStatusesByIds(userIds);
if (!fetchOnly) {
const users = await queryUsersById(database, userIds).fetch();
const userStatuses = statuses.reduce((result: Record<string, UserStatus>, s) => {
result[s.user_id] = s;
return result;
}, {});
if (!fetchOnly && DatabaseManager.serverDatabases[serverUrl]) {
if (operator) {
const users = await queryUsersById(database, userIds).fetch();
const userStatuses = statuses.reduce((result: Record<string, UserStatus>, s) => {
result[s.user_id] = s;
return result;
}, {});
for (const user of users) {
const status = userStatuses[user.id];
user.prepareStatus(status?.status || General.OFFLINE);
for (const user of users) {
const status = userStatuses[user.id];
user.prepareStatus(status?.status || General.OFFLINE);
}
await operator.batchRecords(users, 'fetchStatusByIds');
}
await operator.batchRecords(users, 'fetchStatusByIds');
}
return {statuses};
} catch (error) {
logDebug('error on fetchStatusByIds', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
}
export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetchOnly = false) => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
if (!userIds.length) {
return {users: [], existingUsers: []};
}
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const currentUser = await getCurrentUser(database);
const existingUsers = await queryUsersById(database, userIds).fetch();
try {
const currentUser = await getCurrentUser(operator.database);
const existingUsers = await queryUsersById(operator.database, userIds).fetch();
if (userIds.includes(currentUser!.id)) {
existingUsers.push(currentUser!);
}
@@ -392,22 +417,30 @@ export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetc
return {users, existingUsers};
} catch (error) {
logDebug('error on fetchUsersByIds', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
};
export const fetchUsersByUsernames = async (serverUrl: string, usernames: string[], fetchOnly = false) => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
if (!usernames.length) {
return {users: []};
}
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUser = await getCurrentUser(database);
const existingUsers = await queryUsersByUsername(database, usernames).fetch();
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
const currentUser = await getCurrentUser(operator.database);
const existingUsers = await queryUsersByUsername(operator.database, usernames).fetch();
const exisitingUsersMap = existingUsers.reduce((result: Record<string, UserModel>, u) => {
result[u.username] = u;
return result;
@@ -428,80 +461,29 @@ export const fetchUsersByUsernames = async (serverUrl: string, usernames: string
return {users};
} catch (error) {
logDebug('error on fetchUsersByUsernames', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
};
export const fetchProfiles = async (serverUrl: string, page = 0, perPage: number = General.PROFILE_CHUNK_SIZE, options: any = {}, fetchOnly = false) => {
let client: Client;
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
const users = await client.getProfiles(page, perPage, options);
if (!fetchOnly) {
const currentUserId = await getCurrentUserId(database);
const toStore = removeUserFromList(currentUserId, users);
if (toStore.length) {
await operator.handleUsers({
users: toStore,
prepareRecordsOnly: false,
});
}
}
return {users};
} catch (error) {
logDebug('error on fetchProfiles', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
};
export const fetchProfilesInTeam = async (serverUrl: string, teamId: string, page = 0, perPage: number = General.PROFILE_CHUNK_SIZE, sort = '', options: any = {}, fetchOnly = false) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const users = await client.getProfilesInTeam(teamId, page, perPage, sort, options);
if (!fetchOnly) {
const currentUserId = await getCurrentUserId(database);
const toStore = removeUserFromList(currentUserId, users);
if (toStore.length) {
await operator.handleUsers({
users: toStore,
prepareRecordsOnly: false,
});
}
}
return {users};
} catch (error) {
logDebug('error on fetchProfilesInTeam', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
};
export const fetchProfilesNotInChannel = async (
serverUrl: string,
teamId: string,
channelId: string,
groupConstrained = false,
page = 0,
perPage: number = General.PROFILE_CHUNK_SIZE,
fetchOnly = false,
) => {
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
const users = await client.getProfilesNotInChannel(teamId, channelId, groupConstrained, page, perPage);
if (!fetchOnly && users.length) {
const currentUserId = await getCurrentUserId(database);
const currentUserId = await getCurrentUserId(operator.database);
const toStore = removeUserFromList(currentUserId, users);
await operator.handleUsers({
users: toStore,
@@ -511,21 +493,63 @@ export const fetchProfilesNotInChannel = async (
return {users};
} catch (error) {
logDebug('error on fetchProfilesNotInChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
};
export const fetchProfilesInTeam = async (serverUrl: string, teamId: string, page = 0, perPage: number = General.PROFILE_CHUNK_SIZE, sort = '', options: any = {}, fetchOnly = false) => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
const users = await client.getProfilesInTeam(teamId, page, perPage, sort, options);
if (!fetchOnly) {
const currentUserId = await getCurrentUserId(operator.database);
const toStore = removeUserFromList(currentUserId, users);
await operator.handleUsers({
users: toStore,
prepareRecordsOnly: false,
});
}
return {users};
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
};
export const searchProfiles = async (serverUrl: string, term: string, options: SearchUserOptions, fetchOnly = false) => {
let client: Client;
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const currentUserId = await getCurrentUserId(database);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
const currentUserId = await getCurrentUserId(operator.database);
const users = await client.searchUsers(term, options);
if (!fetchOnly) {
const {database} = operator;
const existing = await queryUsersById(database, users.map((u) => u.id)).fetchIds();
const existingSet = new Set(existing);
const usersToAdd = users.filter((u) => !existingSet.has(u.id));
@@ -540,28 +564,48 @@ export const searchProfiles = async (serverUrl: string, term: string, options: S
return {data: users};
} catch (error) {
logDebug('error on searchProfiles', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
logError('searchProfiles', error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
};
export const fetchMissingProfilesByIds = async (serverUrl: string, userIds: string[]) => {
const {users} = await fetchUsersByIds(serverUrl, userIds);
if (users) {
const statusToLoad = users.map((u) => u.id);
fetchStatusByIds(serverUrl, statusToLoad);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
const {users} = await fetchUsersByIds(serverUrl, userIds);
if (users) {
const statusToLoad = users.map((u) => u.id);
fetchStatusByIds(serverUrl, statusToLoad);
}
return {users};
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
return {users};
};
export const fetchMissingProfilesByUsernames = async (serverUrl: string, usernames: string[]) => {
const {users} = await fetchUsersByUsernames(serverUrl, usernames);
if (users) {
const statusToLoad = users.map((u) => u.id);
fetchStatusByIds(serverUrl, statusToLoad);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
try {
const {users} = await fetchUsersByUsernames(serverUrl, usernames);
if (users) {
const statusToLoad = users.map((u) => u.id);
fetchStatusByIds(serverUrl, statusToLoad);
}
return {users};
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
return {users};
};
export async function updateAllUsersSince(serverUrl: string, since: number, fetchOnly = false) {
@@ -569,13 +613,23 @@ export async function updateAllUsersSince(serverUrl: string, since: number, fetc
return {users: []};
}
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const database = operator.database;
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const currentUserId = await getCurrentUserId(database);
const userIds = (await queryAllUsers(database).fetchIds()).filter((id) => id !== currentUserId);
let userUpdates: UserProfile[] = [];
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUserId = await getCurrentUserId(database);
const userIds = (await queryAllUsers(database).fetchIds()).filter((id) => id !== currentUserId);
userUpdates = await client.getProfilesByIds(userIds, {since});
if (userUpdates.length && !fetchOnly) {
const modelsToBatch: Model[] = [];
@@ -589,9 +643,7 @@ export async function updateAllUsersSince(serverUrl: string, since: number, fetc
await operator.batchRecords(modelsToBatch, 'updateAllUsersSince');
}
} catch (error) {
logDebug('error on updateAllUsersSince', getFullErrorMessage(error));
} catch {
// Do nothing
}
@@ -599,16 +651,25 @@ export async function updateAllUsersSince(serverUrl: string, since: number, fetc
}
export async function updateUsersNoLongerVisible(serverUrl: string, prepareRecordsOnly = false): Promise<{error?: unknown; models?: Model[]}> {
let client: Client;
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const models: Model[] = [];
const serverDatabase = DatabaseManager.serverDatabases[serverUrl];
if (!serverDatabase) {
return {error: `${serverUrl} database not found`};
}
const models: Model[] = [];
try {
const knownUsers = new Set(await client.getKnownUsers());
const currentUserId = await getCurrentUserId(database);
const currentUserId = await getCurrentUserId(serverDatabase.database);
knownUsers.add(currentUserId);
const allUsers = await queryAllUsers(database).fetch();
const allUsers = await queryAllUsers(serverDatabase.database).fetch();
for (const user of allUsers) {
if (!knownUsers.has(user.id)) {
user.prepareDestroyPermanently();
@@ -616,85 +677,131 @@ export async function updateUsersNoLongerVisible(serverUrl: string, prepareRecor
}
}
if (models.length && !prepareRecordsOnly) {
operator.batchRecords(models, 'updateUsersNoLongerVisible');
serverDatabase.operator.batchRecords(models, 'updateUsersNoLongerVisible');
}
return {models};
} catch (error) {
logDebug('error on updateUsersNoLongerVisible', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
return {models};
}
export const setStatus = async (serverUrl: string, status: UserStatus) => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const client = NetworkManager.getClient(serverUrl);
const data = await client.updateStatus(status);
await updateLocalUser(serverUrl, {status: status.status});
return {data};
return {
data,
};
} catch (error) {
logDebug('error on setStatus', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const updateCustomStatus = async (serverUrl: string, customStatus: UserCustomStatus) => {
let client: Client;
try {
const client = NetworkManager.getClient(serverUrl);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
if (!customStatus.duration) {
delete customStatus.expires_at;
}
await client.updateCustomStatus(customStatus);
return {};
return {data: true};
} catch (error) {
logDebug('error on updateCustomStatus', getFullErrorMessage(error));
return {error};
}
};
export const removeRecentCustomStatus = async (serverUrl: string, customStatus: UserCustomStatus) => {
updateRecentCustomStatuses(serverUrl, customStatus, false, true);
let client: Client;
try {
const client = NetworkManager.getClient(serverUrl);
await client.removeRecentCustomStatus(customStatus);
return {};
client = NetworkManager.getClient(serverUrl);
} catch (error) {
logDebug('error on removeRecentCustomStatus', getFullErrorMessage(error));
return {error};
}
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
updateRecentCustomStatuses(serverUrl, customStatus, false, true);
try {
await client.removeRecentCustomStatus(customStatus);
} catch (error) {
return {error};
}
return {data: true};
};
export const unsetCustomStatus = async (serverUrl: string) => {
let client: Client;
try {
const client = NetworkManager.getClient(serverUrl);
await client.unsetCustomStatus();
return {};
client = NetworkManager.getClient(serverUrl);
} catch (error) {
logDebug('error on unsetCustomStatus', getFullErrorMessage(error));
return {error};
}
try {
await client.unsetCustomStatus();
} catch (error) {
return {error};
}
return {data: true};
};
export const setDefaultProfileImage = async (serverUrl: string, userId: string) => {
let client: Client;
try {
const client = NetworkManager.getClient(serverUrl);
await client.setDefaultProfileImage(userId);
updateLocalUser(serverUrl, {last_picture_update: Date.now()});
return {};
client = NetworkManager.getClient(serverUrl);
} catch (error) {
logDebug('error on setDefaultProfileImage', getFullErrorMessage(error));
return {error};
}
try {
await client.setDefaultProfileImage(userId);
updateLocalUser(serverUrl, {last_picture_update: Date.now()});
} catch (error) {
return {error};
}
return {data: true};
};
export const uploadUserProfileImage = async (serverUrl: string, localPath: string) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const currentUser = await getCurrentUser(database);
if (currentUser) {
const endpoint = `${client.getUserRoute(currentUser.id)}/image`;
@@ -707,31 +814,42 @@ export const uploadUserProfileImage = async (serverUrl: string, localPath: strin
},
});
}
return {};
} catch (error) {
logDebug('error on uploadUserProfileImage', getFullErrorMessage(error));
return {error};
} catch (e) {
return {error: e};
}
return {error: undefined};
};
export const searchUsers = async (serverUrl: string, term: string, teamId: string, channelId?: string) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const client = NetworkManager.getClient(serverUrl);
const users = await client.autocompleteUsers(term, teamId, channelId);
return {users};
} catch (error) {
logDebug('error on searchUsers', getFullErrorMessage(error));
return {error};
}
};
export const buildProfileImageUrl = (serverUrl: string, userId: string, timestamp = 0) => {
let client: Client;
try {
const client = NetworkManager.getClient(serverUrl);
return client.getProfilePictureUrl(userId, timestamp);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return '';
}
return client.getProfilePictureUrl(userId, timestamp);
};
export const autoUpdateTimezone = async (serverUrl: string) => {
@@ -740,14 +858,14 @@ export const autoUpdateTimezone = async (serverUrl: string) => {
const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = result.database;
} catch (e) {
return;
return {error: `${serverUrl} database not found`};
}
const config = await getConfig(database);
const currentUser = await getCurrentUser(database);
if (!currentUser || !config || !isTimezoneEnabled(config)) {
return;
return null;
}
// Set timezone
@@ -760,13 +878,26 @@ export const autoUpdateTimezone = async (serverUrl: string) => {
const timezone = {useAutomaticTimezone: 'true', automaticTimezone: deviceTimezone, manualTimezone: currentTimezone.manualTimezone};
await updateMe(serverUrl, {timezone});
}
return null;
};
export const fetchTeamAndChannelMembership = async (serverUrl: string, userId: string, teamId: string, channelId?: string) => {
let operator;
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
operator = result.operator;
} catch (e) {
return {error: `${serverUrl} database not found`};
}
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const requests = await Promise.all([
client.getTeamMember(teamId, userId),
channelId ? client.getChannelMember(channelId, userId) : undefined,
@@ -787,9 +918,8 @@ export const fetchTeamAndChannelMembership = async (serverUrl: string, userId: s
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat(), 'fetchTeamAndChannelMembership');
return {};
return {error: undefined};
} catch (error) {
logDebug('error on searchUsers', getFullErrorMessage(error));
return {error};
}
};
@@ -800,7 +930,7 @@ export const getAllSupportedTimezones = async (serverUrl: string) => {
const allTzs = await client.getTimezones();
return allTzs;
} catch (error) {
logDebug('error on getAllSupportedTimezones', getFullErrorMessage(error));
logError('FAILED TO GET ALL TIMEZONES', error);
return [];
}
};

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