Compare commits

..

10 Commits

Author SHA1 Message Date
d37bc4c14a FIX Yaml syntax error
Some checks failed
continuous-integration/drone/push Build was killed
continuous-integration/drone/tag Build is passing
2023-03-09 22:58:31 +01:00
2bb144042b Full test build.
Some checks failed
continuous-integration/drone/push Build encountered an error
continuous-integration/drone/tag Build encountered an error
2023-03-09 22:52:07 +01:00
b960c2f036 File release test three.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-03-09 22:36:45 +01:00
2aa1265762 Git release test two.
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
2023-03-09 22:25:44 +01:00
565c19ca64 Test file release.
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
2023-03-09 22:19:15 +01:00
b4acbef5f5 Prevent duplicate checkout.
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-09 11:39:21 +01:00
0a766bfb65 Revert "Directory node_modules permission."
Some checks failed
continuous-integration/drone/push Build is failing
This reverts commit b7cc411c3b.

Permission fix step.
2023-03-09 11:17:32 +01:00
b7cc411c3b Directory node_modules permission.
Some checks failed
continuous-integration/drone/push Build is failing
2023-03-09 11:04:03 +01:00
04b35959a3 FIX yml syntax
Some checks failed
continuous-integration/drone/push Build is failing
2023-03-09 10:41:43 +01:00
e639e69d46 First build try.
Some checks failed
continuous-integration/drone/push Build encountered an error
2023-03-09 10:40:46 +01:00
1174 changed files with 61534 additions and 63960 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

31
.drone.yml Normal file
View File

@@ -0,0 +1,31 @@
kind: pipeline
name: default
steps:
- name: permissions
image: alpine/git
commands:
- chmod -R 777 .
- name: build
image: cimg/android:2022.09.2-node
environment:
CIRCLECI: true
NODE_OPTIONS: --max_old_space_size=12000
NODE_ENV: production
BABEL_ENV: production
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

@@ -61,7 +61,6 @@
"afterColon": true
}}],
"@typescript-eslint/member-delimiter-style": 2,
"@typescript-eslint/no-unsafe-declaration-merging": "off",
"import/order": [
2,
{

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,16 +0,0 @@
name: prepare-ios-build
description: Action to prepare environment for ios build
runs:
using: composite
steps:
- 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,19 +0,0 @@
name: prepare-mobile-build
description: Action to prepare environment for mobile build
runs:
using: composite
steps:
# The required ruby version is mentioned in '.ruby-version'
- uses: ruby/setup-ruby@22fdc77bf4148f810455b226c90fb81b5cbc00a7 # v 1.171.0
- 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-file: ".nvmrc"
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,60 +0,0 @@
---
name: build-android-beta
on:
push:
branches:
- build-beta-[0-9]+
- build-beta-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-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-14-large
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-14-large
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-14-large
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-14-large
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,100 +0,0 @@
---
name: build-pr
on:
pull_request:
types:
- labeled
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
if: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for iOS' || github.event.label.name == 'Build App for Android' }}
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: ci/test
uses: ./.github/actions/test
build-ios-pr:
runs-on: macos-14-large
if: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for iOS' }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
with:
ref: ${{ github.event.pull_request.head.sha }}
- 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.event.pull_request.head.ref }}"
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: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for Android' }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
with:
ref: ${{ github.event.pull_request.head.sha }}
- 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.event.pull_request.head.ref }}"
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,21 +0,0 @@
---
name: ci
on:
push:
branches:
- main
- 'release*'
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,98 +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-14-large
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@22fdc77bf4148f810455b226c90fb81b5cbc00a7 # v1.171.0
- 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

1
.gitignore vendored
View File

@@ -103,7 +103,6 @@ detox/detox_pixel_*
# Bundle artifact
*.jsbundle
.bundle
#editor-settings
.vscode

View File

@@ -1 +0,0 @@
18.17

1
.npmrc
View File

@@ -1,2 +1 @@
save-exact=true
engine-strict=true

1
.nvmrc
View File

@@ -1 +0,0 @@
18.17

View File

@@ -1 +0,0 @@
3.0.6

View File

@@ -4,11 +4,29 @@
"output" : "moderate"
},
"requirements": {
"Node": [
{
"rule": "cli",
"binary": "node",
"semver": ">=16.0.0",
"error": "install node using nvm https://github.com/nvm-sh/nvm#installing-and-updating"
},
{
"rule": "cli",
"binary": "npm",
"semver": ">=8.5.5 <9.0.0",
"error": "install npm 8.5.5 `npm i -g npm@8.5.5"
}
],
"Android": [
{
"rule": "cli",
"binary": "emulator"
},
{
"rule": "cli",
"binary": "android"
},
{
"rule": "env",
"variable": "ANDROID_HOME",
@@ -32,14 +50,14 @@
{
"rule": "cli",
"binary": "ruby",
"semver": ">=3.0.0",
"semver": ">=2.7.1 <3.0.0",
"error": "visit rvm install https://rvm.io/rvm/install",
"platform": "darwin"
},
{
"rule": "cli",
"binary": "pod",
"semver": "1.14.3",
"semver": "1.11.3",
"platform": "darwin"
}
],

View File

@@ -34,18 +34,6 @@ A spec-compliant polyfill/ponyfill for Intl.getCanonicalLocales tested by the of
* LICENSE: MIT
---
## @formatjs/intl-listformat
This product contains '@formatjs/intl-listformat' by FormatJS.
This repository is the home of FormatJS and related libraries.
* HOMEPAGE: https://github.com/formatjs/formatjs
* LICENSE MIT
---
## @formatjs/intl-locale
@@ -136,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.
@@ -258,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
@@ -314,6 +324,41 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @nozbe/with-observables
This product contains '@nozbe/with-observables' by Nozbe.
A higher-order component for connecting RxJS Observables to React components.
* HOMEPAGE:
* https://github.com/Nozbe/withObservables
* LICENSE: MIT
MIT License
Copyright (c) Nozbe
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-camera-roll/camera-roll
@@ -531,6 +576,42 @@ Stack navigator component for iOS and Android with animated transitions and gest
---
## @rudderstack/rudder-sdk-react-native
This product contains '@rudderstack/rudder-sdk-react-native' by RudderStack.
Rudder React Native SDK
* HOMEPAGE:
* https://github.com/rudderlabs/rudder-sdk-reactnative#readme
* LICENSE: Apache-2.0
MIT License
Copyright (c) 2021 RudderStack
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.
---
## @sentry/react-native
@@ -602,51 +683,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @tsconfig/react-native
This product contains a modified version of '@tsconfig/react-native' by TSC Base.
Hosts TSConfigs for you to extend in your apps, tuned to a particular runtime environment.
* HOMEPAGE:
* https://github.com/tsconfig/bases
* LICENSE: MIT
---
## @voximplant/react-native-foreground-service
This product contains a modified version of '@voximplant/react-native-foreground-service' by Voximplant.
A foreground service performs some operation that is noticeable to the user.
* HOMEPAGE:
* https://github.com/voximplant/react-native-foreground-service
* LICENSE: MIT License
Copyright (c) 2019 Zingaya, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
@@ -1333,41 +1369,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## path-to-regexp
This product contains 'path-to-regexp' by lastuniverse.
Turn a path string such as /user/:id or /user/:id(\d+) into a regular expression
* HOMEPAGE:
* https://github.com/lastuniverse/path-to-regex
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2018 Roman Surmanidze (kapa6a3er@gmail.com)
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
@@ -1532,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 (8.1.0+)
- **Supported iOS versions:** 12.4+
- **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
@@ -57,7 +57,7 @@ You can still access it! We have moved the code from master to the [v1 branch](h
### I keep getting a message "Cannot connect to the server. Please check your server URL and internet connection."
This sometimes appears when there is an issue with the SSL certificate configuration.
This sometimes appears when there is an issue with the SSL certitificate configuration.
To check that your SSL certificate is set up correctly, test the SSL certificate by visiting a site such as https://www.ssllabs.com/ssltest/index.html. If theres an error about the missing chain or certificate path, there is likely an intermediate certificate missing that needs to be included.

View File

@@ -13,8 +13,8 @@ apply plugin: 'kotlin-android'
// root = file("../")
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
// reactNativeDir = file("../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
// codegenDir = file("../node_modules/@react-native/codegen")
// The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen
// codegenDir = file("../node_modules/react-native-codegen")
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
// cliFile = file("../node_modules/react-native/cli.js")
/* Variants */
@@ -98,7 +98,6 @@ def reactNativeArchitectures() {
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdkVersion rootProject.ext.compileSdkVersion
namespace "com.mattermost.rnbeta"
@@ -111,8 +110,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 512
versionName "2.15.0"
versionCode 461
versionName "2.1.0"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
@@ -191,6 +190,8 @@ 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")
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.squareup.okhttp3', module:'okhttp'
@@ -206,7 +207,6 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'androidx.window:window-core:1.1.0'
implementation 'androidx.window:window-rxjava3:1.0.0'
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
@@ -216,6 +216,7 @@ dependencies {
androidTestImplementation('com.wix:detox:+')
implementation project(':reactnativenotifications')
implementation project(':watermelondb')
implementation project(':watermelondb-jsi')
}
@@ -223,22 +224,25 @@ 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.2.0'
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.1.0'
}
if (details.requested.name == 'play-services-tasks') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.0.2'
}
if (details.requested.name == 'play-services-stats') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '17.0.3'
}
if (details.requested.name == 'play-services-basement') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.2.0'
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.1.0'
}
if (details.requested.name == 'okhttp') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.12.0'
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
}
if (details.requested.name == 'okhttp-tls') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.12.0'
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
}
if (details.requested.name == 'okhttp-urlconnection') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.12.0'
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
}
}
}

Binary file not shown.

View File

@@ -7,5 +7,10 @@
<application
android:usesCleartextTraffic="true"
tools:targetApi="28"
tools:ignore="GoogleAppIndexingWarning"/>
tools:ignore="GoogleAppIndexingWarning">
<activity
android:name="com.facebook.react.devsupport.DevSettingsActivity"
android:exported="false"
/>
</application>
</manifest>

View File

@@ -13,7 +13,6 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission android:name="android.permission.BLUETOOTH"
@@ -103,8 +102,5 @@
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<!-- For Calls microphone to work in the background -->
<service android:name="com.voximplant.foregroundservice.VIForegroundService"/>
</application>
</application>
</manifest>

View File

@@ -29,7 +29,7 @@ import androidx.core.app.RemoteInput;
import androidx.core.graphics.drawable.IconCompat;
import com.mattermost.rnbeta.*;
import com.nozbe.watermelondb.WMDatabase;
import com.nozbe.watermelondb.Database;
import java.io.IOException;
import java.util.Date;
@@ -52,7 +52,6 @@ public class CustomPushNotificationHelper {
public static final String PUSH_TYPE_MESSAGE = "message";
public static final String PUSH_TYPE_CLEAR = "clear";
public static final String PUSH_TYPE_SESSION = "session";
public static final String CATEGORY_CAN_REPLY = "CAN_REPLY";
private static NotificationChannel mHighImportanceChannel;
private static NotificationChannel mMinImportanceChannel;
@@ -81,7 +80,7 @@ public class CustomPushNotificationHelper {
.setKey(senderId)
.setName(senderName);
if (serverUrl != null && type != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
try {
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
if (avatar != null) {
@@ -133,9 +132,8 @@ public class CustomPushNotificationHelper {
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");
boolean canReply = bundle.containsKey("category") && Objects.equals(bundle.getString("category"), CATEGORY_CAN_REPLY);
if (android.text.TextUtils.isEmpty(postId) || serverUrl == null || !canReply) {
if (android.text.TextUtils.isEmpty(postId) || serverUrl == null) {
return;
}
@@ -183,7 +181,7 @@ public class CustomPushNotificationHelper {
String rootId = bundle.getString("root_id");
int notificationId = postId != null ? postId.hashCode() : MESSAGE_NOTIFICATION_ID;
boolean is_crt_enabled = bundle.containsKey("is_crt_enabled") && Objects.equals(bundle.getString("is_crt_enabled"), "true");
boolean is_crt_enabled = bundle.containsKey("is_crt_enabled") && bundle.getString("is_crt_enabled").equals("true");
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
addNotificationExtras(notification, bundle);
@@ -275,7 +273,7 @@ public class CustomPushNotificationHelper {
.setKey(senderId)
.setName("Me");
if (serverUrl != null && type != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
try {
Bitmap avatar = userAvatar(context, serverUrl, "me", urlOverride);
if (avatar != null) {
@@ -418,7 +416,7 @@ public class CustomPushNotificationHelper {
} else {
DatabaseHelper dbHelper = DatabaseHelper.Companion.getInstance();
if (dbHelper != null) {
WMDatabase db = getDatabaseForServer(dbHelper, context, serverUrl);
Database db = getDatabaseForServer(dbHelper, context, serverUrl);
if (db != null) {
lastUpdateAt = getLastPictureUpdate(db, userId);
if (lastUpdateAt == null) {

View File

@@ -1,21 +1,17 @@
package com.mattermost.helpers
import android.content.Context
import android.database.Cursor
import android.net.Uri
import com.facebook.react.bridge.WritableMap
import com.nozbe.watermelondb.WMDatabase
import com.nozbe.watermelondb.Database
import java.lang.Exception
import org.json.JSONArray
import org.json.JSONObject
typealias QueryArgs = Array<Any?>
class DatabaseHelper {
var defaultDatabase: WMDatabase? = null
var defaultDatabase: Database? = null
val onlyServerUrl: String?
get() {
@@ -43,7 +39,7 @@ class DatabaseHelper {
private fun setDefaultDatabase(context: Context) {
val databaseName = "app.db"
val databasePath = Uri.fromFile(context.filesDir).toString() + "/" + databaseName
defaultDatabase = WMDatabase.getInstance(databasePath, context)
defaultDatabase = Database(databasePath, context)
}
internal fun JSONObject.toMap(): Map<String, Any?> = keys().asSequence().associateWith { it ->
@@ -77,15 +73,3 @@ class DatabaseHelper {
private set
}
}
fun WritableMap.mapCursor(cursor: Cursor) {
for (i in 0 until cursor.columnCount) {
when (cursor.getType(i)) {
Cursor.FIELD_TYPE_NULL -> putNull(cursor.getColumnName(i))
Cursor.FIELD_TYPE_INTEGER -> putDouble(cursor.getColumnName(i), cursor.getDouble(i))
Cursor.FIELD_TYPE_FLOAT -> putDouble(cursor.getColumnName(i), cursor.getDouble(i))
Cursor.FIELD_TYPE_STRING -> putString(cursor.getColumnName(i), cursor.getString(i))
else -> putString(cursor.getColumnName(i), "")
}
}
}

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

@@ -2,9 +2,9 @@ package com.mattermost.helpers.database_extension
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.nozbe.watermelondb.WMDatabase
import com.nozbe.watermelondb.Database
fun insertCategory(db: WMDatabase, category: ReadableMap) {
fun insertCategory(db: Database, category: ReadableMap) {
try {
val id = category.getString("id") ?: return
val collapsed = false
@@ -31,7 +31,7 @@ fun insertCategory(db: WMDatabase, category: ReadableMap) {
}
}
fun insertCategoryChannels(db: WMDatabase, categoryId: String, teamId: String, channelIds: ReadableArray) {
fun insertCategoryChannels(db: Database, categoryId: String, teamId: String, channelIds: ReadableArray) {
try {
for (i in 0 until channelIds.size()) {
val channelId = channelIds.getString(i)
@@ -50,7 +50,7 @@ fun insertCategoryChannels(db: WMDatabase, categoryId: String, teamId: String, c
}
}
fun insertCategoriesWithChannels(db: WMDatabase, orderCategories: ReadableMap) {
fun insertCategoriesWithChannels(db: Database, orderCategories: ReadableMap) {
val categories = orderCategories.getArray("categories") ?: return
for (i in 0 until categories.size()) {
val category = categories.getMap(i)
@@ -64,7 +64,7 @@ fun insertCategoriesWithChannels(db: WMDatabase, orderCategories: ReadableMap) {
}
}
fun insertChannelToDefaultCategory(db: WMDatabase, categoryChannels: ReadableArray) {
fun insertChannelToDefaultCategory(db: Database, categoryChannels: ReadableArray) {
try {
for (i in 0 until categoryChannels.size()) {
val cc = categoryChannels.getMap(i)

View File

@@ -3,11 +3,11 @@ 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.WMDatabase
import com.nozbe.watermelondb.Database
import org.json.JSONException
import org.json.JSONObject
fun findChannel(db: WMDatabase?, channelId: String): Boolean {
fun findChannel(db: Database?, channelId: String): Boolean {
if (db != null) {
val team = find(db, "Channel", channelId)
return team != null
@@ -15,7 +15,7 @@ fun findChannel(db: WMDatabase?, channelId: String): Boolean {
return false
}
fun findMyChannel(db: WMDatabase?, channelId: String): Boolean {
fun findMyChannel(db: Database?, channelId: String): Boolean {
if (db != null) {
val team = find(db, "MyChannel", channelId)
return team != null
@@ -23,7 +23,7 @@ fun findMyChannel(db: WMDatabase?, channelId: String): Boolean {
return false
}
internal fun handleChannel(db: WMDatabase, channel: ReadableMap) {
internal fun handleChannel(db: Database, channel: ReadableMap) {
try {
val exists = channel.getString("id")?.let { findChannel(db, it) } ?: false
if (!exists) {
@@ -37,7 +37,7 @@ internal fun handleChannel(db: WMDatabase, channel: ReadableMap) {
}
}
internal fun DatabaseHelper.handleMyChannel(db: WMDatabase, myChannel: ReadableMap, postsData: ReadableMap?, receivingThreads: Boolean) {
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
@@ -71,7 +71,7 @@ internal fun DatabaseHelper.handleMyChannel(db: WMDatabase, myChannel: ReadableM
}
}
fun insertChannel(db: WMDatabase, channel: JSONObject): Boolean {
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 }
@@ -104,7 +104,7 @@ fun insertChannel(db: WMDatabase, channel: JSONObject): Boolean {
}
}
fun insertChannelInfo(db: WMDatabase, channel: JSONObject) {
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) { "" }
@@ -123,7 +123,7 @@ fun insertChannelInfo(db: WMDatabase, channel: JSONObject) {
}
}
fun insertMyChannel(db: WMDatabase, myChanel: JSONObject): Boolean {
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) { "" }
@@ -156,7 +156,7 @@ fun insertMyChannel(db: WMDatabase, myChanel: JSONObject): Boolean {
}
}
fun insertMyChannelSettings(db: WMDatabase, myChanel: JSONObject) {
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 }
@@ -173,7 +173,7 @@ fun insertMyChannelSettings(db: WMDatabase, myChanel: JSONObject) {
}
}
fun insertChannelMember(db: WMDatabase, myChanel: JSONObject) {
fun insertChannelMember(db: Database, myChanel: JSONObject) {
try {
val userId = queryCurrentUserId(db) ?: return
val channelId = try { myChanel.getString("id") } catch (e: JSONException) { return }
@@ -193,7 +193,7 @@ fun insertChannelMember(db: WMDatabase, myChanel: JSONObject) {
}
}
fun updateMyChannel(db: WMDatabase, myChanel: JSONObject) {
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 }

View File

@@ -1,9 +1,9 @@
package com.mattermost.helpers.database_extension
import com.nozbe.watermelondb.WMDatabase
import com.nozbe.watermelondb.Database
import org.json.JSONArray
internal fun insertCustomEmojis(db: WMDatabase, customEmojis: JSONArray) {
internal fun insertCustomEmojis(db: Database, customEmojis: JSONArray) {
for (i in 0 until customEmojis.length()) {
try {
val emoji = customEmojis.getJSONObject(i)

View File

@@ -1,10 +1,10 @@
package com.mattermost.helpers.database_extension
import com.nozbe.watermelondb.WMDatabase
import com.nozbe.watermelondb.Database
import org.json.JSONArray
import org.json.JSONException
internal fun insertFiles(db: WMDatabase, files: JSONArray) {
internal fun insertFiles(db: Database, files: JSONArray) {
try {
for (i in 0 until files.length()) {
val file = files.getJSONObject(i)

View File

@@ -5,13 +5,13 @@ import android.text.TextUtils
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableMap
import com.mattermost.helpers.DatabaseHelper
import com.mattermost.helpers.QueryArgs
import com.mattermost.helpers.mapCursor
import com.nozbe.watermelondb.WMDatabase
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: WMDatabase, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) {
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) }
@@ -50,14 +50,14 @@ fun DatabaseHelper.getServerUrlForIdentifier(identifier: String): String? {
return null
}
fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): WMDatabase? {
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 WMDatabase.getInstance(databasePath, context!!)
return Database(databasePath, context!!)
}
}
} catch (e: Exception) {
@@ -67,7 +67,7 @@ fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): W
return null
}
fun find(db: WMDatabase, tableName: String, id: String?): ReadableMap? {
fun find(db: Database, tableName: String, id: String?): ReadableMap? {
try {
db.rawQuery(
"SELECT * FROM $tableName WHERE id == ? LIMIT 1",
@@ -87,7 +87,7 @@ fun find(db: WMDatabase, tableName: String, id: String?): ReadableMap? {
}
}
fun findByColumns(db: WMDatabase, tableName: String, columnNames: Array<String>, values: QueryArgs): ReadableMap? {
fun findByColumns(db: Database, tableName: String, columnNames: Array<String>, values: QueryArgs): ReadableMap? {
try {
val whereString = columnNames.joinToString(" AND ") { "$it = ?" }
db.rawQuery(
@@ -108,7 +108,7 @@ fun findByColumns(db: WMDatabase, tableName: String, columnNames: Array<String>,
}
}
fun queryIds(db: WMDatabase, tableName: String, ids: Array<String>): List<String> {
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 {
@@ -129,7 +129,7 @@ fun queryIds(db: WMDatabase, tableName: String, ids: Array<String>): List<String
return list
}
fun queryByColumn(db: WMDatabase, tableName: String, columnName: String, values: Array<Any?>): List<String> {
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 {
@@ -149,7 +149,7 @@ fun queryByColumn(db: WMDatabase, tableName: String, columnName: String, values:
return list
}
fun countByColumn(db: WMDatabase, tableName: String, columnName: String, value: Any?): Int {
fun countByColumn(db: Database, tableName: String, columnName: String, value: Any?): Int {
try {
db.rawQuery(
"SELECT COUNT(*) FROM $tableName WHERE $columnName == ? LIMIT 1",

View File

@@ -3,13 +3,13 @@ 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.WMDatabase
import com.nozbe.watermelondb.Database
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import kotlin.Exception
internal fun queryLastPostCreateAt(db: WMDatabase?, channelId: String): Double? {
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"
@@ -35,7 +35,7 @@ internal fun queryLastPostCreateAt(db: WMDatabase?, channelId: String): Double?
return null
}
fun queryPostSinceForChannel(db: WMDatabase?, channelId: String): Double? {
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
try {
if (db != null) {
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
@@ -57,7 +57,7 @@ fun queryPostSinceForChannel(db: WMDatabase?, channelId: String): Double? {
return null
}
fun queryLastPostInThread(db: WMDatabase?, rootId: String): Double? {
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"
@@ -75,7 +75,7 @@ fun queryLastPostInThread(db: WMDatabase?, rootId: String): Double? {
return null
}
internal fun insertPost(db: WMDatabase, post: JSONObject) {
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 }
@@ -86,7 +86,6 @@ internal fun insertPost(db: WMDatabase, post: JSONObject) {
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) { "" }
@@ -101,13 +100,13 @@ internal fun insertPost(db: WMDatabase, post: JSONObject) {
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,
(id, channel_id, create_at, delete_at, update_at, edit_at, is_pinned, message, metadata, original_id, pending_post_id,
previous_post_id, root_id, type, user_id, props, _changed, _status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
""".trimIndent(),
arrayOf(
id, channelId, createAt, deleteAt, updateAt, editAt,
isPinned, message, messageSource, metadata.toString(),
isPinned, message, metadata.toString(),
originalId, pendingId, prevId, rootId,
type, userId, props
)
@@ -129,7 +128,7 @@ internal fun insertPost(db: WMDatabase, post: JSONObject) {
}
}
internal fun updatePost(db: WMDatabase, post: JSONObject) {
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 }
@@ -140,7 +139,6 @@ internal fun updatePost(db: WMDatabase, post: JSONObject) {
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) { "" }
@@ -156,13 +154,13 @@ internal fun updatePost(db: WMDatabase, post: JSONObject) {
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 = ?,
is_pinned = ?, message = ?, metadata = ?, original_id = ?, pending_post_id = ?, previous_post_id = ?,
root_id = ?, type = ?, user_id = ?, props = ?, _status = 'updated'
WHERE id = ?
""".trimIndent(),
arrayOf(
channelId, createAt, deleteAt, updateAt, editAt,
isPinned, message, messageSource, metadata.toString(),
isPinned, message, metadata.toString(),
originalId, pendingId, prevId, rootId,
type, userId, props,
id,
@@ -182,7 +180,7 @@ internal fun updatePost(db: WMDatabase, post: JSONObject) {
}
}
fun DatabaseHelper.handlePosts(db: WMDatabase, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
fun DatabaseHelper.handlePosts(db: Database, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
try {
if (postsData != null) {

View File

@@ -4,8 +4,8 @@ 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.mattermost.helpers.mapCursor
import com.nozbe.watermelondb.WMDatabase
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()) {
@@ -18,7 +18,7 @@ internal fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest:
return null
}
internal fun insertPostInChannel(db: WMDatabase, channelId: String, earliest: Double, latest: Double): ReadableMap? {
internal fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap? {
return try {
val id = RandomId.generate()
db.execute(
@@ -41,7 +41,7 @@ internal fun insertPostInChannel(db: WMDatabase, channelId: String, earliest: Do
}
}
internal fun mergePostsInChannel(db: WMDatabase, existingChunks: ReadableArray, newChunk: ReadableMap) {
internal fun mergePostsInChannel(db: Database, existingChunks: ReadableArray, newChunk: ReadableMap) {
for (i in 0 until existingChunks.size()) {
try {
val chunk = existingChunks.getMap(i)
@@ -56,7 +56,7 @@ internal fun mergePostsInChannel(db: WMDatabase, existingChunks: ReadableArray,
}
}
internal fun handlePostsInChannel(db: WMDatabase, channelId: String, earliest: Double, latest: Double) {
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 = ?",

View File

@@ -1,10 +1,10 @@
package com.mattermost.helpers.database_extension
import com.facebook.react.bridge.Arguments
import com.mattermost.helpers.mapCursor
import com.nozbe.watermelondb.WMDatabase
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.mapCursor
fun getTeammateDisplayNameSetting(db: WMDatabase): String {
fun getTeammateDisplayNameSetting(db: Database): String {
val configSetting = queryConfigDisplayNameSetting(db)
if (configSetting != null) {
return configSetting

View File

@@ -1,10 +1,10 @@
package com.mattermost.helpers.database_extension
import com.mattermost.helpers.RandomId
import com.nozbe.watermelondb.WMDatabase
import com.nozbe.watermelondb.Database
import org.json.JSONArray
internal fun insertReactions(db: WMDatabase, reactions: JSONArray) {
internal fun insertReactions(db: Database, reactions: JSONArray) {
for (i in 0 until reactions.length()) {
try {
val reaction = reactions.getJSONObject(i)

View File

@@ -1,19 +1,19 @@
package com.mattermost.helpers.database_extension
import com.nozbe.watermelondb.WMDatabase
import com.nozbe.watermelondb.Database
import org.json.JSONObject
fun queryCurrentUserId(db: WMDatabase): String? {
fun queryCurrentUserId(db: Database): String? {
val result = find(db, "System", "currentUserId")
return result?.getString("value")?.removeSurrounding("\"")
}
fun queryCurrentTeamId(db: WMDatabase): String? {
fun queryCurrentTeamId(db: Database): String? {
val result = find(db, "System", "currentTeamId")
return result?.getString("value")?.removeSurrounding("\"")
}
fun queryConfigDisplayNameSetting(db: WMDatabase): String? {
fun queryConfigDisplayNameSetting(db: Database): String? {
val license = find(db, "System", "license")
val lockDisplayName = find(db, "Config", "LockTeammateNameDisplay")
val displayName = find(db, "Config", "TeammateNameDisplay")

View File

@@ -3,10 +3,10 @@ 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.mattermost.helpers.mapCursor
import com.nozbe.watermelondb.WMDatabase
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.mapCursor
fun findTeam(db: WMDatabase?, teamId: String): Boolean {
fun findTeam(db: Database?, teamId: String): Boolean {
if (db != null) {
val team = find(db, "Team", teamId)
return team != null
@@ -14,7 +14,7 @@ fun findTeam(db: WMDatabase?, teamId: String): Boolean {
return false
}
fun findMyTeam(db: WMDatabase?, teamId: String): Boolean {
fun findMyTeam(db: Database?, teamId: String): Boolean {
if (db != null) {
val team = find(db, "MyTeam", teamId)
return team != null
@@ -22,7 +22,7 @@ fun findMyTeam(db: WMDatabase?, teamId: String): Boolean {
return false
}
fun queryMyTeams(db: WMDatabase?): ArrayList<ReadableMap>? {
fun queryMyTeams(db: Database?): ArrayList<ReadableMap>? {
db?.rawQuery("SELECT * FROM MyTeam")?.use { cursor ->
val results = ArrayList<ReadableMap>()
if (cursor.count > 0) {
@@ -38,7 +38,7 @@ fun queryMyTeams(db: WMDatabase?): ArrayList<ReadableMap>? {
return null
}
fun insertTeam(db: WMDatabase, team: ReadableMap): Boolean {
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) {
@@ -78,7 +78,7 @@ fun insertTeam(db: WMDatabase, team: ReadableMap): Boolean {
}
}
fun insertMyTeam(db: WMDatabase, myTeam: ReadableMap): Boolean {
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) { "" }

View File

@@ -5,11 +5,11 @@ 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.mattermost.helpers.mapCursor
import com.nozbe.watermelondb.WMDatabase
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.mapCursor
import org.json.JSONObject
internal fun insertThread(db: WMDatabase, thread: ReadableMap) {
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 }
@@ -36,7 +36,7 @@ internal fun insertThread(db: WMDatabase, thread: ReadableMap) {
}
}
internal fun updateThread(db: WMDatabase, thread: ReadableMap, existingRecord: ReadableMap) {
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 }
@@ -63,7 +63,7 @@ internal fun updateThread(db: WMDatabase, thread: ReadableMap, existingRecord: R
}
}
internal fun insertThreadParticipants(db: WMDatabase, threadId: String, participants: ReadableArray) {
internal fun insertThreadParticipants(db: Database, threadId: String, participants: ReadableArray) {
for (i in 0 until participants.size()) {
try {
val participant = participants.getMap(i)
@@ -82,7 +82,7 @@ internal fun insertThreadParticipants(db: WMDatabase, threadId: String, particip
}
}
fun insertTeamThreadsSync(db: WMDatabase, teamId: String, earliest: Double, latest: Double) {
fun insertTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double) {
try {
val query = """
INSERT INTO TeamThreadsSync (id, _changed, _status, earliest, latest)
@@ -94,7 +94,7 @@ fun insertTeamThreadsSync(db: WMDatabase, teamId: String, earliest: Double, late
}
}
fun updateTeamThreadsSync(db: WMDatabase, teamId: String, earliest: Double, latest: Double, existingRecord: ReadableMap) {
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"))
@@ -105,7 +105,7 @@ fun updateTeamThreadsSync(db: WMDatabase, teamId: String, earliest: Double, late
}
}
fun syncParticipants(db: WMDatabase, thread: ReadableMap) {
fun syncParticipants(db: Database, thread: ReadableMap) {
try {
val threadId = thread.getString("id")
val participants = thread.getArray("participants")
@@ -121,7 +121,7 @@ fun syncParticipants(db: WMDatabase, thread: ReadableMap) {
}
}
internal fun handlePostsInThread(db: WMDatabase, postsInThread: Map<String, List<JSONObject>>) {
internal fun handlePostsInThread(db: Database, postsInThread: Map<String, List<JSONObject>>) {
postsInThread.forEach { (key, list) ->
try {
val sorted = list.sortedBy { it.getDouble("create_at") }
@@ -161,7 +161,7 @@ internal fun handlePostsInThread(db: WMDatabase, postsInThread: Map<String, List
}
}
fun handleThreads(db: WMDatabase, threads: ArrayList<ReadableMap>, teamId: String?) {
fun handleThreads(db: Database, threads: ArrayList<ReadableMap>, teamId: String?) {
val teamIds = ArrayList<String>()
if (teamId.isNullOrEmpty()) {
val myTeams = queryMyTeams(db)
@@ -186,7 +186,7 @@ fun handleThreads(db: WMDatabase, threads: ArrayList<ReadableMap>, teamId: Strin
handleTeamThreadsSync(db, threads, teamIds)
}
fun handleThread(db: WMDatabase, thread: ReadableMap, teamIds: ArrayList<String>) {
fun handleThread(db: Database, thread: ReadableMap, teamIds: ArrayList<String>) {
// Insert/Update the thread
val threadId = thread.getString("id")
val isFollowing = thread.getBoolean("is_following")
@@ -207,7 +207,7 @@ fun handleThread(db: WMDatabase, thread: ReadableMap, teamIds: ArrayList<String>
}
}
fun handleThreadInTeam(db: WMDatabase, thread: ReadableMap, teamId: String) {
fun handleThreadInTeam(db: Database, thread: ReadableMap, teamId: String) {
val threadId = thread.getString("id") ?: return
val existingRecord = findByColumns(
db,
@@ -229,7 +229,7 @@ fun handleThreadInTeam(db: WMDatabase, thread: ReadableMap, teamId: String) {
}
}
fun handleTeamThreadsSync(db: WMDatabase, threadList: ArrayList<ReadableMap>, teamIds: ArrayList<String>) {
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") }

View File

@@ -4,9 +4,9 @@ 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.WMDatabase
import com.nozbe.watermelondb.Database
fun getLastPictureUpdate(db: WMDatabase?, userId: String): Double? {
fun getLastPictureUpdate(db: Database?, userId: String): Double? {
try {
if (db != null) {
var id = userId
@@ -28,7 +28,7 @@ fun getLastPictureUpdate(db: WMDatabase?, userId: String): Double? {
return null
}
fun getCurrentUserLocale(db: WMDatabase): String {
fun getCurrentUserLocale(db: Database): String {
try {
val currentUserId = queryCurrentUserId(db) ?: return "en"
val userQuery = "SELECT locale FROM User WHERE id=?"
@@ -45,7 +45,7 @@ fun getCurrentUserLocale(db: WMDatabase): String {
return "en"
}
fun handleUsers(db: WMDatabase, users: ReadableArray) {
fun handleUsers(db: Database, users: ReadableArray) {
for (i in 0 until users.size()) {
val user = users.getMap(i)
val roles = user.getString("roles") ?: ""

View File

@@ -7,9 +7,9 @@ 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.WMDatabase
import com.nozbe.watermelondb.Database
suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: WMDatabase, serverUrl: String, teamId: String): ReadableMap? {
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")
@@ -20,7 +20,7 @@ suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: WMD
}
}
fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: WMDatabase, channel: ReadableMap): ReadableArray? {
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()
@@ -44,7 +44,7 @@ fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: WMDa
return categoryChannels
}
private fun categoryChannelForTeam(db: WMDatabase, channelId: String, teamId: String?, type: String): ReadableMap? {
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")

View File

@@ -8,11 +8,12 @@ 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.WMDatabase
import com.nozbe.watermelondb.Database
import java.text.Collator
import java.util.Locale
import kotlin.math.max
suspend fun PushNotificationDataRunnable.Companion.fetchMyChannel(db: WMDatabase, serverUrl: String, channelId: String, isCRTEnabled: Boolean): Triple<ReadableMap?, ReadableMap?, ReadableArray?> {
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) }
@@ -108,7 +109,7 @@ private suspend fun PushNotificationDataRunnable.Companion.fetchMyChannelData(se
return null
}
private suspend fun PushNotificationDataRunnable.Companion.fetchProfileInChannel(db: WMDatabase, serverUrl: String, channelId: String): ReadableArray? {
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=")

View File

@@ -9,10 +9,10 @@ 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.WMDatabase
import com.nozbe.watermelondb.Database
internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts(
db: WMDatabase, serverUrl: String, channelId: String, isCRTEnabled: Boolean,
db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean,
rootId: String?, loadedProfiles: ReadableArray?
): ReadableMap? {
return try {
@@ -66,20 +66,6 @@ internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts(
}
}
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)
@@ -87,22 +73,17 @@ internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts(
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 (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")

View File

@@ -4,9 +4,9 @@ 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.WMDatabase
import com.nozbe.watermelondb.Database
suspend fun PushNotificationDataRunnable.Companion.fetchTeamIfNeeded(db: WMDatabase, serverUrl: String, teamId: String): Pair<ReadableMap?, ReadableMap?> {
suspend fun PushNotificationDataRunnable.Companion.fetchTeamIfNeeded(db: Database, serverUrl: String, teamId: String): Pair<ReadableMap?, ReadableMap?> {
return try {
var team: ReadableMap? = null
var myTeam: ReadableMap? = null

View File

@@ -3,9 +3,9 @@ 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.WMDatabase
import com.nozbe.watermelondb.Database
internal suspend fun PushNotificationDataRunnable.Companion.fetchThread(db: WMDatabase, serverUrl: String, threadId: String, teamId: String?): ReadableMap? {
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

View File

@@ -1,13 +1,9 @@
package com.mattermost.rnbeta
import android.app.Activity
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import androidx.window.layout.WindowMetricsCalculator
import androidx.window.rxjava3.layout.windowLayoutInfoObservable
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
@@ -16,23 +12,6 @@ import io.reactivex.rxjava3.disposables.Disposable
class FoldableObserver(private val activity: Activity) {
private var disposable: Disposable? = null
private lateinit var observable: Observable<WindowLayoutInfo>
public var isDeviceFolded: Boolean = false
companion object {
private var instance: FoldableObserver? = null
fun getInstance(activity: Activity): FoldableObserver {
if (instance == null) {
instance = FoldableObserver(activity)
}
return instance!!
}
fun getInstance(): FoldableObserver? {
return instance
}
}
fun onCreate() {
observable = WindowInfoTracker.getOrCreate(activity)
@@ -46,8 +25,20 @@ class FoldableObserver(private val activity: Activity) {
disposable = observable.observeOn(AndroidSchedulers.mainThread())
.subscribe { layoutInfo ->
val splitViewModule = SplitViewModule.getInstance()
setIsDeviceFolded(layoutInfo)
splitViewModule?.setDeviceFolded()
val foldingFeature = layoutInfo.displayFeatures
.filterIsInstance<FoldingFeature>()
.firstOrNull()
when {
foldingFeature?.state === FoldingFeature.State.FLAT ->
splitViewModule?.setDeviceFolded(false)
isTableTopPosture(foldingFeature) ->
splitViewModule?.setDeviceFolded(false)
isBookPosture(foldingFeature) ->
splitViewModule?.setDeviceFolded(false)
else -> {
splitViewModule?.setDeviceFolded(true)
}
}
}
}
@@ -55,31 +46,6 @@ class FoldableObserver(private val activity: Activity) {
disposable?.dispose()
}
private fun setIsDeviceFolded(layoutInfo: WindowLayoutInfo) {
val foldingFeature = layoutInfo.displayFeatures
.filterIsInstance<FoldingFeature>()
.firstOrNull()
isDeviceFolded = when {
foldingFeature === null -> isCompactView()
foldingFeature.state === FoldingFeature.State.FLAT -> false
isTableTopPosture(foldingFeature) -> false
isBookPosture(foldingFeature) -> false
else -> true
}
}
fun isCompactView(): Boolean {
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)
val width = metrics.bounds.width()
val height = metrics.bounds.height()
val density = activity.resources.displayMetrics.density
val windowSizeClass = WindowSizeClass.compute(width/density, height/density)
val widthWindowSizeClass = windowSizeClass.windowWidthSizeClass
val heightWindowSizeClass = windowSizeClass.windowHeightSizeClass
return widthWindowSizeClass === WindowWidthSizeClass.COMPACT || heightWindowSizeClass === WindowHeightSizeClass.COMPACT
}
private fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL

View File

@@ -18,7 +18,7 @@ import java.util.Objects;
public class MainActivity extends NavigationActivity {
private boolean HWKeyboardConnected = false;
private final FoldableObserver foldableObserver = FoldableObserver.Companion.getInstance(this);
private final FoldableObserver foldableObserver = new FoldableObserver(this);
@Override
protected String getMainComponentName() {
@@ -36,7 +36,10 @@ public class MainActivity extends NavigationActivity {
this,
Objects.requireNonNull(getMainComponentName()),
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.getFabricEnabled());
DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
// If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
);
}
@Override

View File

@@ -5,6 +5,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEm
import com.learnium.RNDeviceInfo.resolver.DeviceTypeResolver
class SplitViewModule(private var reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private var isDeviceFolded: Boolean = false
private var listenerCount = 0
companion object {
@@ -38,11 +39,7 @@ class SplitViewModule(private var reactContext: ReactApplicationContext) : React
if (currentActivity != null) {
val deviceResolver = DeviceTypeResolver(this.reactContext)
val map = Arguments.createMap()
var isSplitView = folded;
if (currentActivity?.isInMultiWindowMode == true) {
isSplitView = FoldableObserver.getInstance()?.isCompactView() == true
}
map.putBoolean("isSplitView", isSplitView)
map.putBoolean("isSplitView", currentActivity!!.isInMultiWindowMode || folded)
map.putBoolean("isTablet", deviceResolver.isTablet)
return map
}
@@ -50,16 +47,17 @@ class SplitViewModule(private var reactContext: ReactApplicationContext) : React
return null
}
fun setDeviceFolded() {
val map = getSplitViewResults(FoldableObserver.getInstance()?.isDeviceFolded == true)
if (listenerCount > 0) {
fun setDeviceFolded(folded: Boolean) {
val map = getSplitViewResults(folded)
if (listenerCount > 0 && isDeviceFolded != folded) {
sendEvent(map)
}
isDeviceFolded = folded
}
@ReactMethod(isBlockingSynchronousMethod = true)
fun isRunningInSplitView(): WritableMap? {
return getSplitViewResults(FoldableObserver.getInstance()?.isDeviceFolded == true)
@ReactMethod
fun isRunningInSplitView(promise: Promise) {
promise.resolve(getSplitViewResults(isDeviceFolded))
}
@ReactMethod

View File

@@ -16,7 +16,7 @@
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
<selector>
<!--
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'

View File

@@ -7,10 +7,10 @@ buildscript {
compileSdkVersion = 33
targetSdkVersion = 33
supportLibVersion = "33.0.0"
kotlinVersion = "1.8.21"
kotlin_version = kotlinVersion
kotlinVersion = "1.7.21"
kotlin_version = "1.7.21"
firebaseVersion = "23.1.1"
RNNKotlinVersion = kotlinVersion
firebaseVersion = "23.3.1"
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
@@ -21,9 +21,9 @@ buildscript {
google()
}
dependencies {
classpath("com.android.tools.build:gradle")
classpath("com.android.tools.build:gradle:7.3.1")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath('com.google.gms:google-services:4.4.0')
classpath('com.google.gms:google-services:4.3.15')
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.182.0
FLIPPER_VERSION=0.177.0
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using

Binary file not shown.

View File

@@ -1,6 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip

20
android/gradlew vendored
View File

@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,11 +80,11 @@ do
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
@@ -143,16 +143,12 @@ fi
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -209,12 +205,6 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
@@ -241,4 +231,4 @@ eval "set -- $(
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
exec "$JAVACMD" "$@"

17
android/gradlew.bat vendored
View File

@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,8 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -76,17 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
:omega

View File

@@ -5,6 +5,8 @@ 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')
includeBuild('../node_modules/react-native-gradle-plugin')

View File

@@ -4,7 +4,6 @@
import {Tutorial} from '@constants';
import {GLOBAL_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {getActiveServerUrl} from '@init/credentials';
import {logError} from '@utils/log';
export const storeGlobal = async (id: string, value: unknown, prepareRecordsOnly = false) => {
@@ -51,37 +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);
};
export const storePushDisabledInServerAcknowledged = async (serverUrl: string) => {
return storeGlobal(`${GLOBAL_IDENTIFIERS.PUSH_DISABLED_ACK}${serverUrl}`, 'true', false);
};
export const removePushDisabledInServerAcknowledged = async (serverUrl: string) => {
return storeGlobal(`${GLOBAL_IDENTIFIERS.PUSH_DISABLED_ACK}${serverUrl}`, null, false);
};

View File

@@ -1,69 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {handleConvertedGMCategories} from './category';
import type ServerDataOperator from '@database/operator/server_data_operator';
describe('handleConvertedGMCategories', () => {
const serverUrl = 'baseHandler.test.com';
const channelId = 'channel_id_1';
const teamId1 = 'team_id_1';
const teamId2 = 'team_id_2';
const team: Team = {
id: teamId1,
} as Team;
let operator: ServerDataOperator;
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
});
it('base case', async () => {
await operator.handleTeam({teams: [team], prepareRecordsOnly: false});
const defaultCategory: Category = {
id: 'default_category_id',
team_id: teamId1,
type: 'channels',
} as Category;
const customCategory: Category = {
id: 'custom_category_id',
team_id: teamId2,
type: 'custom',
} as Category;
const dmCategory: Category = {
id: 'dm_category_id',
team_id: teamId1,
type: 'direct_messages',
} as Category;
await operator.handleCategories({categories: [defaultCategory, customCategory, dmCategory], prepareRecordsOnly: false});
const dmCategoryChannel: CategoryChannel = {
id: 'dm_category_channel_id',
category_id: 'dm_category_id',
channel_id: channelId,
sort_order: 1,
};
const customCategoryChannel: CategoryChannel = {
id: 'custom_category_channel_id',
category_id: 'dm_category_id',
channel_id: channelId,
sort_order: 1,
};
await operator.handleCategoryChannels({categoryChannels: [dmCategoryChannel, customCategoryChannel], prepareRecordsOnly: false});
const {models, error} = await handleConvertedGMCategories(serverUrl, channelId, teamId1, true);
expect(error).toBeUndefined();
expect(models).toBeDefined();
expect(models!.length).toBe(3); // two for removing channel for a custom and a DM category, and one for adding it to default channels category
});
});

View File

@@ -3,13 +3,12 @@
import {CHANNELS_CATEGORY, DMS_CATEGORY} from '@constants/categories';
import DatabaseManager from '@database/manager';
import {prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById, prepareCategoriesAndCategoriesChannels, queryCategoryChannelsByChannelId} from '@queries/servers/categories';
import {prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById, prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
import {getCurrentUserId} from '@queries/servers/system';
import {queryMyTeams} from '@queries/servers/team';
import {isDMorGM} from '@utils/channel';
import {logDebug, logError} from '@utils/log';
import {logError} from '@utils/log';
import type {Database, Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
export const deleteCategory = async (serverUrl: string, categoryId: string) => {
@@ -92,8 +91,11 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
categoriesWithChannels.push(cwc);
}
} else {
const cwc = await prepareAddNonGMDMChannelToDefaultCategory(database, teamId, channel.id);
if (cwc) {
const categories = await queryCategoriesByTeamIds(database, [teamId]).fetch();
const channelCategory = categories.find((c) => c.type === CHANNELS_CATEGORY);
if (channelCategory) {
const cwc = await channelCategory.toCategoryWithChannels();
cwc.channel_ids.unshift(channel.id);
categoriesWithChannels.push(cwc);
}
}
@@ -106,62 +108,6 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
return {models};
} catch (error) {
logError('Failed to add channel to default category', error);
return {error};
}
}
async function prepareAddNonGMDMChannelToDefaultCategory(database: Database, teamId: string, channelId: string): Promise<CategoryWithChannels | undefined> {
const categories = await queryCategoriesByTeamIds(database, [teamId]).fetch();
const channelCategory = categories.find((category) => category.type === CHANNELS_CATEGORY);
if (channelCategory) {
const cwc = await channelCategory.toCategoryWithChannels();
if (cwc.channel_ids.indexOf(channelId) < 0) {
cwc.channel_ids.unshift(channelId);
return cwc;
}
}
return undefined;
}
export async function handleConvertedGMCategories(serverUrl: string, channelId: string, targetTeamID: string, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const categoryChannels = await queryCategoryChannelsByChannelId(database, channelId).fetch();
const categories = await queryCategoriesByTeamIds(database, [targetTeamID]).fetch();
const channelCategory = categories.find((category) => category.type === CHANNELS_CATEGORY);
if (!channelCategory) {
const error = 'Failed to find default category when handling category of converted GM';
logError(error);
return {error};
}
const models: Model[] = [];
categoryChannels.forEach((categoryChannel) => {
if (categoryChannel.categoryId !== channelCategory.id) {
models.push(categoryChannel.prepareDestroyPermanently());
}
});
const cwc = await prepareAddNonGMDMChannelToDefaultCategory(database, targetTeamID, channelId);
if (cwc) {
const model = await prepareCategoryChannels(operator, [cwc]);
models.push(...model);
} else {
logDebug('handleConvertedGMCategories: could not find channel category of target team');
}
if (models.length > 0 && !prepareRecordsOnly) {
await operator.batchRecords(models, 'putGMInCorrectCategory');
}
return {models};
} catch (error) {
logError('Failed to handle category update for GM converted to channel', error);
return {error};
}
}

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';
@@ -32,7 +32,7 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
let models: Model[] = [];
const dt = Date.now();
const isTabletDevice = isTablet();
const isTabletDevice = await isTablet();
const system = await getCommonSystemValues(database);
const member = await getMyChannel(database, channelId);

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

@@ -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';
@@ -43,7 +43,7 @@ export const switchToGlobalThreads = async (serverUrl: string, teamId?: string,
await operator.batchRecords(models, 'switchToGlobalThreads');
}
const isTabletDevice = isTablet();
const isTabletDevice = await isTablet();
if (isTabletDevice) {
DeviceEventEmitter.emit(Navigation.NAVIGATION_HOME, Screens.GLOBAL_THREADS);
} else {
@@ -75,27 +75,20 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
}
const currentTeamId = await getCurrentTeamId(database);
const isTabletDevice = isTablet();
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 {};
let switchingTeams = false;
if (currentTeamId === teamId) {
const models = await prepareCommonSystemValues(operator, {
currentChannelId: channel.id,
});
if (models.length) {
await operator.batchRecords(models, 'switchToThread');
}
await dismissAllModalsAndPopToRoot();
await NavigationStore.waitUntilScreenIsTop(Screens.HOME);
if (currentTeamId !== teamId && isTabletDevice) {
DeviceEventEmitter.emit(Navigation.NAVIGATION_HOME, Screens.GLOBAL_THREADS);
}
}
if (currentTeamId !== teamId) {
} else {
const modelPromises: Array<Promise<Model[]>> = [];
switchingTeams = true;
modelPromises.push(addTeamToTeamHistory(operator, teamId, true));
const commonValues: PrepareCommonSystemValuesArgs = {
currentChannelId: channel.id,
@@ -108,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);
@@ -123,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: {
@@ -135,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,14 +21,11 @@ export async function setCurrentUserStatus(serverUrl: string, status: string) {
throw new Error(`No current user for ${serverUrl}`);
}
if (user.status !== status) {
user.prepareStatus(status);
await operator.batchRecords([user], 'setCurrentUserStatus');
}
user.prepareStatus(General.OFFLINE);
await operator.batchRecords([user], 'setCurrentUserStatusOffline');
return null;
} catch (error) {
logError('Failed setCurrentUserStatus', error);
logError('Failed setCurrentUserStatusOffline', error);
return {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:
@@ -143,7 +139,7 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc
const match = matchDeepLink(location, serverUrl, config?.SiteURL);
if (match) {
handleDeepLink(match.url, intl, location);
handleDeepLink(match, intl, location);
} else {
const {formatMessage} = intl;
const onError = () => Alert.alert(
@@ -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,15 +1,15 @@
// 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 {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, fetchDataRetentionPolicy} from '@actions/remote/systems';
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, type MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
import {fetchConfigAndLicense} from '@actions/remote/systems';
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
import {syncTeamThreads} from '@actions/remote/thread';
import {fetchMe, type MyUserRequest, updateAllUsersSince, autoUpdateTimezone} from '@actions/remote/user';
import {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
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 +19,18 @@ import {selectDefaultTeam} from '@helpers/api/team';
import {DEFAULT_LOCALE} from '@i18n';
import NetworkManager from '@managers/network_manager';
import {getDeviceToken} from '@queries/app/global';
import {getChannelById, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
import {queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
import {getHasCRTChanged} from '@queries/servers/preference';
import {getConfig, getCurrentChannelId, getCurrentTeamId, getIsDataRetentionEnabled, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {getConfig, getCurrentChannelId, getCurrentTeamId, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
import NavigationStore from '@store/navigation_store';
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
import {getFullErrorMessage, isErrorWithStatusCode} from '@utils/errors';
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 = {
@@ -42,12 +42,10 @@ export type AppEntryData = {
removeTeamIds?: string[];
removeChannelIds?: string[];
isCRTEnabled: boolean;
initialChannelId?: string;
gmConverted: boolean;
}
export type AppEntryError = {
error: unknown;
error: Error | ClientError | string;
}
export type EntryResponse = {
@@ -58,7 +56,6 @@ export type EntryResponse = {
teamData: MyTeamsRequest;
chData?: MyChannelsRequest;
meData?: MyUserRequest;
gmConverted: boolean;
} | {
error: unknown;
}
@@ -66,43 +63,19 @@ export type EntryResponse = {
const FETCH_MISSING_DM_TIMEOUT = 2500;
export const FETCH_UNREADS_TIMEOUT = 2500;
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const result = entryRest(serverUrl, teamId, channelId, since);
// Fetch data retention policies
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
if (isDataRetentionEnabled) {
fetchDataRetentionPolicy(serverUrl);
}
return result;
};
export async function deferredAppEntryActions(
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) {
const result = restDeferredAppEntryActions(serverUrl, since, currentUserId, currentUserLocale, preferences, config, license, teamData, chData, initialTeamId, initialChannelId);
autoUpdateTimezone(serverUrl);
return result;
}
const getRemoveTeamIds = async (database: Database, teamData: MyTeamsRequest) => {
export const getRemoveTeamIds = async (database: Database, teamData: MyTeamsRequest) => {
const myTeams = await queryMyTeams(database).fetch();
const joinedTeams = new Set(teamData.memberships?.filter((m) => m.delete_at === 0).map((m) => m.team_id));
return myTeams.filter((m) => !joinedTeams.has(m.id)).map((m) => m.id);
};
const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[]) => {
export const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[]) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
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();
@@ -117,23 +90,24 @@ const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[]) => {
return [];
};
const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
export const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
const lastDisconnectedAt = since || await getWebSocketLastDisconnected(database);
const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, teamId, channelId);
const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, teamId);
if ('error' in fetchedData) {
return {error: fetchedData.error};
}
const {initialTeamId, initialChannelId: fetchedChannelId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds, isCRTEnabled, gmConverted} = fetchedData;
const chError = chData?.error;
if (isErrorWithStatusCode(chError) && chError.status_code === 403) {
const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds, isCRTEnabled} = fetchedData;
const chError = chData?.error as ClientError | undefined;
if (chError?.status_code === 403) {
// if the user does not have appropriate permissions, which means the user those not belong to the team,
// we set it as there is no errors, so that the teams and others can be properly handled
chData!.error = undefined;
@@ -145,7 +119,7 @@ const entryRest = async (serverUrl: string, teamId?: string, channelId?: string,
const rolesData = await fetchRoles(serverUrl, teamData.memberships, chData?.memberships, meData.user, true);
const initialChannelId = await entryInitialChannelId(database, fetchedChannelId, teamId, initialTeamId, meData?.user?.locale || '', chData?.channels, chData?.memberships);
const initialChannelId = await entryInitialChannelId(database, channelId, teamId, initialTeamId, meData?.user?.locale || '', chData?.channels, chData?.memberships);
const removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
@@ -161,10 +135,10 @@ const entryRest = async (serverUrl: string, teamId?: string, channelId?: string,
const models = await Promise.all(modelPromises);
return {models: models.flat(), initialChannelId, initialTeamId, prefData, teamData, chData, meData, gmConverted};
return {models: models.flat(), initialChannelId, initialTeamId, prefData, teamData, chData, meData};
};
const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeamId = '', channelId?: string): Promise<AppEntryData | AppEntryError> => {
export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, initialTeamId = ''): Promise<AppEntryData | AppEntryError> => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
@@ -191,45 +165,16 @@ const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeam
}
}
// Fetch in parallel teams / team membership / user preferences / user
const promises: [Promise<MyTeamsRequest>, Promise<MyUserRequest>] = [
// Fetch in parallel teams / team membership / channels for current team / user preferences / user
const promises: [Promise<MyTeamsRequest>, Promise<MyChannelsRequest | undefined>, Promise<MyUserRequest>] = [
fetchMyTeams(serverUrl, fetchOnly),
initialTeamId ? fetchMyChannelsForTeam(serverUrl, initialTeamId, includeDeletedChannels, since, fetchOnly, false, isCRTEnabled) : Promise.resolve(undefined),
fetchMe(serverUrl, fetchOnly),
];
const resolution = await Promise.all(promises);
const [teamData, meData] = resolution;
let chData;
let initialTeamId = onLoadTeamId;
let initialChannelId = channelId;
let gmConverted = false;
if (channelId) {
const existingChannel = await getChannelById(database, channelId);
if (existingChannel && existingChannel.type === General.GM_CHANNEL) {
// Okay, so now we know the channel existsin in mobile app's database as a GM.
// We now need to also check if channel on server is actually a private channel,
// and if so, which team does it belong to now. That team will become the
// active team on mobile app after this point.
const client = NetworkManager.getClient(serverUrl);
const serverChannel = await client.getChannel(channelId);
// Although yon can convert GM only to a pirvate channel, a private channel can furthur be converted to a public channel.
// So between the mobile app being on the GM and reconnecting,
// it may have become either a public or a private channel. So we need to check for both.
if (serverChannel.type === General.PRIVATE_CHANNEL || serverChannel.type === General.OPEN_CHANNEL) {
initialTeamId = serverChannel.team_id;
initialChannelId = channelId;
gmConverted = true;
}
}
}
if (initialTeamId) {
chData = await fetchMyChannelsForTeam(serverUrl, initialTeamId, includeDeletedChannels, since, fetchOnly, false, isCRTEnabled);
}
const [teamData, , meData] = resolution;
let [, chData] = resolution;
if (!initialTeamId && teamData.teams?.length && teamData.memberships?.length) {
// If no initial team was set in the database but got teams in the response
@@ -253,8 +198,6 @@ const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeam
meData,
removeTeamIds,
isCRTEnabled,
initialChannelId,
gmConverted,
};
if (teamData.teams?.length === 0 && !teamData.error) {
@@ -266,8 +209,8 @@ const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeam
}
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);
@@ -302,7 +245,7 @@ const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeam
return data;
};
const fetchAlternateTeamData = async (
export const fetchAlternateTeamData = async (
serverUrl: string, availableTeamIds: string[], removeTeamIds: string[],
includeDeleted = true, since = 0, fetchOnly = false, isCRTEnabled?: boolean) => {
let initialTeamId = '';
@@ -311,8 +254,8 @@ 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;
@@ -327,7 +270,7 @@ const fetchAlternateTeamData = async (
return {initialTeamId, removeTeamIds};
};
async function entryInitialChannelId(database: Database, requestedChannelId = '', requestedTeamId = '', initialTeamId: string, locale: string, channels?: Channel[], memberships?: ChannelMember[]) {
export async function entryInitialChannelId(database: Database, requestedChannelId = '', requestedTeamId = '', initialTeamId: string, locale: string, channels?: Channel[], memberships?: ChannelMember[]) {
const membershipIds = new Set(memberships?.map((m) => m.channel_id));
const requestedChannel = channels?.find((c) => (c.id === requestedChannelId) && membershipIds.has(c.id));
@@ -365,7 +308,7 @@ async function entryInitialChannelId(database: Database, requestedChannelId = ''
return myFirstTeamChannel?.id || '';
}
async function restDeferredAppEntryActions(
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) {
@@ -414,38 +357,50 @@ 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 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) {
@@ -457,9 +412,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
}
}
@@ -472,7 +425,6 @@ export async function handleEntryAfterLoadNavigation(
currentChannelId: string,
initialTeamId: string,
initialChannelId: string,
gmConverted: boolean,
) {
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@@ -482,7 +434,7 @@ export async function handleEntryAfterLoadNavigation(
const mountedScreens = NavigationStore.getScreensInStack();
const isChannelScreenMounted = mountedScreens.includes(Screens.CHANNEL);
const isThreadsMounted = mountedScreens.includes(Screens.THREAD);
const tabletDevice = isTablet();
const tabletDevice = await isTablet();
if (!currentTeamIdAfterLoad) {
// First load or no team
@@ -497,11 +449,7 @@ export async function handleEntryAfterLoadNavigation(
await handleKickFromTeam(serverUrl, currentTeamIdAfterLoad);
}
} else if (currentTeamIdAfterLoad !== initialTeamId) {
if (gmConverted) {
await setCurrentTeamAndChannelId(operator, initialTeamId, currentChannelId);
} else {
await handleKickFromTeam(serverUrl, currentTeamIdAfterLoad);
}
await handleKickFromTeam(serverUrl, currentTeamIdAfterLoad);
} else if (currentChannelIdAfterLoad !== currentChannelId) {
// Switched channels while loading
if (!channelMembers.find((m) => m.channel_id === currentChannelIdAfterLoad)) {

View File

@@ -0,0 +1,310 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
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 {MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
import {syncTeamThreads} from '@actions/remote/thread';
import {autoUpdateTimezone, fetchProfilesInGroupChannels, updateAllUsersSince} from '@actions/remote/user';
import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry';
import {General, Preferences} from '@constants';
import DatabaseManager from '@database/manager';
import {getPreferenceValue} from '@helpers/api/preference';
import {selectDefaultTeam} from '@helpers/api/team';
import {queryAllChannels, queryAllChannelsForTeam} from '@queries/servers/channel';
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
import {getHasCRTChanged} from '@queries/servers/preference';
import {getConfig, getIsDataRetentionEnabled} from '@queries/servers/system';
import {filterAndTransformRoles, getMemberChannelsFromGQLQuery, getMemberTeamsFromGQLQuery, gqlToClientChannelMembership, gqlToClientPreference, gqlToClientSidebarCategory, gqlToClientTeamMembership, gqlToClientUser} from '@utils/graphql';
import {logDebug} from '@utils/log';
import {processIsCRTEnabled} from '@utils/thread';
import {teamsToRemove, FETCH_UNREADS_TIMEOUT, entryRest, EntryResponse, entryInitialChannelId, restDeferredAppEntryActions, getRemoveTeamIds} from './common';
import type {MyChannelsRequest} from '@actions/remote/channel';
import type ClientError from '@client/rest/error';
import type {Database} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
const FETCH_MISSING_GM_TIMEOUT = 2500;
export async function deferredAppEntryGraphQLActions(
serverUrl: string,
since: number,
currentUserId: string,
teamData: MyTeamsRequest,
chData: MyChannelsRequest | undefined,
preferences: PreferenceType[] | undefined,
config: ClientConfig,
initialTeamId?: string,
initialChannelId?: string,
) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
setTimeout(() => {
if (chData?.channels?.length && chData.memberships?.length) {
// defer fetching posts for unread channels on initial team
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
}
}, FETCH_UNREADS_TIMEOUT);
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
if (initialTeamId) {
await syncTeamThreads(serverUrl, initialTeamId);
}
if (teamData.teams?.length) {
for await (const team of teamData.teams) {
if (team.id !== initialTeamId) {
// need to await here since GM/DM threads in different teams overlap
await syncTeamThreads(serverUrl, team.id);
}
}
}
}
if (initialTeamId) {
const result = await getChannelData(serverUrl, initialTeamId, currentUserId, true);
if ('error' in result) {
return result;
}
const removeChannels = await getRemoveChannels(database, result.chData, initialTeamId, false);
const modelPromises = await prepareModels({operator, removeChannels, chData: result.chData}, true);
const roles = filterAndTransformRoles(result.roles);
if (roles.length) {
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
}
const models = (await Promise.all(modelPromises)).flat();
operator.batchRecords(models, 'deferredAppEntryActions');
setTimeout(() => {
if (result.chData?.channels?.length && result.chData.memberships?.length) {
// defer fetching posts for unread channels on other teams
fetchPostsForUnreadChannels(serverUrl, result.chData.channels, result.chData.memberships, initialChannelId);
}
}, FETCH_UNREADS_TIMEOUT);
}
// Fetch groups for current user
fetchGroupsForMember(serverUrl, currentUserId);
updateCanJoinTeams(serverUrl);
updateAllUsersSince(serverUrl, since);
// defer sidebar GM profiles
setTimeout(async () => {
const gmIds = chData?.channels?.reduce<Set<string>>((acc, v) => {
if (v?.type === General.GM_CHANNEL) {
acc.add(v.id);
}
return acc;
}, new Set<string>());
if (gmIds?.size) {
fetchProfilesInGroupChannels(serverUrl, Array.from(gmIds));
}
}, FETCH_MISSING_GM_TIMEOUT);
return {error: undefined};
}
const getRemoveChannels = async (database: Database, chData: MyChannelsRequest | undefined, initialTeamId: string, singleTeam: boolean) => {
const removeChannels: ChannelModel[] = [];
if (chData?.channels) {
const fetchedChannelIds = chData.channels?.map((channel) => channel.id);
const query = singleTeam ? queryAllChannelsForTeam(database, initialTeamId) : queryAllChannels(database);
const channels = await query.fetch();
for (const channel of channels) {
const excludeCondition = singleTeam ? true : channel.teamId !== initialTeamId && channel.teamId !== '';
if (excludeCondition && !fetchedChannelIds?.includes(channel.id)) {
removeChannels.push(channel);
}
}
}
return removeChannels;
};
const getChannelData = async (serverUrl: string, initialTeamId: string, userId: string, exclude: boolean): Promise<{chData: MyChannelsRequest; roles: Array<Partial<GQLRole>|undefined>} | {error: unknown}> => {
let response;
try {
const request = exclude ? gqlOtherChannels : gqlEntryChannels;
response = await request(serverUrl, initialTeamId);
} catch (error) {
return {error: (error as ClientError).message};
}
if ('error' in response) {
return {error: response.error};
}
if ('errors' in response && response.errors?.length) {
return {error: response.errors[0].message};
}
const channelsFetchedData = response.data;
const chData = {
channels: getMemberChannelsFromGQLQuery(channelsFetchedData),
memberships: channelsFetchedData.channelMembers?.map((m) => gqlToClientChannelMembership(m, userId)),
categories: channelsFetchedData.sidebarCategories?.map((c) => gqlToClientSidebarCategory(c, '')),
};
const roles = channelsFetchedData.channelMembers?.map((m) => m.roles).flat() || [];
return {chData, roles};
};
export const entryGQL = async (serverUrl: string, currentTeamId?: string, currentChannelId?: string): Promise<EntryResponse> => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
let response;
try {
response = await gqlEntry(serverUrl);
} catch (error) {
return {error: (error as ClientError).message};
}
if ('error' in response) {
return {error: response.error};
}
if ('errors' in response && response.errors?.length) {
return {error: response.errors[0].message};
}
const fetchedData = response.data;
const config = fetchedData.config || {} as ClientConfig;
const license = fetchedData.license || {} as ClientLicense;
await storeConfigAndLicense(serverUrl, config, license);
const meData = {
user: gqlToClientUser(fetchedData.user!),
};
const allTeams = getMemberTeamsFromGQLQuery(fetchedData);
const allTeamMemberships = fetchedData.teamMembers.map((m) => gqlToClientTeamMembership(m, meData.user.id));
const [nonArchivedTeams, archivedTeamIds] = allTeams.reduce((acc, t) => {
if (t.delete_at) {
acc[1].add(t.id);
return acc;
}
return [[...acc[0], t], acc[1]];
}, [[], new Set<string>()]);
const nonArchivedTeamMemberships = allTeamMemberships.filter((m) => !archivedTeamIds.has(m.team_id));
const teamData = {
teams: nonArchivedTeams,
memberships: nonArchivedTeamMemberships,
};
const prefData = {
preferences: fetchedData.user?.preferences?.map(gqlToClientPreference),
};
if (prefData.preferences) {
const crtToggled = await getHasCRTChanged(database, prefData.preferences);
if (crtToggled) {
const {error} = await truncateCrtRelatedTables(serverUrl);
if (error) {
return {error: `Resetting CRT on ${serverUrl} failed`};
}
}
}
let initialTeamId = currentTeamId;
if (!teamData.teams.length) {
initialTeamId = '';
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId && t.delete_at === 0)) {
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
initialTeamId = selectDefaultTeam(teamData.teams, meData.user.locale, teamOrderPreference, config.ExperimentalPrimaryTeam)?.id || '';
}
const gqlRoles = [
...fetchedData.user?.roles || [],
...fetchedData.teamMembers?.map((m) => m.roles).flat() || [],
];
let chData;
if (initialTeamId) {
const result = await getChannelData(serverUrl, initialTeamId, meData.user.id, false);
if ('error' in result) {
return result;
}
chData = result.chData;
gqlRoles.push(...result.roles);
}
const roles = filterAndTransformRoles(gqlRoles);
const initialChannelId = await entryInitialChannelId(database, currentChannelId, currentTeamId, initialTeamId, meData.user.id, chData?.channels, chData?.memberships);
const removeChannels = await getRemoveChannels(database, chData, initialTeamId, true);
const removeTeamIds = await getRemoveTeamIds(database, teamData);
const removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData}, true);
if (roles.length) {
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
}
const models = (await Promise.all(modelPromises)).flat();
return {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData};
};
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const config = await getConfig(database);
let result;
if (config?.FeatureFlagGraphQL === 'true') {
result = await entryGQL(serverUrl, teamId, channelId);
if ('error' in result) {
logDebug('Error using GraphQL, trying REST', result.error);
result = entryRest(serverUrl, teamId, channelId, since);
}
} else {
result = entryRest(serverUrl, teamId, channelId, since);
}
// Fetch data retention policies
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
if (isDataRetentionEnabled) {
fetchDataRetentionPolicy(serverUrl);
}
return result;
};
export async function deferredAppEntryActions(
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) {
let result;
if (config?.FeatureFlagGraphQL === 'true') {
result = await deferredAppEntryGraphQLActions(serverUrl, since, currentUserId, teamData, chData, preferences, config, initialTeamId, initialChannelId);
if (result.error) {
logDebug('Error using GraphQL, trying REST', result.error);
result = restDeferredAppEntryActions(serverUrl, since, currentUserId, currentUserLocale, preferences, config, license, teamData, chData, initialTeamId, initialChannelId);
}
} else {
result = restDeferredAppEntryActions(serverUrl, since, currentUserId, currentUserLocale, preferences, config, license, teamData, chData, initialTeamId, initialChannelId);
}
autoUpdateTimezone(serverUrl);
return result;
}

View File

@@ -10,7 +10,7 @@ type AfterLoginArgs = {
serverUrl: string;
}
export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?: unknown}> {
export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?: any}> {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};

View File

@@ -15,25 +15,24 @@ import {getCurrentTeamId} from '@queries/servers/system';
import {getMyTeamById} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
import EphemeralStore from '@store/ephemeral_store';
import {isErrorWithStatusCode} from '@utils/errors';
import {emitNotificationError} from '@utils/notification';
import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme';
import type ClientError from '@client/rest/error';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
import type MyTeamModel from '@typings/database/models/servers/my_team';
import type PostModel from '@typings/database/models/servers/post';
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!;
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.channel_id!;
const rootId = notification.root_id!;
const {database} = operator;
const currentTeamId = await getCurrentTeamId(database);
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
@@ -66,7 +65,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
if (!myTeam) {
const resp = await fetchMyTeam(serverUrl, teamId);
if (resp.error) {
if (isErrorWithStatusCode(resp.error) && resp.error.status_code === 403) {
if ((resp.error as ClientError).status_code === 403) {
emitNotificationError('Team');
} else {
emitNotificationError('Connection');
@@ -79,7 +78,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
if (!myChannel) {
const resp = await fetchMyChannel(serverUrl, teamId, channelId);
if (resp.error) {
if (isErrorWithStatusCode(resp.error) && resp.error.status_code === 403) {
if ((resp.error as ClientError).status_code === 403) {
emitNotificationError('Channel');
} else {
emitNotificationError('Connection');

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

@@ -25,15 +25,19 @@ 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,7 +96,7 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati
}
return {};
} catch (error) {
forceLogoutIfNecessary(serverUrl, error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
@@ -180,19 +184,16 @@ export const backgroundNotification = async (serverUrl: string, notification: No
};
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 +232,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,14 +4,18 @@
/* 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';
import {getNeededAtMentionedUsernames} from '@helpers/api/user';
import {extractRecordsForTable} from '@helpers/database';
import NetworkManager from '@managers/network_manager';
import {getMyChannel, prepareMissingChannelsForAllTeams, queryAllMyChannel} from '@queries/servers/channel';
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
@@ -19,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';
@@ -57,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 {
@@ -66,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}`;
@@ -130,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,
@@ -201,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 {
@@ -210,6 +212,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
return {error};
}
const {database} = operator;
const isCRTEnabled = await getIsCRTEnabled(database);
try {
@@ -252,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 ||
@@ -272,7 +274,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
return {error};
}
return {};
return {error: undefined};
};
export const fetchPostsForCurrentChannel = async (serverUrl: string) => {
@@ -287,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;
@@ -308,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);
@@ -324,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,
});
@@ -376,34 +375,42 @@ export async function fetchPosts(serverUrl: string, channelId: string, page = 0,
models.push(...threadModels);
}
}
if (models.length) {
await operator.batchRecords(models, 'fetchPosts');
}
await operator.batchRecords(models, 'fetchPosts');
}
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({
@@ -432,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) {
@@ -481,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)));
@@ -548,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, {
@@ -598,20 +624,28 @@ export async function fetchPostThread(serverUrl: string, postId: string, options
await operator.batchRecords(models, 'fetchPostThread');
}
setFetchingThreadState(postId, false);
return {posts: result.posts};
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, {
@@ -666,20 +700,29 @@ export async function fetchPostsAround(serverUrl: string, channelId: string, pos
await operator.batchRecords(models, 'fetchPostsAround');
}
return {posts: data.posts};
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>> = [];
@@ -696,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);
@@ -719,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[] = [];
@@ -749,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) {
@@ -762,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;
@@ -787,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));
@@ -807,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);
@@ -841,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 || [];
@@ -910,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);
@@ -927,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));
}
@@ -947,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 || [];
@@ -971,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);
@@ -1020,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,60 +1,63 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {chunk} from 'lodash';
import {DeviceEventEmitter} from 'react-native';
import {handleReconnect} from '@actions/websocket';
import {Events, General, Preferences} from '@constants';
import DatabaseManager from '@database/manager';
import {getPreferenceAsBool} from '@helpers/api/preference';
import NetworkManager from '@managers/network_manager';
import {queryAllUnreadDMsAndGMsIds, getChannelById} from '@queries/servers/channel';
import {getChannelById} from '@queries/servers/channel';
import {truncateCrtRelatedTables} from '@queries/servers/entry';
import {queryPreferencesByCategoryAndName, querySavedPostsPreferences} from '@queries/servers/preference';
import {querySavedPostsPreferences} from '@queries/servers/preference';
import {getCurrentUserId} from '@queries/servers/system';
import EphemeralStore from '@store/ephemeral_store';
import {isDMorGM} from '@utils/channel';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {getUserIdFromChannelName} from '@utils/user';
import {forceLogoutIfNecessary} from './session';
import type ChannelModel from '@typings/database/models/servers/channel';
export type MyPreferencesRequest = {
preferences?: PreferenceType[];
error?: unknown;
};
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,
@@ -68,10 +71,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,
@@ -84,30 +90,30 @@ export const savePostPreference = async (serverUrl: string, postId: string) => {
}
};
export const savePreference = async (serverUrl: string, preferences: PreferenceType[], prepareRecordsOnly = false) => {
export const savePreference = async (serverUrl: string, preferences: PreferenceType[]) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
if (!preferences.length) {
return {preferences: []};
}
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const userId = await getCurrentUserId(database);
const chunkSize = 100;
const chunks = chunk(preferences, chunkSize);
chunks.forEach((c: PreferenceType[]) => {
client.savePreferences(userId, c);
});
const preferenceModels = await operator.handlePreferences({
preferences,
prepareRecordsOnly,
});
return {preferences: preferenceModels};
client = NetworkManager.getClient(serverUrl);
} catch (error) {
logDebug('error on savePreference', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
try {
const userId = await getCurrentUserId(operator.database);
client.savePreferences(userId, preferences);
await operator.handlePreferences({
preferences,
prepareRecordsOnly: false,
});
return {preferences};
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
@@ -135,79 +141,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 openChannelIfNeeded = async (serverUrl: string, channelId: string) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channel = await getChannelById(database, channelId);
if (!channel || !isDMorGM(channel)) {
return {};
}
const res = await openChannels(serverUrl, [channel]);
return res;
} catch (error) {
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
};
export const openAllUnreadChannels = async (serverUrl: string) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channels = await queryAllUnreadDMsAndGMsIds(database).fetch();
const res = await openChannels(serverUrl, channels);
return res;
} catch (error) {
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
};
const openChannels = async (serverUrl: string, channels: ChannelModel[]) => {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const userId = await getCurrentUserId(database);
const {DIRECT_CHANNEL_SHOW, GROUP_CHANNEL_SHOW} = Preferences.CATEGORIES;
const directChannelShowPreferences = await queryPreferencesByCategoryAndName(database, DIRECT_CHANNEL_SHOW).fetch();
const groupChannelShowPreferences = await queryPreferencesByCategoryAndName(database, GROUP_CHANNEL_SHOW).fetch();
const showPreferences = directChannelShowPreferences.concat(groupChannelShowPreferences);
const prefs: PreferenceType[] = [];
for (const channel of channels) {
const category = channel.type === General.DM_CHANNEL ? DIRECT_CHANNEL_SHOW : GROUP_CHANNEL_SHOW;
const name = channel.type === General.DM_CHANNEL ? getUserIdFromChannelName(userId, channel.name) : channel.id;
const visible = getPreferenceAsBool(showPreferences, category, name, false);
if (visible) {
continue;
}
prefs.push(
{
user_id: userId,
category,
name,
value: 'true',
},
{
user_id: userId,
category: Preferences.CATEGORIES.CHANNEL_OPEN_TIME,
name: channel.id,
value: Date.now().toString(),
},
);
}
return savePreference(serverUrl, prefs);
};
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);
@@ -223,9 +168,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};
}
};

View File

@@ -8,51 +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 getIsReactionAlreadyAddedToPost(serverUrl: string, postId: string, emojiName: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUserId = await getCurrentUserId(database);
const emojiAlias = getEmojiFirstAlias(emojiName);
return await queryReaction(database, emojiAlias, postId, currentUserId).fetchCount() > 0;
} catch (error) {
logDebug('error on getIsReactionAlreadyAddedToPost', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function toggleReaction(serverUrl: string, postId: string, emojiName: string) {
try {
const isReactionAlreadyAddedToPost = await getIsReactionAlreadyAddedToPost(serverUrl, postId, emojiName);
if (isReactionAlreadyAddedToPost) {
return removeReaction(serverUrl, postId, emojiName);
}
return addReaction(serverUrl, postId, emojiName);
} catch (error) {
logDebug('error on toggleReaction', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
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[] = [];
@@ -85,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);
@@ -111,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

@@ -1,12 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {General} from '@constants';
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';
@@ -20,36 +17,44 @@ 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);
});
}
const getRolesRequests = [];
for (let i = 0; i < newRoles.length; i += General.MAX_GET_ROLES_BY_NAMES) {
const chunk = newRoles.slice(i, i + General.MAX_GET_ROLES_BY_NAMES);
getRolesRequests.push(client.getRolesByNames(chunk));
}
if (!newRoles.length) {
return {roles: []};
}
const roles = (await Promise.all(getRolesRequests)).flat();
try {
const roles = await client.getRolesByNames(newRoles);
if (!fetchOnly) {
await operator.handleRole({
roles,
@@ -59,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

@@ -1,24 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getPosts} from '@actions/local/post';
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';
import type Model from '@nozbe/watermelondb/Model';
import type PostModel from '@typings/database/models/servers/post';
export async function fetchRecentMentions(serverUrl: string): Promise<PostSearchRequest> {
try {
@@ -54,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 || [];
@@ -73,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));
}
@@ -120,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};
}
};
@@ -134,31 +120,16 @@ export const searchFiles = async (serverUrl: string, teamId: string, params: Fil
const client = NetworkManager.getClient(serverUrl);
const result = await client.searchFiles(teamId, params.terms);
const files = result?.file_infos ? Object.values(result.file_infos) : [];
const [allChannelIds, allPostIds] = files.reduce<[Set<string>, Set<string>]>((acc, f) => {
const allChannelIds = files.reduce<string[]>((acc, f) => {
if (f.channel_id) {
acc[0].add(f.channel_id);
}
if (f.post_id) {
acc[1].add(f.post_id);
acc.push(f.channel_id);
}
return acc;
}, [new Set<string>(), new Set<string>()]);
const channels = Array.from(allChannelIds.values());
// Attach the file's post's props to the FileInfo (needed for captioning videos)
const postIds = Array.from(allPostIds);
const posts = await getPosts(serverUrl, postIds);
const idToPost = posts.reduce<Dictionary<PostModel>>((acc, p) => {
acc[p.id] = p;
return acc;
}, {});
files.forEach((f) => {
f.postProps = idToPost[f.post_id]?.props;
});
}, []);
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

@@ -14,13 +14,13 @@ import {getServerDisplayName} from '@queries/app/servers';
import {getCurrentUserId, getExpiredSession} 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 {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;
@@ -44,7 +44,7 @@ export const addPushProxyVerificationStateFromLogin = async (serverUrl: string)
logDebug('error setting the push proxy verification state on login', error);
}
};
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 +52,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 +69,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 +86,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 +123,16 @@ 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};
return {error: error as ClientError, failed: false};
} catch (error) {
return {error, failed: false};
return {error: error as ClientError, failed: false};
}
};
@@ -138,7 +143,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 +203,50 @@ 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 {
status: response.status,
error: undefined,
};
};
export const ssoLogin = async (serverUrl: string, serverDisplayName: string, serverIdentifier: string, bearerToken: string, csrfToken: string): Promise<LoginActionResponse> => {
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 +255,7 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
displayName: serverDisplayName,
},
});
const user = await client.getMe();
user = await client.getMe();
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
await server?.operator.handleSystem({
systems: [{
@@ -243,18 +264,17 @@ 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};
return {error: error as ClientError, failed: false};
} catch (error) {
return {error, failed: false};
return {error: error as ClientError, failed: false};
}
};

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 (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) {
@@ -395,7 +421,7 @@ export async function handleTeamChange(serverUrl: string, teamId: string) {
let channelId = '';
DeviceEventEmitter.emit(Events.TEAM_SWITCH, true);
if (isTablet()) {
if (await isTablet()) {
channelId = await getNthLastChannelFromTeam(database, teamId);
if (channelId) {
await switchToChannelById(serverUrl, channelId, 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);
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
@@ -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 {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 {getDeviceTimezone} from '@utils/timezone';
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[] = [];
@@ -294,19 +298,14 @@ const debouncedFetchUserOrGroupsByMentionNames = debounce(
},
);
const notFoundMentions: {[serverUrl: string]: Set<string>} = {};
const fetchUserOrGroupsByMentionNames = async (serverUrl: string, mentions: string[]) => {
try {
if (!notFoundMentions[serverUrl]) {
notFoundMentions[serverUrl] = new Set();
}
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
// Get any missing users
const usersInDb = await queryUsersByIdsOrUsernames(database, [], mentions).fetch();
const usersMap = new Set(usersInDb.map((u) => u.username));
const usernamesToFetch = mentions.filter((m) => !usersMap.has(m) && !notFoundMentions[serverUrl].has(m));
const usernamesToFetch = mentions.filter((m) => !usersMap.has(m));
let fetchedUsers;
if (usernamesToFetch.length) {
@@ -322,117 +321,81 @@ const fetchUserOrGroupsByMentionNames = async (serverUrl: string, mentions: stri
const groupsToFetch = groupsToCheck.filter((g) => !groupsMap.has(g));
if (groupsToFetch.length) {
const results = await fetchGroupsByNames(serverUrl, groupsToFetch, false);
if (!('error' in results)) {
const retrievedSet = new Set(results.map((r) => r.name));
for (const g of groupsToFetch) {
if (!retrievedSet.has(g)) {
notFoundMentions[serverUrl].add(g);
}
}
}
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;
}, {});
const usersToBatch = [];
for (const user of users) {
const receivedStatus = userStatuses[user.id];
const statusToSet = receivedStatus?.status || General.OFFLINE;
if (statusToSet !== user.status) {
user.prepareStatus(statusToSet);
usersToBatch.push(user);
for (const user of users) {
const status = userStatuses[user.id];
user.prepareStatus(status?.status || General.OFFLINE);
}
}
if (usersToBatch.length) {
await operator.batchRecords(usersToBatch, '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};
}
}
let usersByIdBatch: {
serverUrl: string;
userIds: Set<string>;
timeout?: NodeJS.Timeout;
} | undefined;
const TIME_TO_BATCH = 500;
const processBatch = () => {
if (!usersByIdBatch) {
return;
}
if (usersByIdBatch.timeout) {
clearTimeout(usersByIdBatch.timeout);
}
if (usersByIdBatch.userIds.size) {
fetchUsersByIds(usersByIdBatch.serverUrl, Array.from(usersByIdBatch.userIds));
}
usersByIdBatch = undefined;
};
export const fetchUserByIdBatched = async (serverUrl: string, userId: string) => {
if (serverUrl !== usersByIdBatch?.serverUrl) {
processBatch();
}
if (!usersByIdBatch) {
usersByIdBatch = {
serverUrl,
userIds: new Set(),
};
}
if (usersByIdBatch.timeout) {
clearTimeout(usersByIdBatch.timeout);
}
usersByIdBatch.userIds.add(userId);
usersByIdBatch.timeout = setTimeout(processBatch, TIME_TO_BATCH);
};
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!);
}
@@ -454,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;
@@ -490,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,
@@ -573,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));
@@ -602,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) {
@@ -631,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[] = [];
@@ -651,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
}
@@ -661,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();
@@ -678,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`;
@@ -769,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) => {
@@ -802,13 +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) {
return;
if (!currentUser || !config || !isTimezoneEnabled(config)) {
return null;
}
// Set timezone
@@ -821,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,
@@ -848,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};
}
};
@@ -861,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 [];
}
};

View File

@@ -78,7 +78,13 @@ export async function handleCategoryDeletedEvent(serverUrl: string, msg: Websock
export async function handleCategoryOrderUpdatedEvent(serverUrl: string, msg: WebsocketCategoriesMessage) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
const {database} = operator;
// Update category order
if (msg.data.order?.length) {

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {addChannelToDefaultCategory, handleConvertedGMCategories} from '@actions/local/category';
import {addChannelToDefaultCategory} from '@actions/local/category';
import {
markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt,
storeMyChannelsForTeam, updateChannelInfoFromChannel, updateMyChannelFromWebsocket,
@@ -11,30 +11,31 @@ import {fetchMissingDirectChannelsInfo, fetchMyChannel, fetchChannelStats, fetch
import {fetchPostsForChannel} from '@actions/remote/post';
import {fetchRolesIfNeeded} from '@actions/remote/role';
import {fetchUsersByIds, updateUsersNoLongerVisible} from '@actions/remote/user';
import {loadCallForChannel, leaveCall} from '@calls/actions/calls';
import {userLeftChannelErr, userRemovedFromChannelErr} from '@calls/errors';
import {getCurrentCall} from '@calls/state';
import {Events, General} from '@constants';
import {loadCallForChannel} from '@calls/actions/calls';
import {Events} from '@constants';
import DatabaseManager from '@database/manager';
import {deleteChannelMembership, getChannelById, prepareMyChannelsForTeam, getCurrentChannel} from '@queries/servers/channel';
import {getConfig, getCurrentChannelId, getCurrentTeamId, setCurrentTeamId} from '@queries/servers/system';
import {getConfig, getCurrentChannelId} from '@queries/servers/system';
import {getCurrentUser, getTeammateNameDisplay, getUserById} from '@queries/servers/user';
import EphemeralStore from '@store/ephemeral_store';
import MyChannelModel from '@typings/database/models/servers/my_channel';
import {logDebug} from '@utils/log';
import type {Model} from '@nozbe/watermelondb';
// Received when current user created a channel in a different client
export async function handleChannelCreatedEvent(serverUrl: string, msg: any) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
const {database} = operator;
const {team_id: teamId, channel_id: channelId} = msg.data;
if (EphemeralStore.creatingChannel) {
return; // We probably don't need to handle this WS because we provoked it
}
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channel = await getChannelById(database, channelId);
if (channel) {
return; // We already have this channel
@@ -75,9 +76,12 @@ export async function handleChannelUnarchiveEvent(serverUrl: string, msg: any) {
}
export async function handleChannelConvertedEvent(serverUrl: string, msg: any) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
try {
const channelId = msg.data.channel_id;
if (EphemeralStore.isConvertingChannel(channelId)) {
return;
@@ -93,37 +97,19 @@ export async function handleChannelConvertedEvent(serverUrl: string, msg: any) {
}
export async function handleChannelUpdatedEvent(serverUrl: string, msg: any) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const updatedChannel = JSON.parse(msg.data.channel) as Channel;
if (EphemeralStore.isConvertingChannel(updatedChannel.id)) {
return;
}
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const existingChannel = await getChannelById(database, updatedChannel.id);
const existingChannelType = existingChannel?.type;
const updatedChannel = JSON.parse(msg.data.channel);
const models: Model[] = await operator.handleChannel({channels: [updatedChannel], prepareRecordsOnly: true});
const infoModel = await updateChannelInfoFromChannel(serverUrl, updatedChannel, true);
if (infoModel.model) {
models.push(...infoModel.model);
}
operator.batchRecords(models, 'handleChannelUpdatedEvent');
// This indicates a GM was converted to a private channel
if (existingChannelType === General.GM_CHANNEL && updatedChannel.type === General.PRIVATE_CHANNEL) {
await handleConvertedGMCategories(serverUrl, updatedChannel.id, updatedChannel.team_id);
const currentChannelId = await getCurrentChannelId(database);
const currentTeamId = await getCurrentTeamId(database);
// Making sure user is in the correct team
if (currentChannelId === updatedChannel.id && currentTeamId !== updatedChannel.team_id) {
await setCurrentTeamId(operator, updatedChannel.team_id);
}
}
} catch {
// Do nothing
}
@@ -131,7 +117,10 @@ export async function handleChannelUpdatedEvent(serverUrl: string, msg: any) {
export async function handleChannelViewedEvent(serverUrl: string, msg: any) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return;
}
const {channel_id: channelId} = msg.data;
@@ -146,48 +135,14 @@ export async function handleChannelViewedEvent(serverUrl: string, msg: any) {
}
}
export async function handleMultipleChannelsViewedEvent(serverUrl: string, msg: any) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {channel_times: channelTimes} = msg.data;
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
const currentChannelId = await getCurrentChannelId(database);
const promises: Array<ReturnType<typeof markChannelAsViewed>> = [];
for (const id of Object.keys(channelTimes)) {
if (activeServerUrl === serverUrl && (currentChannelId === id || EphemeralStore.isSwitchingToChannel(id))) {
continue;
}
promises.push(markChannelAsViewed(serverUrl, id, false, true));
}
const members = (await Promise.allSettled(promises)).reduce<MyChannelModel[]>((acum, v) => {
if (v.status === 'rejected') {
return acum;
}
const value = v.value;
if (value.member) {
acum.push(value.member);
}
return acum;
}, []);
if (members.length) {
operator.batchRecords(members, 'handleMultipleCahnnelViewedEvent');
}
} catch {
// do nothing
}
}
// This event is triggered by changes in the notify props or in the roles.
export async function handleChannelMemberUpdatedEvent(serverUrl: string, msg: any) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
try {
const models: Model[] = [];
const updatedChannelMember: ChannelMembership = JSON.parse(msg.data.channelMember);
@@ -217,6 +172,13 @@ export async function handleChannelMemberUpdatedEvent(serverUrl: string, msg: an
}
export async function handleDirectAddedEvent(serverUrl: string, msg: WebSocketMessage) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
const {database} = operator;
if (EphemeralStore.creatingDMorGMTeammates.length) {
let userList: string[] | undefined;
if ('teammate_ids' in msg.data) { // GM
@@ -237,8 +199,6 @@ export async function handleDirectAddedEvent(serverUrl: string, msg: WebSocketMe
}
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {channel_id: channelId} = msg.broadcast;
const channel = await getChannelById(database, channelId);
if (channel) {
@@ -282,13 +242,17 @@ export async function handleDirectAddedEvent(serverUrl: string, msg: WebSocketMe
}
export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any) {
const userId = msg.data.user_id || msg.broadcast.userId;
const channelId = msg.data.channel_id || msg.broadcast.channel_id;
const {team_id: teamId} = msg.data;
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {database} = operator;
const currentUser = await getCurrentUser(database);
const userId = msg.data.user_id || msg.broadcast.userId;
const channelId = msg.data.channel_id || msg.broadcast.channel_id;
const {team_id: teamId} = msg.data;
const models: Model[] = [];
if (userId === currentUser?.id) {
@@ -362,9 +326,6 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
const channelId = msg.data.channel_id || msg.broadcast.channel_id;
if (EphemeralStore.isLeavingChannel(channelId)) {
if (getCurrentCall()?.channelId === channelId) {
leaveCall(userLeftChannelErr);
}
return;
}
@@ -387,12 +348,7 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
if (currentChannelId && currentChannelId === channelId) {
await handleKickFromChannel(serverUrl, currentChannelId);
}
await removeCurrentUserFromChannel(serverUrl, channelId);
if (getCurrentCall()?.channelId === channelId) {
leaveCall(userRemovedFromChannelErr);
}
} else {
const {models: deleteMemberModels} = await deleteChannelMembership(operator, userId, channelId, true);
if (deleteMemberModels) {
@@ -407,26 +363,32 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
}
export async function handleChannelDeletedEvent(serverUrl: string, msg: WebSocketMessage) {
const {channel_id: channelId, delete_at: deleteAt} = msg.data;
if (EphemeralStore.isLeavingChannel(channelId) || EphemeralStore.isArchivingChannel(channelId)) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
try {
const {database} = operator;
const {channel_id: channelId, delete_at: deleteAt} = msg.data;
if (EphemeralStore.isLeavingChannel(channelId) || EphemeralStore.isArchivingChannel(channelId)) {
return;
}
const currentChannel = await getCurrentChannel(database);
const user = await getCurrentUser(database);
if (!user) {
return;
}
const config = await getConfig(database);
await setChannelDeleteAt(serverUrl, channelId, deleteAt);
if (user.isGuest) {
updateUsersNoLongerVisible(serverUrl);
}
const currentChannel = await getCurrentChannel(database);
const config = await getConfig(database);
if (config?.ExperimentalViewArchivedChannels !== 'true') {
if (currentChannel && currentChannel.id === channelId) {
await handleKickFromChannel(serverUrl, channelId, Events.CHANNEL_ARCHIVED);

View File

@@ -4,14 +4,9 @@
import {markChannelAsViewed} from '@actions/local/channel';
import {dataRetentionCleanup} from '@actions/local/systems';
import {markChannelAsRead} from '@actions/remote/channel';
import {
deferredAppEntryActions,
entry,
handleEntryAfterLoadNavigation,
registerDeviceToken,
} from '@actions/remote/entry/common';
import {handleEntryAfterLoadNavigation, registerDeviceToken} from '@actions/remote/entry/common';
import {deferredAppEntryActions, entry} from '@actions/remote/entry/gql_common';
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
import {openAllUnreadChannels} from '@actions/remote/preference';
import {autoUpdateTimezone} from '@actions/remote/user';
import {loadConfigAndCalls} from '@calls/actions/calls';
import {
@@ -22,9 +17,9 @@ import {
handleCallRecordingState,
handleCallScreenOff,
handleCallScreenOn,
handleCallStarted, handleCallUserConnected, handleCallUserDisconnected,
handleCallUserJoined,
handleCallUserLeft,
handleCallStarted,
handleCallUserConnected,
handleCallUserDisconnected,
handleCallUserMuted,
handleCallUserRaiseHand,
handleCallUserReacted,
@@ -32,7 +27,6 @@ import {
handleCallUserUnraiseHand,
handleCallUserVoiceOff,
handleCallUserVoiceOn,
handleUserDismissedNotification,
} from '@calls/connection/websocket_event_handlers';
import {isSupportedServerCalls} from '@calls/utils';
import {Screens, WebsocketEvents} from '@constants';
@@ -56,57 +50,26 @@ import {setTeamLoading} from '@store/team_load_store';
import {isTablet} from '@utils/helpers';
import {logDebug, logInfo} from '@utils/log';
import {
handleCategoryCreatedEvent,
handleCategoryDeletedEvent,
handleCategoryOrderUpdatedEvent,
handleCategoryUpdatedEvent,
} from './category';
import {
handleChannelConvertedEvent, handleChannelCreatedEvent,
import {handleCategoryCreatedEvent, handleCategoryDeletedEvent, handleCategoryOrderUpdatedEvent, handleCategoryUpdatedEvent} from './category';
import {handleChannelConvertedEvent, handleChannelCreatedEvent,
handleChannelDeletedEvent,
handleChannelMemberUpdatedEvent,
handleChannelUnarchiveEvent,
handleChannelUpdatedEvent,
handleChannelViewedEvent,
handleMultipleChannelsViewedEvent,
handleDirectAddedEvent,
handleUserAddedToChannelEvent,
handleUserRemovedFromChannelEvent,
} from './channel';
import {
handleGroupMemberAddEvent,
handleGroupMemberDeleteEvent,
handleGroupReceivedEvent,
handleGroupTeamAssociatedEvent,
handleGroupTeamDissociateEvent,
} from './group';
handleUserRemovedFromChannelEvent} from './channel';
import {handleGroupMemberAddEvent, handleGroupMemberDeleteEvent, handleGroupReceivedEvent, handleGroupTeamAssociatedEvent, handleGroupTeamDissociateEvent} from './group';
import {handleOpenDialogEvent} from './integrations';
import {
handleNewPostEvent,
handlePostAcknowledgementAdded,
handlePostAcknowledgementRemoved,
handlePostDeleted,
handlePostEdited,
handlePostUnread,
} from './posts';
import {
handlePreferenceChangedEvent,
handlePreferencesChangedEvent,
handlePreferencesDeletedEvent,
} from './preferences';
import {handleNewPostEvent, handlePostDeleted, handlePostEdited, handlePostUnread} from './posts';
import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences';
import {handleAddCustomEmoji, handleReactionRemovedFromPostEvent, handleReactionAddedToPostEvent} from './reactions';
import {handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent, handleRoleUpdatedEvent} from './roles';
import {handleLicenseChangedEvent, handleConfigChangedEvent} from './system';
import {
handleLeaveTeamEvent,
handleUserAddedToTeamEvent,
handleUpdateTeamEvent,
handleTeamArchived,
handleTeamRestored,
} from './teams';
import {handleLeaveTeamEvent, handleUserAddedToTeamEvent, handleUpdateTeamEvent, handleTeamArchived, handleTeamRestored} from './teams';
import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads';
import {handleUserUpdatedEvent, handleUserTypingEvent, handleStatusChangedEvent} from './users';
import {handleUserUpdatedEvent, handleUserTypingEvent} from './users';
export async function handleFirstConnect(serverUrl: string) {
registerDeviceToken(serverUrl);
@@ -159,16 +122,16 @@ async function doReconnect(serverUrl: string) {
setTeamLoading(serverUrl, false);
return entryData.error;
}
const {models, initialTeamId, initialChannelId, prefData, teamData, chData, gmConverted} = entryData;
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId || '', currentChannelId || '', initialTeamId, initialChannelId, gmConverted);
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId || '', currentChannelId || '', initialTeamId, initialChannelId);
const dt = Date.now();
if (models?.length) {
await operator.batchRecords(models, 'doReconnect');
}
const tabletDevice = isTablet();
const tabletDevice = await isTablet();
if (tabletDevice && initialChannelId === currentChannelId) {
await markChannelAsRead(serverUrl, initialChannelId);
markChannelAsViewed(serverUrl, initialChannelId);
@@ -189,8 +152,6 @@ async function doReconnect(serverUrl: string) {
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
openAllUnreadChannels(serverUrl);
dataRetentionCleanup(serverUrl);
AppsManager.refreshAppBindings(serverUrl);
@@ -216,13 +177,6 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
handlePostUnread(serverUrl, msg);
break;
case WebsocketEvents.POST_ACKNOWLEDGEMENT_ADDED:
handlePostAcknowledgementAdded(serverUrl, msg);
break;
case WebsocketEvents.POST_ACKNOWLEDGEMENT_REMOVED:
handlePostAcknowledgementRemoved(serverUrl, msg);
break;
case WebsocketEvents.LEAVE_TEAM:
handleLeaveTeamEvent(serverUrl, msg);
break;
@@ -290,10 +244,6 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
handleChannelViewedEvent(serverUrl, msg);
break;
case WebsocketEvents.MULTIPLE_CHANNELS_VIEWED:
handleMultipleChannelsViewedEvent(serverUrl, msg);
break;
case WebsocketEvents.CHANNEL_MEMBER_UPDATED:
handleChannelMemberUpdatedEvent(serverUrl, msg);
break;
@@ -320,8 +270,9 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
break;
case WebsocketEvents.STATUS_CHANGED:
handleStatusChangedEvent(serverUrl, msg);
break;
// return dispatch(handleStatusChangedEvent(msg));
case WebsocketEvents.TYPING:
handleUserTypingEvent(serverUrl, msg);
break;
@@ -382,23 +333,12 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
case WebsocketEvents.CALLS_CHANNEL_DISABLED:
handleCallChannelDisabled(serverUrl, msg);
break;
// DEPRECATED in favour of user_joined (since v0.21.0)
case WebsocketEvents.CALLS_USER_CONNECTED:
handleCallUserConnected(serverUrl, msg);
break;
// DEPRECATED in favour of user_left (since v0.21.0)
case WebsocketEvents.CALLS_USER_DISCONNECTED:
handleCallUserDisconnected(serverUrl, msg);
break;
case WebsocketEvents.CALLS_USER_JOINED:
handleCallUserJoined(serverUrl, msg);
break;
case WebsocketEvents.CALLS_USER_LEFT:
handleCallUserLeft(serverUrl, msg);
break;
case WebsocketEvents.CALLS_USER_MUTED:
handleCallUserMuted(serverUrl, msg);
break;
@@ -438,9 +378,6 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
case WebsocketEvents.CALLS_HOST_CHANGED:
handleCallHostChanged(serverUrl, msg);
break;
case WebsocketEvents.CALLS_USER_DISMISSED_NOTIFICATION:
handleUserDismissedNotification(serverUrl, msg);
break;
case WebsocketEvents.GROUP_RECEIVED:
handleGroupReceivedEvent(serverUrl, msg);
@@ -479,7 +416,7 @@ async function fetchPostDataIfNeeded(serverUrl: string) {
const mountedScreens = NavigationStore.getScreensInStack();
const isChannelScreenMounted = mountedScreens.includes(Screens.CHANNEL);
const isThreadScreenMounted = mountedScreens.includes(Screens.THREAD);
const tabletDevice = isTablet();
const tabletDevice = await isTablet();
if (isCRTEnabled && isThreadScreenMounted) {
// Fetch new posts in the thread only when CRT is enabled,

View File

@@ -4,20 +4,17 @@
import {DeviceEventEmitter} from 'react-native';
import {storeMyChannelsForTeam, markChannelAsUnread, markChannelAsViewed, updateLastPostAt} from '@actions/local/channel';
import {addPostAcknowledgement, markPostAsDeleted, removePostAcknowledgement} from '@actions/local/post';
import {markPostAsDeleted} from '@actions/local/post';
import {createThreadFromNewPost, updateThread} from '@actions/local/thread';
import {fetchChannelStats, fetchMyChannel} from '@actions/remote/channel';
import {fetchPostAuthors, fetchPostById} from '@actions/remote/post';
import {openChannelIfNeeded} from '@actions/remote/preference';
import {fetchThread} from '@actions/remote/thread';
import {fetchMissingProfilesByIds} from '@actions/remote/user';
import {ActionType, Events, Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {getChannelById, getMyChannel} from '@queries/servers/channel';
import {getPostById} from '@queries/servers/post';
import {getCurrentChannelId, getCurrentTeamId, getCurrentUserId} from '@queries/servers/system';
import {getIsCRTEnabled} from '@queries/servers/thread';
import EphemeralStore from '@store/ephemeral_store';
import NavigationStore from '@store/navigation_store';
import {isTablet} from '@utils/helpers';
import {isFromWebhook, isSystemMessage, shouldIgnorePost} from '@utils/post';
@@ -47,7 +44,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
}
const currentUserId = await getCurrentUserId(database);
const existing = await getPostById(database, post.pending_post_id) || await getPostById(database, post.id);
const existing = await getPostById(database, post.pending_post_id);
if (existing) {
return;
@@ -130,7 +127,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
} else if ((post.channel_id === currentChannelId)) {
const isChannelScreenMounted = NavigationStore.getScreensInStack().includes(Screens.CHANNEL);
const isTabletDevice = isTablet();
const isTabletDevice = await isTablet();
if (isChannelScreenMounted || isTabletDevice) {
markAsViewed = false;
}
@@ -158,8 +155,6 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
models.push(unreadAt);
}
}
openChannelIfNeeded(serverUrl, post.channel_id);
}
let actionType: string = ActionType.POSTS.RECEIVED_NEW;
@@ -167,20 +162,6 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
actionType = ActionType.POSTS.RECEIVED_IN_THREAD;
}
const outOfOrderWebsocketEvent = EphemeralStore.getLastPostWebsocketEvent(serverUrl, post.id);
if (outOfOrderWebsocketEvent?.deleted) {
for (const model of models) {
if (model._preparedState === 'update') {
model.cancelPrepareUpdate();
}
}
return;
}
if (outOfOrderWebsocketEvent?.post) {
post = outOfOrderWebsocketEvent.post;
}
const postModels = await operator.handlePosts({
actionType,
order: [post.id],
@@ -198,7 +179,6 @@ export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage)
if (!operator) {
return;
}
const {database} = operator;
let post: Post;
try {
@@ -208,14 +188,10 @@ export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage)
}
const models: Model[] = [];
const {database} = operator;
const oldPost = await getPostById(database, post.id);
if (!oldPost) {
EphemeralStore.addEditingPost(serverUrl, post);
return;
}
if (oldPost.isPinned !== post.is_pinned) {
if (oldPost && oldPost.isPinned !== post.is_pinned) {
fetchChannelStats(serverUrl, post.channel_id);
}
@@ -226,7 +202,7 @@ export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage)
}
let actionType: string = ActionType.POSTS.RECEIVED_NEW;
const isCRTEnabled = await getIsCRTEnabled(database);
const isCRTEnabled = await getIsCRTEnabled(operator.database);
if (isCRTEnabled && post.root_id) {
actionType = ActionType.POSTS.RECEIVED_IN_THREAD;
}
@@ -243,17 +219,15 @@ export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage)
}
export async function handlePostDeleted(serverUrl: string, msg: WebSocketMessage) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {database} = operator;
const post: Post = JSON.parse(msg.data.post);
const oldPost = await getPostById(database, post.id);
if (!oldPost) {
EphemeralStore.addRemovingPost(serverUrl, post.id);
return;
}
const models: Model[] = [];
const {model: deleteModel} = await markPostAsDeleted(serverUrl, post, true);
@@ -326,41 +300,3 @@ export async function handlePostUnread(serverUrl: string, msg: WebSocketMessage)
markChannelAsUnread(serverUrl, channelId, delta, mentions, lastViewedAt);
}
}
export async function handlePostAcknowledgementAdded(serverUrl: string, msg: WebSocketMessage) {
try {
const acknowledgement = JSON.parse(msg.data.acknowledgement);
const {user_id, post_id, acknowledged_at} = acknowledgement;
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return;
}
const currentUserId = getCurrentUserId(database);
if (EphemeralStore.isAcknowledgingPost(post_id) && currentUserId === user_id) {
return;
}
addPostAcknowledgement(serverUrl, post_id, user_id, acknowledged_at);
fetchMissingProfilesByIds(serverUrl, [user_id]);
} catch (error) {
// Do nothing
}
}
export async function handlePostAcknowledgementRemoved(serverUrl: string, msg: WebSocketMessage) {
try {
const acknowledgement = JSON.parse(msg.data.acknowledgement);
const {user_id, post_id} = acknowledgement;
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return;
}
const currentUserId = getCurrentUserId(database);
if (EphemeralStore.isUnacknowledgingPost(post_id) && currentUserId === user_id) {
return;
}
await removePostAcknowledgement(serverUrl, post_id, user_id);
} catch (error) {
// Do nothing
}
}

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