Compare commits
13 Commits
v2.0.0
...
voice-mess
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c9e9357a5 | ||
|
|
335b8f2925 | ||
|
|
bd9bb48384 | ||
|
|
04030a93f7 | ||
|
|
56987dcf37 | ||
|
|
494df6e0b1 | ||
|
|
c7040707f4 | ||
|
|
d6eb40776f | ||
|
|
375412833d | ||
|
|
7d2f23b5f4 | ||
|
|
7fbc59b322 | ||
|
|
92c18c4893 | ||
|
|
9854683321 |
@@ -1,20 +1,20 @@
|
||||
version: 2.1
|
||||
orbs:
|
||||
owasp: entur/owasp@0.0.10
|
||||
node: circleci/node@5.0.3
|
||||
node: circleci/node@5.0.2
|
||||
|
||||
executors:
|
||||
android:
|
||||
parameters:
|
||||
resource_class:
|
||||
default: xlarge
|
||||
default: large
|
||||
type: string
|
||||
environment:
|
||||
NODE_OPTIONS: --max_old_space_size=12000
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
docker:
|
||||
- image: cimg/android:2022.09.2-node
|
||||
- image: cimg/android:2022.03-node
|
||||
working_directory: ~/mattermost-mobile
|
||||
resource_class: <<parameters.resource_class>>
|
||||
|
||||
@@ -39,7 +39,7 @@ commands:
|
||||
steps:
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "03:1c:a7:07:35:bc:57:e4:1d:6c:e1:2c:4b:be:09:6d"
|
||||
- "59:4d:99:5e:1c:6d:30:36:6d:60:76:88:ff:a7:ab:63"
|
||||
- run:
|
||||
name: Clone the mobile private repo
|
||||
command: git clone git@github.com:mattermost/mattermost-mobile-private.git ~/mattermost-mobile-private
|
||||
@@ -105,15 +105,13 @@ commands:
|
||||
description: "Get JavaScript dependencies"
|
||||
steps:
|
||||
- node/install:
|
||||
node-version: '18.7.0'
|
||||
node-version: '16.14.2'
|
||||
- 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
|
||||
command: NODE_ENV=development npm ci --ignore-scripts
|
||||
- save_cache:
|
||||
name: Save npm cache
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
@@ -309,13 +307,13 @@ jobs:
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
|
||||
build-android-release:
|
||||
executor: android
|
||||
steps:
|
||||
- build-android
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
# build-android-release:
|
||||
# executor: android
|
||||
# steps:
|
||||
# - build-android
|
||||
# - persist
|
||||
# - save:
|
||||
# filename: "*.apk"
|
||||
|
||||
build-android-pr:
|
||||
executor: android
|
||||
@@ -326,27 +324,27 @@ jobs:
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
|
||||
build-android-unsigned:
|
||||
executor: android
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: android
|
||||
- gradle-dependencies
|
||||
- run:
|
||||
name: Jetify Android libraries
|
||||
command: ./node_modules/.bin/jetify
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build unsigned android
|
||||
no_output_timeout: 30m
|
||||
command: bundle exec fastlane android unsigned
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
# build-android-unsigned:
|
||||
# executor: android
|
||||
# steps:
|
||||
# - checkout:
|
||||
# path: ~/mattermost-mobile
|
||||
# - npm-dependencies
|
||||
# - assets
|
||||
# - fastlane-dependencies:
|
||||
# for: android
|
||||
# - gradle-dependencies
|
||||
# - run:
|
||||
# name: Jetify Android libraries
|
||||
# command: ./node_modules/.bin/jetify
|
||||
# - run:
|
||||
# working_directory: fastlane
|
||||
# name: Run fastlane to build unsigned android
|
||||
# no_output_timeout: 30m
|
||||
# command: bundle exec fastlane android unsigned
|
||||
# - persist
|
||||
# - save:
|
||||
# filename: "*.apk"
|
||||
|
||||
build-ios-beta:
|
||||
executor:
|
||||
@@ -358,13 +356,13 @@ jobs:
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
|
||||
build-ios-release:
|
||||
executor: ios
|
||||
steps:
|
||||
- build-ios
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
# build-ios-release:
|
||||
# executor: ios
|
||||
# steps:
|
||||
# - build-ios
|
||||
# - persist
|
||||
# - save:
|
||||
# filename: "*.ipa"
|
||||
|
||||
build-ios-pr:
|
||||
executor: ios
|
||||
@@ -375,64 +373,63 @@ jobs:
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
|
||||
build-ios-unsigned:
|
||||
executor: ios
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- pods-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: ios
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build unsigned iOS
|
||||
no_output_timeout: 30m
|
||||
command: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
bundle exec fastlane ios unsigned
|
||||
- persist_to_workspace:
|
||||
root: ~/
|
||||
paths:
|
||||
- mattermost-mobile/*.ipa
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
# build-ios-unsigned:
|
||||
# executor: ios
|
||||
# steps:
|
||||
# - checkout:
|
||||
# path: ~/mattermost-mobile
|
||||
# - npm-dependencies
|
||||
# - pods-dependencies
|
||||
# - assets
|
||||
# - fastlane-dependencies:
|
||||
# for: ios
|
||||
# - run:
|
||||
# working_directory: fastlane
|
||||
# name: Run fastlane to build unsigned iOS
|
||||
# no_output_timeout: 30m
|
||||
# command: |
|
||||
# HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
# bundle exec fastlane ios unsigned
|
||||
# - persist_to_workspace:
|
||||
# root: ~/
|
||||
# paths:
|
||||
# - mattermost-mobile/*.ipa
|
||||
# - save:
|
||||
# filename: "*.ipa"
|
||||
|
||||
build-ios-simulator:
|
||||
executor: ios
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- pods-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: ios
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build unsigned x86_64 iOS app for iPhone simulator
|
||||
no_output_timeout: 30m
|
||||
command: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
bundle exec fastlane ios simulator
|
||||
- persist_to_workspace:
|
||||
root: ~/
|
||||
paths:
|
||||
- mattermost-mobile/Mattermost-simulator-x86_64.app.zip
|
||||
- save:
|
||||
filename: "Mattermost-simulator-x86_64.app.zip"
|
||||
# build-ios-simulator:
|
||||
# executor: ios
|
||||
# steps:
|
||||
# - checkout:
|
||||
# path: ~/mattermost-mobile
|
||||
# - npm-dependencies
|
||||
# - pods-dependencies
|
||||
# - assets
|
||||
# - fastlane-dependencies:
|
||||
# for: ios
|
||||
# - run:
|
||||
# working_directory: fastlane
|
||||
# name: Run fastlane to build unsigned x86_64 iOS app for iPhone simulator
|
||||
# no_output_timeout: 30m
|
||||
# command: |
|
||||
# HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
# bundle exec fastlane ios simulator
|
||||
# - persist_to_workspace:
|
||||
# root: ~/
|
||||
# paths:
|
||||
# - mattermost-mobile/Mattermost-simulator-x86_64.app.zip
|
||||
# - save:
|
||||
# filename: "Mattermost-simulator-x86_64.app.zip"
|
||||
|
||||
deploy-android-release:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- deploy-to-store:
|
||||
task: "Deploy to Google Play"
|
||||
target: android
|
||||
file: "*.apk"
|
||||
env: "SUPPLY_TRACK=beta"
|
||||
# deploy-android-release:
|
||||
# executor:
|
||||
# name: android
|
||||
# resource_class: medium
|
||||
# steps:
|
||||
# - deploy-to-store:
|
||||
# task: "Deploy to Google Play"
|
||||
# target: android
|
||||
# file: "*.apk"
|
||||
|
||||
deploy-android-beta:
|
||||
executor:
|
||||
@@ -445,14 +442,13 @@ jobs:
|
||||
file: "*.apk"
|
||||
env: "SUPPLY_TRACK=alpha"
|
||||
|
||||
deploy-ios-release:
|
||||
executor: ios
|
||||
steps:
|
||||
- deploy-to-store:
|
||||
task: "Deploy to TestFlight"
|
||||
target: ios
|
||||
file: "*.ipa"
|
||||
env: ""
|
||||
# deploy-ios-release:
|
||||
# executor: ios
|
||||
# steps:
|
||||
# - deploy-to-store:
|
||||
# task: "Deploy to TestFlight"
|
||||
# target: ios
|
||||
# file: "*.ipa"
|
||||
|
||||
deploy-ios-beta:
|
||||
executor: ios
|
||||
@@ -463,17 +459,17 @@ jobs:
|
||||
file: "*.ipa"
|
||||
env: ""
|
||||
|
||||
github-release:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run:
|
||||
name: Create GitHub release
|
||||
working_directory: fastlane
|
||||
command: bundle exec fastlane github
|
||||
# github-release:
|
||||
# executor:
|
||||
# name: android
|
||||
# resource_class: medium
|
||||
# steps:
|
||||
# - attach_workspace:
|
||||
# at: ~/
|
||||
# - run:
|
||||
# name: Create GitHub release
|
||||
# working_directory: fastlane
|
||||
# command: bundle exec fastlane github
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
@@ -485,24 +481,26 @@ workflows:
|
||||
# requires:
|
||||
# - test
|
||||
|
||||
- build-android-release:
|
||||
context: mattermost-mobile-android-release
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-release-\d+$/
|
||||
- /^build-android-release-\d+$/
|
||||
- deploy-android-release:
|
||||
context: mattermost-mobile-android-release
|
||||
requires:
|
||||
- build-android-release
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-release-\d+$/
|
||||
- /^build-android-release-\d+$/
|
||||
# - build-android-release:
|
||||
# context: mattermost-mobile-android-release
|
||||
# requires:
|
||||
# - test
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - /^build-\d+$/
|
||||
# - /^build-android-\d+$/
|
||||
# - /^build-android-release-\d+$/
|
||||
# - deploy-android-release:
|
||||
# context: mattermost-mobile-android-release
|
||||
# requires:
|
||||
# - build-android-release
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - /^build-\d+$/
|
||||
# - /^build-android-\d+$/
|
||||
# - /^build-android-release-\d+$/
|
||||
|
||||
- build-android-beta:
|
||||
context: mattermost-mobile-android-beta
|
||||
@@ -523,24 +521,26 @@ workflows:
|
||||
- /^build-android-\d+$/
|
||||
- /^build-android-beta-\d+$/
|
||||
|
||||
- build-ios-release:
|
||||
context: mattermost-mobile-ios-release
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-release-\d+$/
|
||||
- /^build-ios-release-\d+$/
|
||||
- deploy-ios-release:
|
||||
context: mattermost-mobile-ios-release
|
||||
requires:
|
||||
- build-ios-release
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-release-\d+$/
|
||||
- /^build-ios-release-\d+$/
|
||||
# - build-ios-release:
|
||||
# context: mattermost-mobile-ios-release
|
||||
# requires:
|
||||
# - test
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - /^build-\d+$/
|
||||
# - /^build-ios-\d+$/
|
||||
# - /^build-ios-release-\d+$/
|
||||
# - deploy-ios-release:
|
||||
# context: mattermost-mobile-ios-release
|
||||
# requires:
|
||||
# - build-ios-release
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - /^build-\d+$/
|
||||
# - /^build-ios-\d+$/
|
||||
# - /^build-ios-release-\d+$/
|
||||
|
||||
- build-ios-beta:
|
||||
context: mattermost-mobile-ios-beta
|
||||
@@ -576,41 +576,43 @@ workflows:
|
||||
branches:
|
||||
only: /^(build|ios)-pr-.*/
|
||||
|
||||
- build-android-unsigned:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
only: unsigned
|
||||
- build-ios-unsigned:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
only: unsigned
|
||||
- build-ios-simulator:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-ios-sim-\d+$/
|
||||
|
||||
- github-release:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- build-android-unsigned
|
||||
- build-ios-unsigned
|
||||
filters:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
only: unsigned
|
||||
# - build-android-unsigned:
|
||||
# context: mattermost-mobile-unsigned
|
||||
# requires:
|
||||
# - test
|
||||
# filters:
|
||||
# tags:
|
||||
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
# branches:
|
||||
# only: unsigned
|
||||
# - build-ios-unsigned:
|
||||
# context: mattermost-mobile-unsigned
|
||||
# requires:
|
||||
# - test
|
||||
# filters:
|
||||
# tags:
|
||||
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
# branches:
|
||||
# only: unsigned
|
||||
# - build-ios-simulator:
|
||||
# context: mattermost-mobile-unsigned
|
||||
# requires:
|
||||
# - test
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - /^build-\d+$/
|
||||
# - /^build-ios-\d+$/
|
||||
# - /^build-ios-beta-\d+$/
|
||||
# - /^build-ios-sim-\d+$/
|
||||
|
||||
# - github-release:
|
||||
# context: mattermost-mobile-unsigned
|
||||
# requires:
|
||||
# - build-android-unsigned
|
||||
# - build-ios-unsigned
|
||||
# filters:
|
||||
# tags:
|
||||
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
# branches:
|
||||
# only: unsigned
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"./eslint/eslint-mattermost",
|
||||
"./eslint/eslint-react",
|
||||
"plugin:mattermost/react",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
@@ -9,6 +8,7 @@
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"mattermost",
|
||||
"import"
|
||||
],
|
||||
"settings": {
|
||||
|
||||
@@ -67,4 +67,4 @@ untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.182.0
|
||||
^0.176.3
|
||||
|
||||
13
.github/codeql/codeql-config.yml
vendored
@@ -1,13 +0,0 @@
|
||||
name: "CodeQL config"
|
||||
|
||||
query-filters:
|
||||
- exclude:
|
||||
problem.severity:
|
||||
- warning
|
||||
- recommendation
|
||||
- exclude:
|
||||
id: js/insecure-randomness
|
||||
|
||||
paths-ignore:
|
||||
- test
|
||||
- '**/*.test.*'
|
||||
23
.github/workflows/codeql-analysis.yml
vendored
@@ -9,13 +9,8 @@ on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
permissions:
|
||||
security-events: write
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -23,20 +18,26 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
||||
4
.gitignore
vendored
@@ -44,8 +44,6 @@ ios/.xcode.env.local
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
android/app/bin
|
||||
android/app/build
|
||||
android/build
|
||||
@@ -102,7 +100,7 @@ coverage
|
||||
mattermost-license.txt
|
||||
*.mattermost-license
|
||||
detox/artifacts
|
||||
detox/detox_pixel_*
|
||||
detox/detox_pixel_4_xl_api_30
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
"error": "visit rvm install https://rvm.io/rvm/install",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "bundler",
|
||||
"semver": "2.1.4",
|
||||
"error": "install watchman `gem install bundler --version 2.1.4`",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "pod",
|
||||
|
||||
364
NOTICE.txt
@@ -200,41 +200,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @mattermost/react-native-turbo-mailer
|
||||
|
||||
This product contains '@mattermost/react-native-turbo-mailer' by Avinash Lingaloo.
|
||||
|
||||
An adaptation of react-native-mail that supports Turbo Module
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/mattermost/react-native-turbo-mailer#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Mattermost
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @msgpack/msgpack
|
||||
@@ -259,18 +224,18 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
|
||||
|
||||
## @nozbe/watermelondb
|
||||
|
||||
This product contains '@nozbe/watermelondb' by Nozbe.
|
||||
This product contains 'cameraroll' by Bartol Karuza.
|
||||
|
||||
Build powerful React and React Native apps that scale from hundreds to tens of thousands of records and remain fast ⚡️
|
||||
React-native native module that provides access to the local camera roll or photo library
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/Nozbe/WatermelonDB/
|
||||
* https://github.com/react-native-community/react-native-cameraroll
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) Nozbe
|
||||
Copyright (c) 2020 Elias Nahum
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -328,29 +293,86 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @react-native-camera-roll/camera-roll
|
||||
## @react-native-community/art
|
||||
|
||||
This product contains '@react-native-camera-roll/camera-roll' by Bartol Karuza.
|
||||
This product contains '@react-native-community/art' by react-native-art.
|
||||
|
||||
React Native Camera Roll for iOS & Android
|
||||
React Native module that allows you to draw vector graphics
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-cameraroll/react-native-cameraroll#readme
|
||||
* https://github.com/react-native-art/art
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 react-native-community
|
||||
|
||||
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-clipboard/clipboard
|
||||
## @react-native-community/cameraroll
|
||||
|
||||
This product contains '@react-native-clipboard/clipboard' by React Native Community.
|
||||
This product contains 'cameraroll' by Bartol Karuza.
|
||||
|
||||
React-native native module that provides access to the local camera roll or photo library
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-community/react-native-cameraroll
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @react-native-community/clipboard
|
||||
|
||||
This product contains '@react-native-community/clipboard' by React Native Community.
|
||||
|
||||
React Native Clipboard API for both iOS and Android
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-clipboard/clipboard
|
||||
* https://github.com/react-native-community/clipboard
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
@@ -528,21 +550,6 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @react-navigation/stack
|
||||
|
||||
This product contains '@react-navigation/stack'.
|
||||
|
||||
Stack navigator component for iOS and Android with animated transitions and gestures
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://reactnavigation.org/docs/stack-navigator/
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @rudderstack/rudder-sdk-react-native
|
||||
@@ -1159,40 +1166,6 @@ Lightweight fuzzy-search
|
||||
limitations under the License.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## html-entities
|
||||
|
||||
This product contains 'html-entities' by Marat Dulin.
|
||||
|
||||
Fastest HTML entities encode/decode library.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/mdevils/html-entities#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
Copyright (c) 2021 Dulin Marat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## jail-monkey
|
||||
@@ -2095,42 +2068,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-in-app-review
|
||||
|
||||
This product contains 'react-native-in-app-review' by Mina Samir Shafik.
|
||||
|
||||
react native in app review, to rate on Play store, App Store, Generally, the in-app review flow (see figure 1 for play store, figure 2 for ios) can be triggered at any time throughout the user journey of your app. During the flow, the user has the ability
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/MinaSamir11/react-native-in-app-review#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Mina Samir
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-incall-manager
|
||||
@@ -2411,6 +2348,42 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-neomorph-shadows
|
||||
|
||||
This product contains a modified version of 'react-native-neomorph-shadows' by Daniel.
|
||||
|
||||
Shadows and neumorphism/neomorphism for iOS & Android (like iOS).
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/tokkozhin/react-native-neomorph-shadows
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 tokkozhin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-notifications
|
||||
@@ -2612,42 +2585,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-shadow-2
|
||||
|
||||
This product contains a modified version of 'react-native-shadow-2' by Henrique Bruno Fantauzzi de Almeida.
|
||||
|
||||
Cross-platform shadow for React Native. Supports Android, iOS, Web and Expo.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/SrBrahma/react-native-shadow-2
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Henrique Bruno Fantauzzi de Almeida
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-share
|
||||
@@ -2720,6 +2657,42 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-user-agent
|
||||
|
||||
This product contains 'react-native-user-agent' by Bebnev Anton.
|
||||
|
||||
Library that helps you to get mobile application user agent and web view user agent strings.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/bebnev/react-native-user-agent
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Anton Bebnev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-vector-icons
|
||||
@@ -2962,6 +2935,63 @@ IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
|
||||
---
|
||||
|
||||
## reanimated-bottom-sheet
|
||||
|
||||
This product contains a modified version of 'reanimated-bottom-sheet' by Michał Osadnik.
|
||||
|
||||
Highly configurable component imitating native bottom sheet behavior, with fully native 60 FPS animations!
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/osdnk/react-native-reanimated-bottom-sheet
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
Copyright 2019 – present Michał Osadnik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## rn-placeholder
|
||||
|
||||
This product contains a modified version of 'rn-placeholder' by Marvin FRACHET.
|
||||
|
||||
Display some placeholder stuff before rendering your text or media content in React Native
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/mfrachet/rn-placeholder
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
Copyright (c) 2004-Today Marvin Frachet
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## semver
|
||||
|
||||
18
README.md
@@ -1,10 +1,12 @@
|
||||
# Mattermost Mobile v2
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (7.1.0+)
|
||||
- **Supported iOS versions:** 12.1+
|
||||
This is a work in progress branch for the next major version of the Mattermost mobile app. Once the work is completed and ready to share, this brach will be set as the default branch in this repository.
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (5.25)
|
||||
- **Supported iOS versions:** 11+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 21 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 14 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
|
||||
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
|
||||
|
||||
@@ -49,9 +51,15 @@ You can leave the Beta testing program at any time:
|
||||
|
||||
App data is wiped from the device when a user logs out of the app. If the user is logged in when the account is deactivated, then within one minute the system logs the user out, and as a result all app data is wiped from the device.
|
||||
|
||||
### I need the code for the v1 version
|
||||
### Can I connect to multiple Mattermost servers using the mobile apps?
|
||||
|
||||
You can still access it! We have moved the code from master to the [v1 branch](https://github.com/mattermost/mattermost-mobile/tree/v1). Be aware that we will not be providing any more v1 versions or updates in the public stores.
|
||||
At the moment, we only support connecting to one server at a time. If you need to connect to multiple servers, please [upvote the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/10975938) so we can track demand for it.
|
||||
|
||||
As a work around, you can install both the released "Mattermost" app and sign up to be a [tester](#testing) for the "Mattermost Beta" app so you can connect to two servers at once.
|
||||
|
||||
### Will there be second generation apps available for tablets?
|
||||
|
||||
We plan to add support for tablets in the future, but the timeline depends on how many people have a need for it. If you're looking for a tablet version, please help us out by [upvoting the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/20082079)!
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: 'kotlin-android'
|
||||
import com.android.build.OutputFile
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
/**
|
||||
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
|
||||
@@ -145,21 +144,33 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 452
|
||||
versionCode 426
|
||||
versionName "2.0.0"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
// We configure the CMake build only if you decide to opt-in for the New Architecture.
|
||||
// We configure the NDK build only if you decide to opt-in for the New Architecture.
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DPROJECT_BUILD_DIR=$buildDir",
|
||||
"-DREACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
|
||||
"-DREACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build",
|
||||
"-DNODE_MODULES_DIR=$rootDir/../node_modules",
|
||||
"-DANDROID_STL=c++_shared"
|
||||
ndkBuild {
|
||||
arguments "APP_PLATFORM=android-21",
|
||||
"APP_STL=c++_shared",
|
||||
"NDK_TOOLCHAIN_VERSION=clang",
|
||||
"GENERATED_SRC_DIR=$buildDir/generated/source",
|
||||
"PROJECT_BUILD_DIR=$buildDir",
|
||||
"REACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
|
||||
"REACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build",
|
||||
"NODE_MODULES_DIR=$rootDir/../node_modules"
|
||||
cFlags "-Wall", "-Werror", "-fexceptions", "-frtti", "-DWITH_INSPECTOR=1"
|
||||
cppFlags "-std=c++17"
|
||||
// Make sure this target name is the same you specify inside the
|
||||
// src/main/jni/Android.mk file for the `LOCAL_MODULE` variable.
|
||||
targets "rndiffapp_appmodules"
|
||||
// Fix for windows limit on number of character in file paths and in command lines
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
arguments "NDK_APP_SHORT_COMMANDS=true"
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!enableSeparateBuildPerCPUArchitecture) {
|
||||
@@ -173,8 +184,8 @@ android {
|
||||
if (isNewArchitectureEnabled()) {
|
||||
// We configure the NDK build only if you decide to opt-in for the New Architecture.
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "$projectDir/src/main/jni/CMakeLists.txt"
|
||||
ndkBuild {
|
||||
path "$projectDir/src/main/jni/Android.mk"
|
||||
}
|
||||
}
|
||||
def reactAndroidProjectDir = project(':ReactAndroid').projectDir
|
||||
@@ -196,15 +207,15 @@ android {
|
||||
preReleaseBuild.dependsOn(packageReactNdkReleaseLibs)
|
||||
|
||||
// Due to a bug inside AGP, we have to explicitly set a dependency
|
||||
// between configureCMakeDebug* tasks and the preBuild tasks.
|
||||
// between configureNdkBuild* tasks and the preBuild tasks.
|
||||
// This can be removed once this is solved: https://issuetracker.google.com/issues/207403732
|
||||
configureCMakeDebugRelease.dependsOn(preReleaseBuild)
|
||||
configureCMakeDebugDebug.dependsOn(preDebugBuild)
|
||||
configureNdkBuildRelease.dependsOn(preReleaseBuild)
|
||||
configureNdkBuildDebug.dependsOn(preDebugBuild)
|
||||
reactNativeArchitectures().each { architecture ->
|
||||
tasks.findByName("configureCMakeDebugDebug[${architecture}]")?.configure {
|
||||
tasks.findByName("configureNdkBuildDebug[${architecture}]")?.configure {
|
||||
dependsOn("preDebugBuild")
|
||||
}
|
||||
tasks.findByName("configureCMakeDebugRelease[${architecture}]")?.configure {
|
||||
tasks.findByName("configureNdkBuildRelease[${architecture}]")?.configure {
|
||||
dependsOn("preReleaseBuild")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.wix.detox.Detox;
|
||||
import com.wix.detox.config.DetoxConfig;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -20,11 +19,10 @@ public class DetoxTest {
|
||||
|
||||
@Test
|
||||
public void runDetoxTests() {
|
||||
DetoxConfig detoxConfig = new DetoxConfig();
|
||||
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
|
||||
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
|
||||
detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60);
|
||||
Detox.DetoxIdlePolicyConfig idlePolicyConfig = new Detox.DetoxIdlePolicyConfig();
|
||||
idlePolicyConfig.masterTimeoutSec = 60;
|
||||
idlePolicyConfig.idleResourceTimeoutSec = 30;
|
||||
|
||||
Detox.runTests(mActivityRule, detoxConfig);
|
||||
Detox.runTests(mActivityRule, idlePolicyConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@@ -83,23 +81,5 @@
|
||||
android:resizeableActivity="true"
|
||||
android:exported="true"
|
||||
/>
|
||||
<activity
|
||||
android:name="com.mattermost.share.ShareActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme"
|
||||
android:taskAffinity="com.mattermost.share"
|
||||
android:exported="true"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<!-- for sharing-->
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -74,10 +74,7 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, null);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, senderId))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -273,10 +270,7 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, serverUrl, "me", null);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, "me"))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -403,7 +397,6 @@ public class CustomPushNotificationHelper {
|
||||
String channelName = getConversationTitle(bundle);
|
||||
String senderName = bundle.getString("sender_name");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
String urlOverride = bundle.getString("override_icon_url");
|
||||
|
||||
int smallIconResId = getSmallIconResourceId(context, smallIcon);
|
||||
notification.setSmallIcon(smallIconResId);
|
||||
@@ -411,46 +404,33 @@ public class CustomPushNotificationHelper {
|
||||
if (serverUrl != null && channelName.equals(senderName)) {
|
||||
try {
|
||||
String senderId = bundle.getString("sender_id");
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
|
||||
if (avatar != null) {
|
||||
notification.setLargeIcon(avatar);
|
||||
}
|
||||
notification.setLargeIcon(userAvatar(context, serverUrl, senderId));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap userAvatar(Context context, final String serverUrl, final String userId, final String urlOverride) throws IOException {
|
||||
try {
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
Request request;
|
||||
String url;
|
||||
if (urlOverride != null) {
|
||||
request = new Request.Builder().url(urlOverride).build();
|
||||
Log.i("ReactNative", String.format("Fetch override profile image %s", urlOverride));
|
||||
} else {
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
|
||||
url = String.format("%s/api/v4/users/%s/image", serverUrl, userId);
|
||||
Log.i("ReactNative", String.format("Fetch profile image %s", url));
|
||||
request = new Request.Builder()
|
||||
.header("Authorization", String.format("Bearer %s", token))
|
||||
.url(url)
|
||||
.build();
|
||||
}
|
||||
Response response = client.newCall(request).execute();
|
||||
if (response.code() == 200) {
|
||||
assert response.body() != null;
|
||||
byte[] bytes = Objects.requireNonNull(response.body()).bytes();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
|
||||
return getCircleBitmap(bitmap);
|
||||
}
|
||||
private static Bitmap userAvatar(Context context, final String serverUrl, final String userId) throws IOException {
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
|
||||
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
final String url = String.format("%s/api/v4/users/%s/image", serverUrl, userId);
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", String.format("Bearer %s", token))
|
||||
.url(url)
|
||||
.build();
|
||||
Response response = client.newCall(request).execute();
|
||||
if (response.code() == 200) {
|
||||
assert response.body() != null;
|
||||
byte[] bytes = Objects.requireNonNull(response.body()).bytes();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
|
||||
|
||||
Log.i("ReactNative", String.format("Fetch profile %s", url));
|
||||
return getCircleBitmap(bitmap);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.net.Uri;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.content.ContentResolver;
|
||||
import android.os.Environment;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.util.Log;
|
||||
@@ -183,14 +182,8 @@ public class RealPathUtil {
|
||||
}
|
||||
|
||||
public static String getExtension(String uri) {
|
||||
String extension = "";
|
||||
if (uri == null) {
|
||||
return extension;
|
||||
}
|
||||
|
||||
extension = MimeTypeMap.getFileExtensionFromUrl(uri);
|
||||
if (!extension.equals("")) {
|
||||
return extension;
|
||||
return null;
|
||||
}
|
||||
|
||||
int dot = uri.lastIndexOf(".");
|
||||
@@ -217,15 +210,6 @@ public class RealPathUtil {
|
||||
return getMimeType(file);
|
||||
}
|
||||
|
||||
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
|
||||
try {
|
||||
ContentResolver cR = context.getContentResolver();
|
||||
return cR.getType(uri);
|
||||
} catch (Exception e) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteTempFiles(final File dir) {
|
||||
try {
|
||||
if (dir.isDirectory()) {
|
||||
@@ -257,21 +241,4 @@ public class RealPathUtil {
|
||||
return f.getName();
|
||||
}
|
||||
|
||||
public static File createDirIfNotExists(String path) {
|
||||
File dir = new File(path);
|
||||
if (dir.exists()) {
|
||||
return dir;
|
||||
}
|
||||
|
||||
try {
|
||||
dir.mkdirs();
|
||||
// Add .nomedia to hide the thumbnail directory from gallery
|
||||
File noMedia = new File(path, ".nomedia");
|
||||
noMedia.createNewFile();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import com.mattermost.helpers.Network;
|
||||
import com.mattermost.helpers.NotificationHelper;
|
||||
import com.mattermost.helpers.PushNotificationDataHelper;
|
||||
import com.mattermost.helpers.ResolvePromise;
|
||||
import com.mattermost.share.ShareModule;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
@@ -81,10 +80,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
switch (type) {
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_SESSION:
|
||||
ShareModule shareModule = ShareModule.getInstance();
|
||||
String currentActivityName = shareModule != null ? shareModule.getCurrentActivityName() : "";
|
||||
Log.i("ReactNative", currentActivityName);
|
||||
if (!mAppLifecycleFacade.isAppVisible() || !currentActivityName.equals("MainActivity")) {
|
||||
if (!mAppLifecycleFacade.isAppVisible()) {
|
||||
boolean createSummary = type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE);
|
||||
if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) {
|
||||
if (channelId != null) {
|
||||
|
||||
@@ -41,7 +41,7 @@ public class MainActivity extends NavigationActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(null);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.launch_screen);
|
||||
setHWKeyboardConnected();
|
||||
}
|
||||
|
||||
@@ -12,12 +12,13 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
import com.mattermost.share.ShareModule;
|
||||
import com.wix.reactnativenotifications.RNNotificationsPackage;
|
||||
|
||||
import com.reactnativenavigation.NavigationApplication;
|
||||
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
|
||||
import com.wix.reactnativenotifications.core.notification.IPushNotification;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.INotificationsDrawerApplication;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
@@ -67,12 +68,10 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
switch (name) {
|
||||
case "MattermostManaged":
|
||||
return MattermostManagedModule.getInstance(reactContext);
|
||||
case "MattermostShare":
|
||||
return ShareModule.getInstance(reactContext);
|
||||
case "Notifications":
|
||||
return NotificationsModule.getInstance(instance, reactContext);
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not find module " + name);
|
||||
case "Notifications":
|
||||
return NotificationsModule.getInstance(instance, reactContext);
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not find module " + name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +80,6 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
return () -> {
|
||||
Map<String, ReactModuleInfo> map = new HashMap<>();
|
||||
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
|
||||
map.put("MattermostShare", new ReactModuleInfo("MattermostShare", "com.mattermost.share.ShareModule", false, false, true, false, false));
|
||||
map.put("Notifications", new ReactModuleInfo("Notifications", "com.mattermost.rnbeta.NotificationsModule", false, false, false, false, false));
|
||||
return map;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -21,12 +20,8 @@ import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
import com.mattermost.helpers.Credentials;
|
||||
import com.reactlibrary.createthumbnail.CreateThumbnailModule;
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
|
||||
import java.io.File;
|
||||
@@ -34,7 +29,6 @@ import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
@@ -212,30 +206,6 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void createThumbnail(ReadableMap options, Promise promise) {
|
||||
try {
|
||||
WritableMap optionsMap = Arguments.createMap();
|
||||
optionsMap.merge(options);
|
||||
String url = options.hasKey("url") ? options.getString("url") : "";
|
||||
URL videoUrl = new URL(url);
|
||||
String serverUrl = videoUrl.getProtocol() + "://" + videoUrl.getHost() + ":" + videoUrl.getPort();
|
||||
String token = Credentials.getCredentialsForServerSync(this.reactContext, serverUrl);
|
||||
if (!TextUtils.isEmpty(token)) {
|
||||
WritableMap headers = Arguments.createMap();
|
||||
if (optionsMap.hasKey("headers")) {
|
||||
headers.merge(optionsMap.getMap("headers"));
|
||||
}
|
||||
headers.putString("Authorization", "Bearer " + token);
|
||||
optionsMap.putMap("headers", headers);
|
||||
}
|
||||
CreateThumbnailModule thumb = new CreateThumbnailModule(this.reactContext);
|
||||
thumb.create(optionsMap.copy(), promise);
|
||||
} catch (Exception e) {
|
||||
promise.reject("CreateThumbnail_ERROR", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SaveDataTask extends GuardedResultAsyncTask<Object> {
|
||||
private final WeakReference<Context> weakContext;
|
||||
private final String fromFile;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.mattermost.rnbeta.MainApplication;
|
||||
|
||||
public class ShareActivity extends ReactActivity {
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "MattermostShare";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
MainApplication app = (MainApplication) this.getApplication();
|
||||
app.sharedExtensionIsOpened = true;
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.mattermost.helpers.Credentials;
|
||||
import com.mattermost.rnbeta.MainApplication;
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class ShareModule extends ReactContextBaseJavaModule {
|
||||
private final OkHttpClient client = new OkHttpClient();
|
||||
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
private static ShareModule instance;
|
||||
private final MainApplication mApplication;
|
||||
private ReactApplicationContext mReactContext;
|
||||
private File tempFolder;
|
||||
|
||||
private ShareModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mReactContext = reactContext;
|
||||
mApplication = (MainApplication)reactContext.getApplicationContext();
|
||||
}
|
||||
|
||||
public static ShareModule getInstance(ReactApplicationContext reactContext) {
|
||||
if (instance == null) {
|
||||
instance = new ShareModule(reactContext);
|
||||
} else {
|
||||
instance.mReactContext = reactContext;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static ShareModule getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "MattermostShare";
|
||||
}
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
public String getCurrentActivityName() {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity != null) {
|
||||
String actvName = currentActivity.getComponentName().getClassName();
|
||||
String[] components = actvName.split("\\.");
|
||||
return components[components.length - 1];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void clear() {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity != null && this.getCurrentActivityName().equals("ShareActivity")) {
|
||||
Intent intent = currentActivity.getIntent();
|
||||
intent.setAction("");
|
||||
intent.removeExtra(Intent.EXTRA_TEXT);
|
||||
intent.removeExtra(Intent.EXTRA_STREAM);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
HashMap<String, Object> constants = new HashMap<>(1);
|
||||
constants.put("cacheDirName", RealPathUtil.CACHE_DIR_NAME);
|
||||
constants.put("isOpened", mApplication.sharedExtensionIsOpened);
|
||||
return constants;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void close(ReadableMap data) {
|
||||
this.clear();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity == null || !this.getCurrentActivityName().equals("ShareActivity")) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentActivity.finishAndRemoveTask();
|
||||
if (data != null && data.hasKey("serverUrl")) {
|
||||
ReadableArray files = data.getArray("files");
|
||||
String serverUrl = data.getString("serverUrl");
|
||||
final String token = Credentials.getCredentialsForServerSync(this.getReactApplicationContext(), serverUrl);
|
||||
JSONObject postData = buildPostObject(data);
|
||||
|
||||
if (files != null && files.size() > 0) {
|
||||
uploadFiles(serverUrl, token, files, postData);
|
||||
} else {
|
||||
try {
|
||||
post(serverUrl, token, postData);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mApplication.sharedExtensionIsOpened = false;
|
||||
RealPathUtil.deleteTempFiles(this.tempFolder);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getSharedData(Promise promise) {
|
||||
promise.resolve(processIntent());
|
||||
}
|
||||
|
||||
public WritableArray processIntent() {
|
||||
String type, action, extra;
|
||||
WritableArray items = Arguments.createArray();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (currentActivity != null) {
|
||||
this.tempFolder = new File(currentActivity.getCacheDir(), RealPathUtil.CACHE_DIR_NAME);
|
||||
Intent intent = currentActivity.getIntent();
|
||||
action = intent.getAction();
|
||||
type = intent.getType();
|
||||
extra = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type) && extra != null) {
|
||||
items.pushMap(ShareUtils.getTextItem(extra));
|
||||
} else if (Intent.ACTION_SEND.equals(action)) {
|
||||
if (extra != null) {
|
||||
items.pushMap(ShareUtils.getTextItem(extra));
|
||||
}
|
||||
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
if (uri != null) {
|
||||
ReadableMap fileInfo = ShareUtils.getFileItem(currentActivity, uri);
|
||||
if (fileInfo != null) {
|
||||
items.pushMap(fileInfo);
|
||||
}
|
||||
}
|
||||
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||
if (extra != null) {
|
||||
items.pushMap(ShareUtils.getTextItem(extra));
|
||||
}
|
||||
|
||||
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
for (Uri uri : uris) {
|
||||
ReadableMap fileInfo = ShareUtils.getFileItem(currentActivity, uri);
|
||||
if (fileInfo != null) {
|
||||
items.pushMap(fileInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private JSONObject buildPostObject(ReadableMap data) {
|
||||
JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("user_id", data.getString("userId"));
|
||||
if (data.hasKey("channelId")) {
|
||||
json.put("channel_id", data.getString("channelId"));
|
||||
}
|
||||
if (data.hasKey("message")) {
|
||||
json.put("message", data.getString("message"));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private void post(String serverUrl, String token, JSONObject postData) throws IOException {
|
||||
RequestBody body = RequestBody.create(postData.toString(), JSON);
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", "BEARER " + token)
|
||||
.url(serverUrl + "/api/v4/posts")
|
||||
.post(body)
|
||||
.build();
|
||||
client.newCall(request).execute();
|
||||
}
|
||||
|
||||
private void uploadFiles(String serverUrl, String token, ReadableArray files, JSONObject postData) {
|
||||
try {
|
||||
MultipartBody.Builder builder = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM);
|
||||
|
||||
for(int i = 0 ; i < files.size() ; i++) {
|
||||
ReadableMap file = files.getMap(i);
|
||||
String mime = file.getString("type");
|
||||
String fullPath = file.getString("value");
|
||||
if (fullPath != null) {
|
||||
String filePath = fullPath.replaceFirst("file://", "");
|
||||
File fileInfo = new File(filePath);
|
||||
if (fileInfo.exists() && mime != null) {
|
||||
final MediaType MEDIA_TYPE = MediaType.parse(mime);
|
||||
builder.addFormDataPart("files", file.getString("filename"), RequestBody.create(fileInfo, MEDIA_TYPE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.addFormDataPart("channel_id", postData.getString("channel_id"));
|
||||
RequestBody body = builder.build();
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", "BEARER " + token)
|
||||
.url(serverUrl + "/api/v4/files")
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (response.isSuccessful()) {
|
||||
String responseData = response.body().string();
|
||||
JSONObject responseJson = new JSONObject(responseData);
|
||||
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
|
||||
JSONArray file_ids = new JSONArray();
|
||||
for(int i = 0 ; i < fileInfoArray.length() ; i++) {
|
||||
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
|
||||
file_ids.put(fileInfo.getString("id"));
|
||||
}
|
||||
postData.put("file_ids", file_ids);
|
||||
post(serverUrl, token, postData);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ShareUtils {
|
||||
public static ReadableMap getTextItem(String text) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putString("value", text);
|
||||
map.putString("type", "");
|
||||
map.putBoolean("isString", true);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static ReadableMap getFileItem(Activity activity, Uri uri) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
String filePath = RealPathUtil.getRealPathFromURI(activity, uri);
|
||||
if (filePath == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File file = new File(filePath);
|
||||
String type = RealPathUtil.getMimeTypeFromUri(activity, uri);
|
||||
if (type != null) {
|
||||
if (type.startsWith("image/")) {
|
||||
BitmapFactory.Options bitMapOption = ShareUtils.getImageDimensions(filePath);
|
||||
map.putInt("height", bitMapOption.outHeight);
|
||||
map.putInt("width", bitMapOption.outWidth);
|
||||
|
||||
} else if (type.startsWith("video/")) {
|
||||
File cacheDir = new File(activity.getCacheDir(), RealPathUtil.CACHE_DIR_NAME);
|
||||
addVideoThumbnailToMap(cacheDir, activity.getApplicationContext(), map, "file://" + filePath);
|
||||
}
|
||||
} else {
|
||||
type = "application/octet-stream";
|
||||
}
|
||||
|
||||
map.putString("value", "file://" + filePath);
|
||||
map.putDouble("size", (double) file.length());
|
||||
map.putString("filename", file.getName());
|
||||
map.putString("type", type);
|
||||
map.putString("extension", RealPathUtil.getExtension(filePath).replaceFirst(".", ""));
|
||||
map.putBoolean("isString", false);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static BitmapFactory.Options getImageDimensions(String filePath) {
|
||||
BitmapFactory.Options bitMapOption = new BitmapFactory.Options();
|
||||
bitMapOption.inJustDecodeBounds=true;
|
||||
BitmapFactory.decodeFile(filePath, bitMapOption);
|
||||
return bitMapOption;
|
||||
}
|
||||
|
||||
private static void addVideoThumbnailToMap(File cacheDir, Context context, WritableMap map, String filePath) {
|
||||
String fileName = ("thumb-" + UUID.randomUUID().toString()) + ".png";
|
||||
OutputStream fOut = null;
|
||||
|
||||
try {
|
||||
File file = new File(cacheDir, fileName);
|
||||
Bitmap image = getBitmapAtTime(context, filePath, 1);
|
||||
if (file.createNewFile()) {
|
||||
fOut = new FileOutputStream(file);
|
||||
image.compress(Bitmap.CompressFormat.PNG, 100, fOut);
|
||||
fOut.flush();
|
||||
fOut.close();
|
||||
|
||||
map.putString("videoThumb", "file://" + file.getAbsolutePath());
|
||||
map.putInt("width", image.getWidth());
|
||||
map.putInt("height", image.getHeight());
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap getBitmapAtTime(Context context, String filePath, int time) {
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
if (URLUtil.isFileUrl(filePath)) {
|
||||
String decodedPath;
|
||||
try {
|
||||
decodedPath = URLDecoder.decode(filePath, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
decodedPath = filePath;
|
||||
}
|
||||
|
||||
retriever.setDataSource(decodedPath.replace("file://", ""));
|
||||
} else if (filePath.contains("content://")) {
|
||||
retriever.setDataSource(context, Uri.parse(filePath));
|
||||
}
|
||||
|
||||
Bitmap image = retriever.getFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
|
||||
retriever.release();
|
||||
return image;
|
||||
}
|
||||
}
|
||||
48
android/app/src/main/jni/Android.mk
Normal file
@@ -0,0 +1,48 @@
|
||||
THIS_DIR := $(call my-dir)
|
||||
|
||||
include $(REACT_ANDROID_DIR)/Android-prebuilt.mk
|
||||
|
||||
# If you wish to add a custom TurboModule or Fabric component in your app you
|
||||
# will have to include the following autogenerated makefile.
|
||||
# include $(GENERATED_SRC_DIR)/codegen/jni/Android.mk
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_PATH := $(THIS_DIR)
|
||||
|
||||
# You can customize the name of your application .so file here.
|
||||
LOCAL_MODULE := mattermost_appmodules
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)
|
||||
LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp)
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
|
||||
|
||||
# If you wish to add a custom TurboModule or Fabric component in your app you
|
||||
# will have to uncomment those lines to include the generated source
|
||||
# files from the codegen (placed in $(GENERATED_SRC_DIR)/codegen/jni)
|
||||
#
|
||||
# LOCAL_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni
|
||||
# LOCAL_SRC_FILES += $(wildcard $(GENERATED_SRC_DIR)/codegen/jni/*.cpp)
|
||||
# LOCAL_EXPORT_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni
|
||||
|
||||
# Here you should add any native library you wish to depend on.
|
||||
LOCAL_SHARED_LIBRARIES := \
|
||||
libfabricjni \
|
||||
libfbjni \
|
||||
libfolly_runtime \
|
||||
libglog \
|
||||
libjsi \
|
||||
libreact_codegen_rncore \
|
||||
libreact_debug \
|
||||
libreact_nativemodule_core \
|
||||
libreact_render_componentregistry \
|
||||
libreact_render_core \
|
||||
libreact_render_debug \
|
||||
libreact_render_graphics \
|
||||
librrc_view \
|
||||
libruntimeexecutor \
|
||||
libturbomodulejsijni \
|
||||
libyoga
|
||||
|
||||
LOCAL_CFLAGS := -DLOG_TAG=\"ReactNative\" -fexceptions -frtti -std=c++17 -Wall
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
@@ -1,7 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
# Define the library name here.
|
||||
project(rndiffapp_appmodules)
|
||||
|
||||
# This file includes all the necessary to let you build your application with the New Architecture.
|
||||
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)
|
||||
@@ -1,13 +1,12 @@
|
||||
#include "MainApplicationModuleProvider.h"
|
||||
|
||||
#include <rncli.h>
|
||||
#include <rncore.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
const std::string &moduleName,
|
||||
const std::string moduleName,
|
||||
const JavaTurboModule::InitParams ¶ms) {
|
||||
// Here you can provide your own module provider for TurboModules coming from
|
||||
// either your application or from external libraries. The approach to follow
|
||||
@@ -18,13 +17,6 @@ std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
// return module;
|
||||
// }
|
||||
// return rncore_ModuleProvider(moduleName, params);
|
||||
|
||||
// Module providers autolinked by RN CLI
|
||||
auto rncli_module = rncli_ModuleProvider(moduleName, params);
|
||||
if (rncli_module != nullptr) {
|
||||
return rncli_module;
|
||||
}
|
||||
|
||||
return rncore_ModuleProvider(moduleName, params);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace facebook {
|
||||
namespace react {
|
||||
|
||||
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
const std::string &moduleName,
|
||||
const std::string moduleName,
|
||||
const JavaTurboModule::InitParams ¶ms);
|
||||
|
||||
} // namespace react
|
||||
|
||||
@@ -22,24 +22,24 @@ void MainApplicationTurboModuleManagerDelegate::registerNatives() {
|
||||
|
||||
std::shared_ptr<TurboModule>
|
||||
MainApplicationTurboModuleManagerDelegate::getTurboModule(
|
||||
const std::string &name,
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker) {
|
||||
const std::string name,
|
||||
const std::shared_ptr<CallInvoker> jsInvoker) {
|
||||
// Not implemented yet: provide pure-C++ NativeModules here.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule>
|
||||
MainApplicationTurboModuleManagerDelegate::getTurboModule(
|
||||
const std::string &name,
|
||||
const std::string name,
|
||||
const JavaTurboModule::InitParams ¶ms) {
|
||||
return MainApplicationModuleProvider(name, params);
|
||||
}
|
||||
|
||||
bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule(
|
||||
const std::string &name) {
|
||||
std::string name) {
|
||||
return getTurboModule(name, nullptr) != nullptr ||
|
||||
getTurboModule(name, {.moduleName = name}) != nullptr;
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
} // namespace facebook
|
||||
@@ -14,25 +14,25 @@ class MainApplicationTurboModuleManagerDelegate
|
||||
public:
|
||||
// Adapt it to the package you used for your Java class.
|
||||
static constexpr auto kJavaDescriptor =
|
||||
"Lcom/rndiffapp/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;";
|
||||
"Lcom/mattermost/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;";
|
||||
|
||||
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject>);
|
||||
|
||||
static void registerNatives();
|
||||
|
||||
std::shared_ptr<TurboModule> getTurboModule(
|
||||
const std::string &name,
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker) override;
|
||||
const std::string name,
|
||||
const std::shared_ptr<CallInvoker> jsInvoker) override;
|
||||
std::shared_ptr<TurboModule> getTurboModule(
|
||||
const std::string &name,
|
||||
const std::string name,
|
||||
const JavaTurboModule::InitParams ¶ms) override;
|
||||
|
||||
/**
|
||||
* Test-only method. Allows user to verify whether a TurboModule can be
|
||||
* created by instances of this class.
|
||||
*/
|
||||
bool canCreateTurboModule(const std::string &name);
|
||||
bool canCreateTurboModule(std::string name);
|
||||
};
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
} // namespace facebook
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
||||
#include <react/renderer/components/rncore/ComponentDescriptors.h>
|
||||
#include <rncli.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
@@ -15,9 +14,6 @@ std::shared_ptr<ComponentDescriptorProviderRegistry const>
|
||||
MainComponentsRegistry::sharedProviderRegistry() {
|
||||
auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry();
|
||||
|
||||
// Autolinked providers registered by RN CLI
|
||||
rncli_registerProviders(providerRegistry);
|
||||
|
||||
// Custom Fabric Components go here. You can register custom
|
||||
// components coming from your App or from 3rd party libraries here.
|
||||
//
|
||||
@@ -62,4 +58,4 @@ void MainComponentsRegistry::registerNatives() {
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
} // namespace facebook
|
||||
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 3.0 MiB |
@@ -2,5 +2,5 @@
|
||||
<resources>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
<color name="splashscreen_bg">#090A0B</color>
|
||||
<color name="splashscreen_bg">#1E325C</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<files-path name="files" path="." />
|
||||
<external-files-path name="external_files" path="." />
|
||||
<external-path name="external" path="." />
|
||||
<cache-path name="cache" path="." />
|
||||
<root-path name="root" path="." />
|
||||
</paths>
|
||||
@@ -1,3 +1,5 @@
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
@@ -25,10 +27,10 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.2.1")
|
||||
classpath("com.android.tools.build:gradle:7.1.1")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("de.undercouch:gradle-download-task:5.0.1")
|
||||
classpath('com.google.gms:google-services:4.3.14')
|
||||
classpath('com.google.gms:google-services:4.3.10')
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
@@ -38,20 +40,6 @@ buildscript {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
exclusiveContent {
|
||||
// We get React Native's Android binaries exclusively through npm,
|
||||
// from a local Maven repo inside node_modules/react-native/.
|
||||
// (The use of exclusiveContent prevents looking elsewhere like Maven Central
|
||||
// and potentially getting a wrong version.)
|
||||
filter {
|
||||
includeGroup "com.facebook.react"
|
||||
}
|
||||
forRepository {
|
||||
maven {
|
||||
url "$rootDir/../node_modules/react-native/android"
|
||||
}
|
||||
}
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=1g
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
|
||||
|
||||
@@ -3,45 +3,39 @@
|
||||
|
||||
import {GLOBAL_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export const storeGlobal = async (id: string, value: unknown, prepareRecordsOnly = false) => {
|
||||
export const storeDeviceToken = async (token: string, prepareRecordsOnly = false) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
return operator.handleGlobal({
|
||||
globals: [{id, value}],
|
||||
globals: [{id: GLOBAL_IDENTIFIERS.DEVICE_TOKEN, value: token}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('storeGlobal', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const storeDeviceToken = async (token: string, prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.DEVICE_TOKEN, token, prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeOnboardingViewedValue = async (value = true) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.ONBOARDING, value, false);
|
||||
try {
|
||||
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
return operator.handleGlobal({
|
||||
globals: [{id: GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, value: 'true'}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const storeProfileLongPressTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeDontAskForReview = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.DONT_ASK_FOR_REVIEW, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeLastAskForReview = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.LAST_ASK_FOR_REVIEW, Date.now(), prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeFirstLaunch = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.FIRST_LAUNCH, Date.now(), prepareRecordsOnly);
|
||||
try {
|
||||
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
return operator.handleGlobal({
|
||||
globals: [{id: GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, value: 'true'}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,10 +5,11 @@ import {Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {CHANNELS_CATEGORY, DMS_CATEGORY} from '@constants/categories';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById, prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||
import {prepareCategories, prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById} from '@queries/servers/categories';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {queryMyTeams} from '@queries/servers/team';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {pluckUnique} from '@utils/helpers';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
@@ -33,18 +34,48 @@ export const deleteCategory = async (serverUrl: string, categoryId: string) => {
|
||||
|
||||
export async function storeCategories(serverUrl: string, categories: CategoryWithChannels[], prune = false, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const models = await prepareCategoriesAndCategoriesChannels(operator, categories, prune);
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
const preparedCategories = prepareCategories(operator, categories);
|
||||
if (preparedCategories) {
|
||||
modelPromises.push(preparedCategories);
|
||||
}
|
||||
|
||||
const preparedCategoryChannels = prepareCategoryChannels(operator, categories);
|
||||
if (preparedCategoryChannels) {
|
||||
modelPromises.push(preparedCategoryChannels);
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
const flattenedModels = models.flat();
|
||||
|
||||
if (prune && categories.length) {
|
||||
const remoteCategoryIds = new Set(categories.map((cat) => cat.id));
|
||||
|
||||
// If the passed categories have more than one team, we want to update across teams
|
||||
const teamIds = pluckUnique('team_id')(categories) as string[];
|
||||
const localCategories = await queryCategoriesByTeamIds(database, teamIds).fetch();
|
||||
|
||||
localCategories.
|
||||
filter((category) => category.type === 'custom').
|
||||
forEach((localCategory) => {
|
||||
if (!remoteCategoryIds.has(localCategory.id)) {
|
||||
localCategory.prepareDestroyPermanently();
|
||||
flattenedModels.push(localCategory);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (prepareRecordsOnly) {
|
||||
return {models};
|
||||
return {models: flattenedModels};
|
||||
}
|
||||
|
||||
if (models.length > 0) {
|
||||
await operator.batchRecords(models);
|
||||
if (flattenedModels?.length > 0) {
|
||||
await operator.batchRecords(flattenedModels);
|
||||
}
|
||||
|
||||
return {models};
|
||||
return {models: flattenedModels};
|
||||
} catch (error) {
|
||||
logError('FAILED TO STORE CATEGORIES', error);
|
||||
return {error};
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes,
|
||||
} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId} 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';
|
||||
@@ -76,7 +76,7 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
|
||||
}
|
||||
|
||||
models = (await Promise.all(modelPromises)).flat();
|
||||
const {member: viewedAt} = await markChannelAsViewed(serverUrl, channelId, false, true);
|
||||
const {member: viewedAt} = await markChannelAsViewed(serverUrl, channelId, true);
|
||||
if (viewedAt) {
|
||||
models.push(viewedAt);
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export async function selectAllMyChannelIds(serverUrl: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function markChannelAsViewed(serverUrl: string, channelId: string, onlyCounts = false, prepareRecordsOnly = false) {
|
||||
export async function markChannelAsViewed(serverUrl: string, channelId: string, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const member = await getMyChannel(database, channelId);
|
||||
@@ -172,10 +172,8 @@ export async function markChannelAsViewed(serverUrl: string, channelId: string,
|
||||
m.isUnread = false;
|
||||
m.mentionsCount = 0;
|
||||
m.manuallyUnread = false;
|
||||
if (!onlyCounts) {
|
||||
m.viewedAt = member.lastViewedAt;
|
||||
m.lastViewedAt = Date.now();
|
||||
}
|
||||
m.viewedAt = member.lastViewedAt;
|
||||
m.lastViewedAt = Date.now();
|
||||
});
|
||||
PushNotifications.removeChannelNotifications(serverUrl, channelId);
|
||||
if (!prepareRecordsOnly) {
|
||||
@@ -367,10 +365,9 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha
|
||||
return {};
|
||||
}
|
||||
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const displaySettings = getTeammateNameDisplaySetting(preferences, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const displaySettings = getTeammateNameDisplaySetting(preferences, config, license);
|
||||
const models: Model[] = [];
|
||||
for await (const channel of channels) {
|
||||
let newDisplayName = '';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchPostAuthors} from '@actions/remote/post';
|
||||
import {ActionType, Post} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPostById, prepareDeletePost, queryPostsById} from '@queries/servers/post';
|
||||
@@ -79,7 +78,7 @@ export const sendEphemeralPost = async (serverUrl: string, message: string, chan
|
||||
user_id: authorId,
|
||||
channel_id: channeId,
|
||||
message,
|
||||
type: Post.POST_TYPES.EPHEMERAL as PostType,
|
||||
type: Post.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL as PostType,
|
||||
create_at: timestamp,
|
||||
edit_at: 0,
|
||||
update_at: timestamp,
|
||||
@@ -95,7 +94,6 @@ export const sendEphemeralPost = async (serverUrl: string, message: string, chan
|
||||
props: {},
|
||||
} as Post;
|
||||
|
||||
await fetchPostAuthors(serverUrl, [post], false);
|
||||
await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||
order: [post.id],
|
||||
|
||||
@@ -6,7 +6,7 @@ import deepEqual from 'deep-equal';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export async function storeConfigAndLicense(serverUrl: string, config: ClientConfig, license: ClientLicense) {
|
||||
@@ -14,11 +14,17 @@ export async function storeConfigAndLicense(serverUrl: string, config: ClientCon
|
||||
// If we have credentials for this server then update the values in the database
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials) {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentLicense = await getLicense(database);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const current = await getCommonSystemValues(operator.database);
|
||||
const systems: IdValue[] = [];
|
||||
if (!deepEqual(config, current.config)) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.CONFIG,
|
||||
value: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
if (!deepEqual(license, currentLicense)) {
|
||||
if (!deepEqual(license, current.license)) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.LICENSE,
|
||||
value: JSON.stringify(license),
|
||||
@@ -28,87 +34,8 @@ export async function storeConfigAndLicense(serverUrl: string, config: ClientCon
|
||||
if (systems.length) {
|
||||
await operator.handleSystem({systems, prepareRecordsOnly: false});
|
||||
}
|
||||
|
||||
await storeConfig(serverUrl, config);
|
||||
}
|
||||
} catch (error) {
|
||||
logError('An error occurred while saving config & license', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function storeConfig(serverUrl: string, config: ClientConfig | undefined, prepareRecordsOnly = false) {
|
||||
if (!config) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentConfig = await getConfig(database);
|
||||
const configsToUpdate: IdValue[] = [];
|
||||
const configsToDelete: IdValue[] = [];
|
||||
|
||||
let k: keyof ClientConfig;
|
||||
for (k in config) {
|
||||
if (currentConfig?.[k] !== config[k]) {
|
||||
configsToUpdate.push({
|
||||
id: k,
|
||||
value: config[k],
|
||||
});
|
||||
}
|
||||
}
|
||||
for (k in currentConfig) {
|
||||
if (config[k] === undefined) {
|
||||
configsToDelete.push({
|
||||
id: k,
|
||||
value: currentConfig[k],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (configsToDelete.length || configsToUpdate.length) {
|
||||
return operator.handleConfigs({configs: configsToUpdate, configsToDelete, prepareRecordsOnly});
|
||||
}
|
||||
} catch (error) {
|
||||
logError('storeConfig', error);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function setLastServerVersionCheck(serverUrl: string, reset = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
await operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.LAST_SERVER_VERSION_CHECK,
|
||||
value: reset ? 0 : Date.now(),
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('setLastServerVersionCheck', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setGlobalThreadsTab(serverUrl: string, globalThreadsTab: GlobalThreadsTab) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
await operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.GLOBAL_THREADS_TAB,
|
||||
value: globalThreadsTab,
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('setGlobalThreadsTab', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function dismissAnnouncement(serverUrl: string, announcementText: string) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.LAST_DISMISSED_BANNER, value: announcementText}], prepareRecordsOnly: false});
|
||||
} catch (error) {
|
||||
logError('An error occurred while dismissing an announcement', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareDeleteTeam, getMyTeamById, queryTeamSearchHistoryByTeamId, removeTeamFromTeamHistory, getTeamSearchHistoryById, getTeamById} from '@queries/servers/team';
|
||||
import {prepareDeleteTeam, getMyTeamById, queryTeamSearchHistoryByTeamId, removeTeamFromTeamHistory, getTeamSearchHistoryById} from '@queries/servers/team';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
@@ -12,7 +12,7 @@ export async function removeUserFromTeam(serverUrl: string, teamId: string) {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const myTeam = await getMyTeamById(database, teamId);
|
||||
if (myTeam) {
|
||||
const team = await getTeamById(database, myTeam.id);
|
||||
const team = await myTeam.team.fetch();
|
||||
if (!team) {
|
||||
throw new Error('Team not found');
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ 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, PrepareCommonSystemValuesArgs, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getCommonSystemValues, getCurrentTeamId, getCurrentUserId, prepareCommonSystemValues, PrepareCommonSystemValuesArgs, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled, getThreadById, prepareThreadsFromReceivedPosts, queryThreadsInTeam} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
@@ -74,24 +74,16 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
|
||||
throw new Error('Channel not found');
|
||||
}
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const system = await getCommonSystemValues(database);
|
||||
const isTabletDevice = await isTablet();
|
||||
const teamId = channel.teamId || currentTeamId;
|
||||
const teamId = channel.teamId || system.currentTeamId;
|
||||
|
||||
let switchingTeams = false;
|
||||
if (currentTeamId === teamId) {
|
||||
const models = await prepareCommonSystemValues(operator, {
|
||||
currentChannelId: channel.id,
|
||||
});
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
} else {
|
||||
if (system.currentTeamId !== teamId) {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
switchingTeams = true;
|
||||
modelPromises.push(addTeamToTeamHistory(operator, teamId, true));
|
||||
const commonValues: PrepareCommonSystemValuesArgs = {
|
||||
currentChannelId: channel.id,
|
||||
currentTeamId: teamId,
|
||||
};
|
||||
modelPromises.push(prepareCommonSystemValues(operator, commonValues));
|
||||
@@ -176,7 +168,7 @@ export async function createThreadFromNewPost(serverUrl: string, post: Post, pre
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const models: Model[] = [];
|
||||
if (post.root_id) {
|
||||
// Update the thread data: `reply_count`
|
||||
// Update the thread data: `reply_count`
|
||||
const {model: threadModel} = await updateThread(serverUrl, post.root_id, {reply_count: post.reply_count}, true);
|
||||
if (threadModel) {
|
||||
models.push(threadModel);
|
||||
@@ -212,7 +204,7 @@ export async function createThreadFromNewPost(serverUrl: string, post: Post, pre
|
||||
}
|
||||
|
||||
// On receiving threads, Along with the "threads" & "thread participants", extract and save "posts" & "users"
|
||||
export async function processReceivedThreads(serverUrl: string, threads: Thread[], teamId: string, prepareRecordsOnly = false) {
|
||||
export async function processReceivedThreads(serverUrl: string, threads: Thread[], teamId: string, loadedInGlobalThreads = false, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentUserId = await getCurrentUserId(database);
|
||||
@@ -244,6 +236,7 @@ export async function processReceivedThreads(serverUrl: string, threads: Thread[
|
||||
threads: threadsToHandle,
|
||||
teamId,
|
||||
prepareRecordsOnly: true,
|
||||
loadedInGlobalThreads,
|
||||
});
|
||||
|
||||
const models = [...postModels, ...threadModels];
|
||||
@@ -335,17 +328,3 @@ export async function updateThread(serverUrl: string, threadId: string, updatedT
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTeamThreadsSync(serverUrl: string, data: TeamThreadsSync, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const models = await operator.handleTeamThreadsSync({data: [data], prepareRecordsOnly});
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
return {models};
|
||||
} catch (error) {
|
||||
logError('Failed updateTeamThreadsSync', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ export function postEphemeralCallResponseForPost(serverUrl: string, response: Ap
|
||||
serverUrl,
|
||||
message,
|
||||
post.channelId,
|
||||
post.rootId || post.id,
|
||||
post.rootId,
|
||||
response.app_metadata?.bot_user_id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,26 +9,22 @@ import {DeviceEventEmitter} from 'react-native';
|
||||
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
|
||||
import {removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {switchToGlobalThreads} from '@actions/local/thread';
|
||||
import {loadCallForChannel} from '@calls/actions/calls';
|
||||
import {DeepLink, Events, General, Preferences, Screens} from '@constants';
|
||||
import {Events, General, Preferences, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {privateChannelJoinPrompt} from '@helpers/api/channel';
|
||||
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getActiveServer} from '@queries/app/servers';
|
||||
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getCommonSystemValues, getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getLicense, setCurrentChannelId, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams, removeChannelFromTeamHistory} from '@queries/servers/team';
|
||||
import {getCommonSystemValues, getConfig, getCurrentTeamId, getCurrentUserId, getLicense, setCurrentChannelId} from '@queries/servers/system';
|
||||
import {prepareMyTeams, getNthLastChannelFromTeam, getMyTeamById, getTeamById, getTeamByName, queryMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {dismissAllModals, popToRoot} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from '@utils/channel';
|
||||
import {generateChannelNameFromDisplayName, getDirectChannelName, isArchived, isDMorGM} from '@utils/channel';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug, logError, logInfo} from '@utils/log';
|
||||
import {logError, logInfo} from '@utils/log';
|
||||
import {showMuteChannelSnackbar} from '@utils/snack_bar';
|
||||
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
|
||||
import {displayGroupMessageName, displayUsername} from '@utils/user';
|
||||
|
||||
import {fetchGroupsForChannelIfConstrained} from './groups';
|
||||
@@ -36,11 +32,14 @@ import {fetchPostsForChannel} from './post';
|
||||
import {setDirectChannelVisible} from './preference';
|
||||
import {fetchRolesIfNeeded} from './role';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from './team';
|
||||
import {addUserToTeam, fetchTeamByName, removeUserFromTeam} from './team';
|
||||
import {fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
export type MyChannelsRequest = {
|
||||
categories?: CategoryWithChannels[];
|
||||
@@ -360,9 +359,6 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string,
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fetchOnly) {
|
||||
setTeamLoading(serverUrl, true);
|
||||
}
|
||||
const [allChannels, channelMemberships, categoriesWithOrder] = await Promise.all([
|
||||
client.getMyChannels(teamId, includeDeleted, since),
|
||||
client.getMyChannelMembers(teamId),
|
||||
@@ -391,14 +387,10 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string,
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
|
||||
return {channels, memberships, categories};
|
||||
} catch (error) {
|
||||
if (!fetchOnly) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
@@ -533,18 +525,17 @@ export async function fetchDirectChannelsInfo(serverUrl: string, directChannels:
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).fetch();
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences, config?.LockTeammateNameDisplay, config?.TeammateNameDisplay, license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences, config, license);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const channels = directChannels.map((d) => d.toApi());
|
||||
return fetchMissingDirectChannelsInfo(serverUrl, channels, currentUser?.locale, teammateDisplayNameSetting, currentUser?.id);
|
||||
}
|
||||
|
||||
export async function joinChannel(serverUrl: string, teamId: string, channelId?: string, channelName?: string, fetchOnly = false) {
|
||||
export async function joinChannel(serverUrl: string, userId: string, teamId: string, channelId?: string, channelName?: string, fetchOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const database = operator.database;
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
@@ -553,8 +544,6 @@ export async function joinChannel(serverUrl: string, teamId: string, channelId?:
|
||||
return {error};
|
||||
}
|
||||
|
||||
const userId = await getCurrentUserId(database);
|
||||
|
||||
let member: ChannelMembership | undefined;
|
||||
let channel: Channel | undefined;
|
||||
try {
|
||||
@@ -608,7 +597,6 @@ export async function joinChannel(serverUrl: string, teamId: string, channelId?:
|
||||
}
|
||||
|
||||
if (channelId || channel?.id) {
|
||||
loadCallForChannel(serverUrl, channelId || channel!.id);
|
||||
EphemeralStore.removeJoiningChannel(channelId || channel!.id);
|
||||
}
|
||||
return {channel, member};
|
||||
@@ -626,7 +614,8 @@ export async function joinChannelIfNeeded(serverUrl: string, channelId: string)
|
||||
return {error: undefined};
|
||||
}
|
||||
|
||||
return joinChannel(serverUrl, '', channelId);
|
||||
const userId = await getCurrentUserId(database);
|
||||
return joinChannel(serverUrl, userId, '', channelId);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
@@ -645,104 +634,171 @@ export async function markChannelAsRead(serverUrl: string, channelId: string) {
|
||||
|
||||
export async function switchToChannelByName(serverUrl: string, channelName: string, teamName: string, errorHandler: (intl: IntlShape) => void, intl: IntlShape) {
|
||||
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 onError = (joinedTeam: boolean, teamId?: string) => {
|
||||
errorHandler(intl);
|
||||
if (joinedTeam && teamId) {
|
||||
removeCurrentUserFromTeam(serverUrl, teamId, false);
|
||||
}
|
||||
};
|
||||
|
||||
let joinedTeam = false;
|
||||
let teamId = '';
|
||||
try {
|
||||
if (teamName === DeepLink.Redirect) {
|
||||
teamId = await getCurrentTeamId(database);
|
||||
} else {
|
||||
const team = await getTeamByName(database, teamName);
|
||||
const isTeamMember = team ? await getMyTeamById(database, team.id) : false;
|
||||
teamId = team?.id || '';
|
||||
let myChannel: MyChannelModel | ChannelMembership | undefined;
|
||||
let team: TeamModel | Team | undefined;
|
||||
let myTeam: MyTeamModel | TeamMembership | undefined;
|
||||
let name = teamName;
|
||||
const roles: string [] = [];
|
||||
const system = await getCommonSystemValues(database);
|
||||
const currentTeam = await getTeamById(database, system.currentTeamId);
|
||||
|
||||
if (!isTeamMember) {
|
||||
const fetchRequest = await fetchTeamByName(serverUrl, teamName);
|
||||
if (!fetchRequest.team) {
|
||||
onError(joinedTeam);
|
||||
return {error: fetchRequest.error || 'no team received'};
|
||||
}
|
||||
const {error} = await addCurrentUserToTeam(serverUrl, fetchRequest.team.id);
|
||||
if (error) {
|
||||
onError(joinedTeam);
|
||||
return {error};
|
||||
}
|
||||
teamId = fetchRequest.team.id;
|
||||
joinedTeam = true;
|
||||
}
|
||||
if (name === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
|
||||
name = currentTeam!.name;
|
||||
} else {
|
||||
team = await getTeamByName(database, teamName);
|
||||
}
|
||||
|
||||
const channel = await getChannelByName(database, teamId, channelName);
|
||||
const isChannelMember = channel ? await getMyChannel(database, channel.id) : false;
|
||||
let channelId = channel?.id || '';
|
||||
if (!isChannelMember) {
|
||||
const fetchRequest = await fetchChannelByName(serverUrl, teamId, channelName, true);
|
||||
if (!fetchRequest.channel) {
|
||||
onError(joinedTeam, teamId);
|
||||
return {error: fetchRequest.error || 'cannot fetch channel'};
|
||||
if (!team) {
|
||||
const fetchTeam = await fetchTeamByName(serverUrl, name, true);
|
||||
if (fetchTeam.error) {
|
||||
errorHandler(intl);
|
||||
return {error: fetchTeam.error};
|
||||
}
|
||||
if (fetchRequest.channel.type === General.PRIVATE_CHANNEL) {
|
||||
const {join} = await privateChannelJoinPrompt(fetchRequest.channel.display_name, intl);
|
||||
|
||||
team = fetchTeam.team!;
|
||||
}
|
||||
|
||||
let joinedNewTeam = false;
|
||||
myTeam = await getMyTeamById(database, team.id);
|
||||
if (!myTeam) {
|
||||
const added = await addUserToTeam(serverUrl, team.id, system.currentUserId, true);
|
||||
if (added.error) {
|
||||
errorHandler(intl);
|
||||
return {error: added.error};
|
||||
}
|
||||
myTeam = added.member!;
|
||||
roles.push(...myTeam.roles.split(' '));
|
||||
joinedNewTeam = true;
|
||||
}
|
||||
|
||||
if (!myTeam) {
|
||||
errorHandler(intl);
|
||||
return {error: 'Could not fetch team member'};
|
||||
}
|
||||
|
||||
let channel: Channel | ChannelModel | undefined = await getChannelByName(database, team.id, channelName);
|
||||
if (!channel) {
|
||||
const chReq = await fetchChannelByName(serverUrl, team.id, channelName, true);
|
||||
if (chReq.error) {
|
||||
errorHandler(intl);
|
||||
return {error: chReq.error};
|
||||
}
|
||||
channel = chReq.channel;
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
errorHandler(intl);
|
||||
return {error: 'Could not fetch channel'};
|
||||
}
|
||||
|
||||
if (isArchived(channel) && system.config.ExperimentalViewArchivedChannels !== 'true') {
|
||||
errorHandler(intl);
|
||||
return {error: 'Channel is archived'};
|
||||
}
|
||||
|
||||
myChannel = await getMyChannel(database, channel.id);
|
||||
|
||||
if (!myChannel) {
|
||||
const channelTeamId = 'team_id' in channel ? channel.team_id : channel.teamId;
|
||||
const req = await fetchMyChannel(serverUrl, channelTeamId || team.id, channel.id, true);
|
||||
myChannel = req.memberships?.[0];
|
||||
}
|
||||
|
||||
if (!myChannel) {
|
||||
if (channel.type === General.PRIVATE_CHANNEL) {
|
||||
const displayName = 'display_name' in channel ? channel.display_name : channel.displayName;
|
||||
const {join} = await privateChannelJoinPrompt(displayName, intl);
|
||||
if (!join) {
|
||||
onError(joinedTeam, teamId);
|
||||
if (joinedNewTeam) {
|
||||
await removeUserFromTeam(serverUrl, team.id, system.currentUserId, true);
|
||||
}
|
||||
errorHandler(intl);
|
||||
return {error: 'Refused to join Private channel'};
|
||||
}
|
||||
}
|
||||
logInfo('joining channel', displayName, channel.id);
|
||||
const result = await joinChannel(serverUrl, system.currentUserId, team.id, channel.id, undefined, true);
|
||||
if (result.error || !result.channel) {
|
||||
if (joinedNewTeam) {
|
||||
await removeUserFromTeam(serverUrl, team.id, system.currentUserId, true);
|
||||
}
|
||||
|
||||
logInfo('joining channel', fetchRequest.channel.display_name, fetchRequest.channel.id);
|
||||
const joinRequest = await joinChannel(serverUrl, teamId, undefined, channelName, false);
|
||||
if (!joinRequest.channel) {
|
||||
onError(joinedTeam, teamId);
|
||||
return {error: joinRequest.error || 'no channel returned from join'};
|
||||
}
|
||||
errorHandler(intl);
|
||||
return {error: result.error};
|
||||
}
|
||||
|
||||
channelId = fetchRequest.channel.id;
|
||||
myChannel = result.member!;
|
||||
roles.push(...myChannel.roles.split(' '));
|
||||
}
|
||||
}
|
||||
|
||||
if (!myChannel) {
|
||||
errorHandler(intl);
|
||||
return {error: 'could not fetch channel member'};
|
||||
}
|
||||
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
if (!(team instanceof Model)) {
|
||||
modelPromises.push(...prepareMyTeams(operator, [team], [(myTeam as TeamMembership)]));
|
||||
} else if (!(myTeam instanceof Model)) {
|
||||
const mt: MyTeam[] = [{
|
||||
id: myTeam.team_id,
|
||||
roles: myTeam.roles,
|
||||
}];
|
||||
modelPromises.push(
|
||||
operator.handleMyTeam({myTeams: mt, prepareRecordsOnly: true}),
|
||||
operator.handleTeamMemberships({teamMemberships: [myTeam], prepareRecordsOnly: true}),
|
||||
);
|
||||
}
|
||||
|
||||
// We are checking both, so this may become an issue
|
||||
if (!(myChannel instanceof Model) && !(channel instanceof Model)) {
|
||||
modelPromises.push(...await prepareMyChannelsForTeam(operator, team.id, [channel], [myChannel]));
|
||||
}
|
||||
|
||||
let teamId;
|
||||
if (team.id !== system.currentTeamId) {
|
||||
teamId = team.id;
|
||||
}
|
||||
|
||||
let channelId;
|
||||
if (channel.id !== system.currentChannelId) {
|
||||
channelId = channel.id;
|
||||
}
|
||||
|
||||
if (modelPromises.length) {
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
fetchMyChannelsForTeam(serverUrl, teamId, true, 0, false, true);
|
||||
}
|
||||
|
||||
if (teamId || channelId) {
|
||||
await switchToChannelById(serverUrl, channel.id, team.id);
|
||||
}
|
||||
|
||||
if (roles.length) {
|
||||
fetchRolesIfNeeded(serverUrl, roles);
|
||||
}
|
||||
|
||||
switchToChannelById(serverUrl, channelId, teamId);
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
onError(joinedTeam, teamId);
|
||||
errorHandler(intl);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function goToNPSChannel(serverUrl: string) {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await client.getUserByUsername(General.NPS_PLUGIN_BOT_USERNAME);
|
||||
const {data, error} = await createDirectChannel(serverUrl, user.id);
|
||||
if (error || !data) {
|
||||
throw error || new Error('channel not found');
|
||||
}
|
||||
await switchToChannelById(serverUrl, data.id, data.team_id);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export async function createDirectChannel(serverUrl: string, userId: string, displayName = '') {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
@@ -777,9 +833,8 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
|
||||
created.display_name = displayName;
|
||||
} else {
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const system = await getCommonSystemValues(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], system.config, system.license);
|
||||
const {directChannels, users} = await fetchMissingDirectChannelsInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
|
||||
created.display_name = directChannels?.[0].display_name || created.display_name;
|
||||
if (users?.length) {
|
||||
@@ -895,16 +950,19 @@ export async function fetchArchivedChannels(serverUrl: string, teamId: string, p
|
||||
}
|
||||
|
||||
export async function createGroupChannel(serverUrl: string, userIds: 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 {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const currentUser = await getCurrentUser(operator.database);
|
||||
if (!currentUser) {
|
||||
return {error: 'Cannot get the current user'};
|
||||
}
|
||||
@@ -918,10 +976,9 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
|
||||
return {data: created};
|
||||
}
|
||||
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const preferences = await queryPreferencesByCategoryAndName(operator.database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const system = await getCommonSystemValues(operator.database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], system.config, system.license);
|
||||
const {directChannels, users} = await fetchMissingDirectChannelsInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
|
||||
|
||||
const member = {
|
||||
@@ -1057,10 +1114,6 @@ export async function switchToChannelById(serverUrl: string, channelId: string,
|
||||
|
||||
DeviceEventEmitter.emit(Events.CHANNEL_SWITCH, false);
|
||||
|
||||
if (await AppsManager.isAppsEnabled(serverUrl)) {
|
||||
AppsManager.fetchBindings(serverUrl, channelId);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1094,7 +1147,7 @@ export async function switchToLastChannel(serverUrl: string, teamId?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchChannels(serverUrl: string, term: string, teamId: string, isSearch = false) {
|
||||
export async function searchChannels(serverUrl: string, term: string, isSearch = false) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -1108,8 +1161,9 @@ export async function searchChannels(serverUrl: string, term: string, teamId: st
|
||||
}
|
||||
|
||||
try {
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const autoCompleteFunc = isSearch ? client.autocompleteChannelsForSearch : client.autocompleteChannels;
|
||||
const channels = await autoCompleteFunc(teamId, term);
|
||||
const channels = await autoCompleteFunc(currentTeamId, term);
|
||||
return {channels};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
@@ -1264,7 +1318,7 @@ export const unarchiveChannel = async (serverUrl: string, channelId: string) =>
|
||||
try {
|
||||
EphemeralStore.addArchivingChannel(channelId);
|
||||
await client.unarchiveChannel(channelId);
|
||||
await setChannelDeleteAt(serverUrl, channelId, 0);
|
||||
await setChannelDeleteAt(serverUrl, channelId, Date.now());
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
@@ -1308,45 +1362,3 @@ export const convertChannelToPrivate = async (serverUrl: string, channelId: stri
|
||||
EphemeralStore.removeConvertingChannel(channelId);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleKickFromChannel = async (serverUrl: string, channelId: string, event: string = Events.LEAVE_CHANNEL) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
if (currentChannelId !== channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentServer = await getActiveServer();
|
||||
if (currentServer?.url === serverUrl) {
|
||||
const channel = await getChannelById(database, channelId);
|
||||
DeviceEventEmitter.emit(event, channel?.displayName);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
}
|
||||
|
||||
const tabletDevice = await isTablet();
|
||||
|
||||
if (tabletDevice) {
|
||||
const teamId = await getCurrentTeamId(database);
|
||||
await removeChannelFromTeamHistory(operator, teamId, channelId);
|
||||
const newChannelId = await getNthLastChannelFromTeam(database, teamId, 0, channelId);
|
||||
if (newChannelId) {
|
||||
if (currentServer?.url === serverUrl) {
|
||||
if (newChannelId === Screens.GLOBAL_THREADS) {
|
||||
await switchToGlobalThreads(serverUrl, teamId, false);
|
||||
} else {
|
||||
await switchToChannelById(serverUrl, newChannelId, teamId, true);
|
||||
}
|
||||
} else {
|
||||
await setCurrentTeamAndChannelId(operator, teamId, channelId);
|
||||
}
|
||||
} // TODO else jump to "join a channel" screen https://mattermost.atlassian.net/browse/MM-41051
|
||||
} else {
|
||||
await setCurrentChannelId(operator, '');
|
||||
}
|
||||
} catch (error) {
|
||||
logDebug('cannot kick user from channel', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,21 +4,25 @@
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {doAppSubmit, postEphemeralCallResponseForCommandArgs} from '@actions/remote/apps';
|
||||
import {showPermalink} from '@actions/remote/permalink';
|
||||
import {Client} from '@client/rest';
|
||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||
import {AppCallResponseTypes} from '@constants/apps';
|
||||
import DeepLinkType from '@constants/deep_linking';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getConfig, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {showAppForm} from '@screens/navigation';
|
||||
import {handleDeepLink, matchDeepLink} from '@utils/deep_link';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
import {getTeammateNameDisplay, queryUsersByUsername} from '@queries/servers/user';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import * as DraftUtils from '@utils/draft';
|
||||
import {matchDeepLink, tryOpenURL} from '@utils/url';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string): Promise<{data?: CommandResponse; error?: string | {message: string}}> => {
|
||||
import {makeDirectChannel, switchToChannelById, switchToChannelByName} from './channel';
|
||||
|
||||
import type {DeepLinkChannel, DeepLinkPermalink, DeepLinkDM, DeepLinkGM, DeepLinkPlugin} from '@typings/launch';
|
||||
|
||||
export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -31,6 +35,14 @@ export const executeCommand = async (serverUrl: string, intl: IntlShape, message
|
||||
return {error: error as ClientErrorProps};
|
||||
}
|
||||
|
||||
// const config = await queryConfig(operator.database)
|
||||
// if (config.FeatureFlagAppsEnabled) {
|
||||
// const parser = new AppCommandParser(serverUrl, intl, channelId, rootId);
|
||||
// if (parser.isAppCommand(msg)) {
|
||||
// return executeAppCommand(serverUrl, intl, parser);
|
||||
// }
|
||||
// }
|
||||
|
||||
const channel = await getChannelById(operator.database, channelId);
|
||||
const teamId = channel?.teamId || (await getCurrentTeamId(operator.database));
|
||||
|
||||
@@ -41,14 +53,6 @@ export const executeCommand = async (serverUrl: string, intl: IntlShape, message
|
||||
parent_id: rootId,
|
||||
};
|
||||
|
||||
const appsEnabled = await AppsManager.isAppsEnabled(serverUrl);
|
||||
if (appsEnabled) {
|
||||
const parser = new AppCommandParser(serverUrl, intl, channelId, teamId, rootId);
|
||||
if (parser.isAppCommand(message)) {
|
||||
return executeAppCommand(serverUrl, intl, parser, message, args);
|
||||
}
|
||||
}
|
||||
|
||||
let msg = filterEmDashForCommand(message);
|
||||
|
||||
let cmdLength = msg.indexOf(' ');
|
||||
@@ -77,51 +81,44 @@ export const executeCommand = async (serverUrl: string, intl: IntlShape, message
|
||||
return {data};
|
||||
};
|
||||
|
||||
const executeAppCommand = async (serverUrl: string, intl: IntlShape, parser: AppCommandParser, msg: string, args: CommandArgs) => {
|
||||
const {creq, errorMessage} = await parser.composeCommandSubmitCall(msg);
|
||||
const createErrorMessage = (errMessage: string) => {
|
||||
return {error: {message: errMessage}};
|
||||
};
|
||||
// TODO https://mattermost.atlassian.net/browse/MM-41234
|
||||
// const executeAppCommand = (serverUrl: string, intl: IntlShape, parser: any) => {
|
||||
// const {call, errorMessage} = await parser.composeCallFromCommand(msg);
|
||||
// const createErrorMessage = (errMessage: string) => {
|
||||
// return {error: {message: errMessage}};
|
||||
// };
|
||||
|
||||
if (!creq) {
|
||||
return createErrorMessage(errorMessage!);
|
||||
}
|
||||
// if (!call) {
|
||||
// return createErrorMessage(errorMessage!);
|
||||
// }
|
||||
|
||||
const res = await doAppSubmit(serverUrl, creq, intl);
|
||||
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 as AppCallResponse;
|
||||
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.text) {
|
||||
postEphemeralCallResponseForCommandArgs(serverUrl, callResp, callResp.text, args);
|
||||
}
|
||||
return {data: {}};
|
||||
case AppCallResponseTypes.FORM:
|
||||
if (callResp.form) {
|
||||
showAppForm(callResp.form, creq.context);
|
||||
}
|
||||
return {data: {}};
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
if (callResp.navigate_to_url) {
|
||||
handleGotoLocation(serverUrl, intl, callResp.navigate_to_url);
|
||||
}
|
||||
return {data: {}};
|
||||
default:
|
||||
return createErrorMessage(intl.formatMessage({
|
||||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {
|
||||
type: callResp.type,
|
||||
}));
|
||||
}
|
||||
};
|
||||
// const res = await dispatch(doAppCall(call, AppCallTypes.SUBMIT, intl));
|
||||
// if (res.error) {
|
||||
// const errorResponse = res.error as AppCallResponse;
|
||||
// return createErrorMessage(errorResponse.error || intl.formatMessage({
|
||||
// id: 'apps.error.unknown',
|
||||
// defaultMessage: 'Unknown error.',
|
||||
// }));
|
||||
// }
|
||||
// const callResp = res.data as AppCallResponse;
|
||||
// switch (callResp.type) {
|
||||
// case AppCallResponseTypes.OK:
|
||||
// if (callResp.markdown) {
|
||||
// dispatch(postEphemeralCallResponseForCommandArgs(callResp, callResp.markdown, args));
|
||||
// }
|
||||
// return {data: {}};
|
||||
// case AppCallResponseTypes.FORM:
|
||||
// case AppCallResponseTypes.NAVIGATE:
|
||||
// return {data: {}};
|
||||
// default:
|
||||
// return createErrorMessage(intl.formatMessage({
|
||||
// id: 'apps.error.responses.unknown_type',
|
||||
// defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
// }, {
|
||||
// type: callResp.type,
|
||||
// }));
|
||||
// }
|
||||
// };
|
||||
|
||||
const filterEmDashForCommand = (command: string): string => {
|
||||
return command.replace(/\u2014/g, '--');
|
||||
@@ -136,9 +133,60 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc
|
||||
|
||||
const config = await getConfig(database);
|
||||
const match = matchDeepLink(location, serverUrl, config?.SiteURL);
|
||||
let linkServerUrl: string | undefined;
|
||||
if (match?.data?.serverUrl) {
|
||||
linkServerUrl = DatabaseManager.searchUrl(match.data.serverUrl);
|
||||
}
|
||||
|
||||
if (match) {
|
||||
handleDeepLink(match, intl, location);
|
||||
if (match && linkServerUrl) {
|
||||
switch (match.type) {
|
||||
case DeepLinkType.Channel: {
|
||||
const data = match.data as DeepLinkChannel;
|
||||
switchToChannelByName(linkServerUrl, data.channelName, data.teamName, DraftUtils.errorBadChannel, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.Permalink: {
|
||||
const data = match.data as DeepLinkPermalink;
|
||||
showPermalink(linkServerUrl, data.teamName, data.postId, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.DirectMessage: {
|
||||
const data = match.data as DeepLinkDM;
|
||||
if (!data.userName) {
|
||||
DraftUtils.errorUnkownUser(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
if (data.serverUrl !== serverUrl) {
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
}
|
||||
const user = (await queryUsersByUsername(database, [data.userName]).fetch())[0];
|
||||
if (!user) {
|
||||
DraftUtils.errorUnkownUser(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
makeDirectChannel(linkServerUrl, user.id, displayUsername(user, intl.locale, await getTeammateNameDisplay(database)), true);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.GroupMessage: {
|
||||
const data = match.data as DeepLinkGM;
|
||||
if (!data.channelId) {
|
||||
DraftUtils.errorBadChannel(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
switchToChannelById(linkServerUrl, data.channelId);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.Plugin: {
|
||||
const data = match.data as DeepLinkPlugin;
|
||||
showModal('PluginInternal', data.id, {link: location});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const {formatMessage} = intl;
|
||||
const onError = () => Alert.alert(
|
||||
|
||||
@@ -87,17 +87,10 @@ const debouncedFetchEmojiByNames = debounce(async (serverUrl: string) => {
|
||||
promises.push(client.getCustomEmojiByName(name));
|
||||
}
|
||||
|
||||
const emojis = await Promise.all(promises);
|
||||
|
||||
try {
|
||||
const emojisResult = await Promise.allSettled(promises);
|
||||
const emojis = emojisResult.reduce<CustomEmoji[]>((result, e) => {
|
||||
if (e.status === 'fulfilled') {
|
||||
result.push(e.value);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
if (emojis.length) {
|
||||
await operator.handleCustomEmojis({emojis, prepareRecordsOnly: false});
|
||||
}
|
||||
await operator.handleCustomEmojis({emojis, prepareRecordsOnly: false});
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {setLastServerVersionCheck} from '@actions/local/systems';
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, getCurrentChannelId, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getCurrentChannelId} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {deleteV1Data} from '@utils/file';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logInfo} from '@utils/log';
|
||||
|
||||
import {handleEntryAfterLoadNavigation, registerDeviceToken, syncOtherServers, verifyPushProxy} from './common';
|
||||
import {registerDeviceToken, syncOtherServers, verifyPushProxy} from './common';
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
|
||||
export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) {
|
||||
@@ -22,54 +21,52 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
||||
|
||||
if (!since) {
|
||||
registerDeviceToken(serverUrl);
|
||||
if (Object.keys(DatabaseManager.serverDatabases).length === 1) {
|
||||
await setLastServerVersionCheck(serverUrl, true);
|
||||
}
|
||||
}
|
||||
|
||||
// clear lastUnreadChannelId
|
||||
const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''});
|
||||
if (removeLastUnreadChannelId) {
|
||||
await operator.batchRecords(removeLastUnreadChannelId);
|
||||
operator.batchRecords(removeLastUnreadChannelId);
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const tabletDevice = await isTablet();
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
const lastDisconnectedAt = (await getWebSocketLastDisconnected(database)) || since;
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, currentTeamId, currentChannelId, since);
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
return {error: entryData.error};
|
||||
}
|
||||
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData} = entryData;
|
||||
if (isUpgrade && meData?.user) {
|
||||
const isTabletDevice = await isTablet();
|
||||
const me = await prepareCommonSystemValues(operator, {
|
||||
currentUserId: meData.user.id,
|
||||
currentTeamId: initialTeamId,
|
||||
currentChannelId: isTabletDevice ? initialChannelId : undefined,
|
||||
});
|
||||
const me = await prepareCommonSystemValues(operator, {currentUserId: meData.user.id});
|
||||
if (me?.length) {
|
||||
await operator.batchRecords(me);
|
||||
}
|
||||
}
|
||||
|
||||
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId, currentChannelId, initialTeamId, initialChannelId);
|
||||
let switchToChannel = false;
|
||||
|
||||
// Immediately set the new team as the current team in the database so that the UI
|
||||
// renders the correct team.
|
||||
if (tabletDevice && initialChannelId) {
|
||||
switchToChannel = true;
|
||||
switchToChannelById(serverUrl, initialChannelId, initialTeamId);
|
||||
} else {
|
||||
setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
|
||||
}
|
||||
|
||||
const dt = Date.now();
|
||||
await operator.batchRecords(models);
|
||||
logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = meData?.user || (await getCurrentUser(database))!;
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
|
||||
|
||||
if (!since) {
|
||||
// Load data from other servers
|
||||
@@ -90,8 +87,8 @@ export async function upgradeEntry(serverUrl: string) {
|
||||
const error = configAndLicense.error || entryData.error;
|
||||
|
||||
if (!error) {
|
||||
await DatabaseManager.updateServerIdentifier(serverUrl, configAndLicense.config!.DiagnosticId);
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
DatabaseManager.updateServerIdentifier(serverUrl, configAndLicense.config!.DiagnosticId);
|
||||
DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
deleteV1Data();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
|
||||
import {Database, Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {fetchAllTeams, fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, MyTeamsRequest} from '@actions/remote/team';
|
||||
import {fetchNewThreads} from '@actions/remote/thread';
|
||||
import {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlAllChannels} from '@client/graphQL/entry';
|
||||
import {General, Preferences, Screens} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
@@ -22,17 +22,14 @@ import {selectDefaultTeam} from '@helpers/api/team';
|
||||
import {DEFAULT_LOCALE} from '@i18n';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {getAllServers} from '@queries/app/servers';
|
||||
import {queryAllServers} from '@queries/app/servers';
|
||||
import {prepareMyChannelsForTeam, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getConfig, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
|
||||
import {getMemberChannelsFromGQLQuery, gqlToClientChannelMembership} from '@utils/graphql';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
@@ -68,12 +65,6 @@ export type EntryResponse = {
|
||||
const FETCH_MISSING_DM_TIMEOUT = 2500;
|
||||
export const FETCH_UNREADS_TIMEOUT = 2500;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
export const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[]) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
@@ -148,7 +139,7 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
|
||||
const confReq = await fetchConfigAndLicense(serverUrl);
|
||||
const prefData = await fetchMyPreferences(serverUrl, fetchOnly);
|
||||
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, confReq.config?.CollapsedThreads, confReq.config?.FeatureFlagCollapsedThreads, confReq.config?.Version));
|
||||
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, confReq.config));
|
||||
if (prefData.preferences) {
|
||||
const crtToggled = await getHasCRTChanged(database, prefData.preferences);
|
||||
if (crtToggled) {
|
||||
@@ -171,6 +162,7 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
fetchMe(serverUrl, fetchOnly),
|
||||
];
|
||||
|
||||
const removeTeamIds: string[] = [];
|
||||
const resolution = await Promise.all(promises);
|
||||
const [teamData, , meData] = resolution;
|
||||
let [, chData] = resolution;
|
||||
@@ -187,7 +179,10 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
}
|
||||
}
|
||||
|
||||
const removeTeamIds = await getRemoveTeamIds(database, teamData);
|
||||
const removedFromTeam = teamData.memberships?.filter((m) => m.delete_at > 0);
|
||||
if (removedFromTeam?.length) {
|
||||
removeTeamIds.push(...removedFromTeam.map((m) => m.team_id));
|
||||
}
|
||||
|
||||
let data: AppEntryData = {
|
||||
initialTeamId,
|
||||
@@ -200,6 +195,10 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
};
|
||||
|
||||
if (teamData.teams?.length === 0 && !teamData.error) {
|
||||
// User is no longer a member of any team
|
||||
const myTeams = await queryMyTeams(database).fetch();
|
||||
removeTeamIds.push(...(myTeams.map((myTeam) => myTeam.id) || []));
|
||||
|
||||
return {
|
||||
...data,
|
||||
initialTeamId: '',
|
||||
@@ -309,7 +308,7 @@ export async function entryInitialChannelId(database: Database, requestedChannel
|
||||
|
||||
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,
|
||||
config: ClientConfig, license: ClientLicense, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
initialTeamId?: string, initialChannelId?: string) {
|
||||
// defer sidebar DM & GM profiles
|
||||
let channelsToFetchProfiles: Set<Channel>|undefined;
|
||||
@@ -319,7 +318,7 @@ export async function restDeferredAppEntryActions(
|
||||
channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, true);
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
|
||||
@@ -328,22 +327,22 @@ export async function restDeferredAppEntryActions(
|
||||
fetchTeamsChannelsAndUnreadPosts(serverUrl, since, teamData.teams, teamData.memberships, initialTeamId);
|
||||
}
|
||||
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
|
||||
if (preferences && processIsCRTEnabled(preferences, config)) {
|
||||
if (initialTeamId) {
|
||||
await syncTeamThreads(serverUrl, initialTeamId);
|
||||
await fetchNewThreads(serverUrl, initialTeamId, false);
|
||||
}
|
||||
|
||||
if (teamData.teams?.length) {
|
||||
for await (const team of teamData.teams) {
|
||||
if (team.id !== initialTeamId) {
|
||||
// need to await here since GM/DM threads in different teams overlap
|
||||
await syncTeamThreads(serverUrl, team.id);
|
||||
await fetchNewThreads(serverUrl, team.id, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCanJoinTeams(serverUrl);
|
||||
await fetchAllTeams(serverUrl);
|
||||
await updateAllUsersSince(serverUrl, since);
|
||||
|
||||
// Fetch groups for current user
|
||||
@@ -351,7 +350,7 @@ export async function restDeferredAppEntryActions(
|
||||
|
||||
setTimeout(async () => {
|
||||
if (channelsToFetchProfiles?.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config, license);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId);
|
||||
}
|
||||
}, FETCH_MISSING_DM_TIMEOUT);
|
||||
@@ -365,21 +364,26 @@ export const registerDeviceToken = async (serverUrl: string) => {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const deviceToken = await getDeviceToken();
|
||||
if (deviceToken) {
|
||||
client.attachDevice(deviceToken);
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (appDatabase) {
|
||||
const deviceToken = await getDeviceToken(appDatabase);
|
||||
if (deviceToken) {
|
||||
client.attachDevice(deviceToken);
|
||||
}
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
};
|
||||
|
||||
export const syncOtherServers = async (serverUrl: string) => {
|
||||
const servers = await getAllServers();
|
||||
for (const server of servers) {
|
||||
if (server.url !== serverUrl && server.lastActiveAt > 0) {
|
||||
registerDeviceToken(server.url);
|
||||
syncAllChannelMembersAndThreads(server.url);
|
||||
autoUpdateTimezone(server.url);
|
||||
const database = DatabaseManager.appDatabase?.database;
|
||||
if (database) {
|
||||
const servers = await queryAllServers(database);
|
||||
for (const server of servers) {
|
||||
if (server.url !== serverUrl && server.lastActiveAt > 0) {
|
||||
registerDeviceToken(server.url);
|
||||
syncAllChannelMembersAndThreads(server.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -409,8 +413,6 @@ const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
return 'Server database not found';
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const response = await gqlAllChannels(serverUrl);
|
||||
if ('error' in response) {
|
||||
return response.error;
|
||||
@@ -420,7 +422,7 @@ const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
return response.errors[0].message;
|
||||
}
|
||||
|
||||
const userId = await getCurrentUserId(database);
|
||||
const userId = await getCurrentUserId(operator.database);
|
||||
|
||||
const channels = getMemberChannelsFromGQLQuery(response.data);
|
||||
const memberships = response.data.channelMembers?.map((m) => gqlToClientChannelMembership(m, userId));
|
||||
@@ -429,16 +431,7 @@ const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
const modelPromises = await prepareMyChannelsForTeam(operator, '', channels, memberships, undefined, true);
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
}
|
||||
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
if (isCRTEnabled) {
|
||||
const myTeams = await queryMyTeams(operator.database).fetch();
|
||||
for await (const myTeam of myTeams) {
|
||||
// need to await here since GM/DM threads in different teams overlap
|
||||
await syncTeamThreads(serverUrl, myTeam.id);
|
||||
operator.batchRecords(models);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,12 +452,11 @@ const restSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
const config = await client.getClientConfigOld();
|
||||
|
||||
let excludeDirect = false;
|
||||
for await (const myTeam of myTeams) {
|
||||
for (const myTeam of myTeams) {
|
||||
fetchMyChannelsForTeam(serverUrl, myTeam.id, false, 0, false, excludeDirect);
|
||||
excludeDirect = true;
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
|
||||
// need to await here since GM/DM threads in different teams overlap
|
||||
await syncTeamThreads(serverUrl, myTeam.id);
|
||||
if (preferences && processIsCRTEnabled(preferences, config)) {
|
||||
fetchNewThreads(serverUrl, myTeam.id, false);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -488,7 +480,12 @@ export async function verifyPushProxy(serverUrl: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceId = await getDeviceToken();
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceId = await getDeviceToken(appDatabase);
|
||||
if (!deviceId) {
|
||||
return;
|
||||
}
|
||||
@@ -516,50 +513,3 @@ export async function verifyPushProxy(serverUrl: string) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleEntryAfterLoadNavigation(
|
||||
serverUrl: string,
|
||||
teamMembers: TeamMembership[],
|
||||
channelMembers: ChannelMember[],
|
||||
currentTeamId: string,
|
||||
currentChannelId: string,
|
||||
initialTeamId: string,
|
||||
initialChannelId: string,
|
||||
) {
|
||||
try {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const currentTeamIdAfterLoad = await getCurrentTeamId(database);
|
||||
const currentChannelIdAfterLoad = await getCurrentChannelId(database);
|
||||
const mountedScreens = NavigationStore.getScreensInStack();
|
||||
const isChannelScreenMounted = mountedScreens.includes(Screens.CHANNEL);
|
||||
const isThreadsMounted = mountedScreens.includes(Screens.THREAD);
|
||||
const tabletDevice = await isTablet();
|
||||
|
||||
if (currentTeamIdAfterLoad !== currentTeamId) {
|
||||
// Switched teams while loading
|
||||
if (!teamMembers.find((t) => t.team_id === currentTeamIdAfterLoad && t.delete_at === 0)) {
|
||||
await handleKickFromTeam(serverUrl, currentTeamIdAfterLoad);
|
||||
}
|
||||
} else if (currentTeamIdAfterLoad !== initialTeamId) {
|
||||
await handleKickFromTeam(serverUrl, currentTeamIdAfterLoad);
|
||||
} else if (currentChannelIdAfterLoad !== currentChannelId) {
|
||||
// Switched channels while loading
|
||||
if (!channelMembers.find((m) => m.channel_id === currentChannelIdAfterLoad)) {
|
||||
if (tabletDevice || isChannelScreenMounted || isThreadsMounted) {
|
||||
await handleKickFromChannel(serverUrl, currentChannelIdAfterLoad);
|
||||
} else {
|
||||
await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
|
||||
}
|
||||
}
|
||||
} else if (currentChannelIdAfterLoad && currentChannelIdAfterLoad !== initialChannelId) {
|
||||
if (tabletDevice || isChannelScreenMounted || isThreadsMounted) {
|
||||
await handleKickFromChannel(serverUrl, currentChannelIdAfterLoad);
|
||||
} else {
|
||||
await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logDebug('could not manage the entry after load navigation', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import {storeConfigAndLicense} from '@actions/local/systems';
|
||||
import {MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {MyTeamsRequest} from '@actions/remote/team';
|
||||
import {fetchNewThreads} from '@actions/remote/thread';
|
||||
import {updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry';
|
||||
import {Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
@@ -19,14 +19,16 @@ import {queryAllChannels, queryAllChannelsForTeam} from '@queries/servers/channe
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig} from '@queries/servers/system';
|
||||
import {queryMyTeams} from '@queries/servers/team';
|
||||
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 {teamsToRemove, FETCH_UNREADS_TIMEOUT, entryRest, EntryResponse, entryInitialChannelId, restDeferredAppEntryActions} from './common';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
export async function deferredAppEntryGraphQLActions(
|
||||
serverUrl: string,
|
||||
@@ -48,20 +50,20 @@ export async function deferredAppEntryGraphQLActions(
|
||||
setTimeout(() => {
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, true);
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
|
||||
if (preferences && processIsCRTEnabled(preferences, config)) {
|
||||
if (initialTeamId) {
|
||||
await syncTeamThreads(serverUrl, initialTeamId);
|
||||
await fetchNewThreads(serverUrl, initialTeamId, false);
|
||||
}
|
||||
|
||||
if (teamData.teams?.length) {
|
||||
for await (const team of teamData.teams) {
|
||||
if (team.id !== initialTeamId) {
|
||||
// need to await here since GM/DM threads in different teams overlap
|
||||
await syncTeamThreads(serverUrl, team.id);
|
||||
await fetchNewThreads(serverUrl, team.id, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,7 +97,6 @@ export async function deferredAppEntryGraphQLActions(
|
||||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, currentUserId);
|
||||
|
||||
updateCanJoinTeams(serverUrl);
|
||||
updateAllUsersSince(serverUrl, since);
|
||||
|
||||
return {error: undefined};
|
||||
@@ -181,22 +182,9 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
|
||||
user: gqlToClientUser(fetchedData.user!),
|
||||
};
|
||||
|
||||
const allTeams = getMemberTeamsFromGQLQuery(fetchedData);
|
||||
const allTeamMemberships = fetchedData.teamMembers.map((m) => gqlToClientTeamMembership(m, meData.user.id));
|
||||
|
||||
const [nonArchivedTeams, archivedTeamIds] = allTeams.reduce((acc, t) => {
|
||||
if (t.delete_at) {
|
||||
acc[1].add(t.id);
|
||||
return acc;
|
||||
}
|
||||
return [[...acc[0], t], acc[1]];
|
||||
}, [[], new Set<string>()]);
|
||||
|
||||
const nonArchivedTeamMemberships = allTeamMemberships.filter((m) => !archivedTeamIds.has(m.team_id));
|
||||
|
||||
const teamData = {
|
||||
teams: nonArchivedTeams,
|
||||
memberships: nonArchivedTeamMemberships,
|
||||
teams: getMemberTeamsFromGQLQuery(fetchedData),
|
||||
memberships: fetchedData.teamMembers.map((m) => gqlToClientTeamMembership(m, meData.user.id)),
|
||||
};
|
||||
|
||||
const prefData = {
|
||||
@@ -216,7 +204,7 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
|
||||
let initialTeamId = currentTeamId;
|
||||
if (!teamData.teams.length) {
|
||||
initialTeamId = '';
|
||||
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId && t.delete_at === 0)) {
|
||||
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId)) {
|
||||
const teamOrderPreference = getPreferenceValue(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
|
||||
initialTeamId = selectDefaultTeam(teamData.teams, meData.user.locale, teamOrderPreference, config.ExperimentalPrimaryTeam)?.id || '';
|
||||
}
|
||||
@@ -239,9 +227,23 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
|
||||
const roles = filterAndTransformRoles(gqlRoles);
|
||||
|
||||
const initialChannelId = await entryInitialChannelId(database, currentChannelId, currentTeamId, initialTeamId, meData.user.id, chData?.channels, chData?.memberships);
|
||||
let removeTeams: TeamModel[] = [];
|
||||
const removeChannels = await getRemoveChannels(database, chData, initialTeamId, true);
|
||||
const removeTeamIds = await getRemoveTeamIds(database, teamData);
|
||||
const removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
|
||||
|
||||
const removeTeamIds = [];
|
||||
|
||||
const removedFromTeam = teamData.memberships?.filter((m) => m.delete_at > 0);
|
||||
if (removedFromTeam?.length) {
|
||||
removeTeamIds.push(...removedFromTeam.map((m) => m.team_id));
|
||||
}
|
||||
|
||||
if (teamData.teams?.length === 0) {
|
||||
// User is no longer a member of any team
|
||||
const myTeams = await queryMyTeams(database).fetch();
|
||||
removeTeamIds.push(...(myTeams?.map((myTeam) => myTeam.id) || []));
|
||||
}
|
||||
|
||||
removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
|
||||
|
||||
const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData}, true);
|
||||
if (roles.length) {
|
||||
@@ -270,7 +272,7 @@ export const entry = async (serverUrl: string, teamId?: string, channelId?: stri
|
||||
|
||||
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,
|
||||
config: ClientConfig, license: ClientLicense, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
initialTeamId?: string, initialChannelId?: string) {
|
||||
let result;
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
@@ -283,7 +285,5 @@ export async function deferredAppEntryActions(
|
||||
result = restDeferredAppEntryActions(serverUrl, since, currentUserId, currentUserLocale, preferences, config, license, teamData, chData, initialTeamId, initialChannelId);
|
||||
}
|
||||
|
||||
autoUpdateTimezone(serverUrl);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
@@ -48,11 +47,9 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs)
|
||||
return {error: clData.error};
|
||||
}
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, '', '');
|
||||
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
return {error: entryData.error};
|
||||
}
|
||||
|
||||
@@ -69,7 +66,6 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs)
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
const config = clData.config || {} as ClientConfig;
|
||||
const license = clData.license || {} as ClientLicense;
|
||||
|
||||
@@ -8,13 +8,12 @@ import {getDefaultThemeByAppearance} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getMyChannel} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getConfig, getCurrentTeamId, getLicense, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme';
|
||||
@@ -82,10 +81,8 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
switchedToScreen = true;
|
||||
}
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, teamId, channelId);
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
return {error: entryData.error};
|
||||
}
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
@@ -137,11 +134,9 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
const {config, license} = await getCommonSystemValues(operator.database);
|
||||
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId);
|
||||
|
||||
|
||||
@@ -27,7 +27,12 @@ async function getDeviceIdForPing(serverUrl: string, checkDeviceId: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
return getDeviceToken();
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return getDeviceToken(appDatabase);
|
||||
}
|
||||
|
||||
// Default timeout interval for ping is 5 seconds
|
||||
@@ -119,4 +124,3 @@ export const getRedirectLocation = async (serverUrl: string, link: string) => {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -11,10 +11,9 @@ import {fetchMyTeam} from '@actions/remote/team';
|
||||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getMyChannel, getChannelById} from '@queries/servers/channel';
|
||||
import {getCurrentTeamId, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {getCommonSystemValues, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
|
||||
const fetchNotificationData = async (serverUrl: string, notification: NotificationWithData, skipEvents = false) => {
|
||||
@@ -31,14 +30,14 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const system = await getCommonSystemValues(database);
|
||||
let teamId = notification.payload?.team_id;
|
||||
let isDirectChannel = false;
|
||||
|
||||
if (!teamId) {
|
||||
// If the notification payload does not have a teamId we assume is a DM/GM
|
||||
isDirectChannel = true;
|
||||
teamId = currentTeamId;
|
||||
teamId = system.currentTeamId;
|
||||
}
|
||||
|
||||
// To make the switch faster we determine if we already have the team & channel
|
||||
@@ -123,7 +122,6 @@ export const openNotification = async (serverUrl: string, notification: Notifica
|
||||
}
|
||||
|
||||
try {
|
||||
EphemeralStore.setNotificationTapped(true);
|
||||
const {database} = operator;
|
||||
const channelId = notification.payload!.channel_id!;
|
||||
const rootId = notification.payload!.root_id!;
|
||||
@@ -131,13 +129,13 @@ export const openNotification = async (serverUrl: string, notification: Notifica
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(rootId);
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const system = await getCommonSystemValues(database);
|
||||
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
let teamId = notification.payload?.team_id;
|
||||
|
||||
if (!teamId) {
|
||||
// If the notification payload does not have a teamId we assume is a DM/GM
|
||||
teamId = currentTeamId;
|
||||
teamId = system.currentTeamId;
|
||||
}
|
||||
|
||||
if (currentServerUrl !== serverUrl) {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General} from '@constants';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
export const isNPSEnabled = async (serverUrl: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const manifests = await client.getPluginsManifests();
|
||||
for (const v of manifests) {
|
||||
if (v.id === General.NPS_PLUGIN_ID) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const giveFeedbackAction = async (serverUrl: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const post = await client.npsGiveFeedbackAction();
|
||||
return {post};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
@@ -1,14 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeepLink} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getCurrentTeam} from '@queries/servers/team';
|
||||
import {displayPermalink} from '@utils/permalink';
|
||||
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
export const showPermalink = async (serverUrl: string, teamName: string, postId: string, openAsPermalink = true) => {
|
||||
export const showPermalink = async (serverUrl: string, teamName: string, postId: string, intl: IntlShape, openAsPermalink = true) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -17,7 +18,7 @@ export const showPermalink = async (serverUrl: string, teamName: string, postId:
|
||||
try {
|
||||
let name = teamName;
|
||||
let team: TeamModel | undefined;
|
||||
if (!name || name === DeepLink.Redirect) {
|
||||
if (!name || name === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
|
||||
team = await getCurrentTeam(database);
|
||||
if (team) {
|
||||
name = team.name;
|
||||
|
||||
@@ -23,9 +23,7 @@ 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 {setFetchingThreadState} from '@store/fetching_thread_store';
|
||||
import {getValidEmojis, matchEmoticons} from '@utils/emoji/helpers';
|
||||
import {isServerError} from '@utils/errors';
|
||||
import {logError} from '@utils/log';
|
||||
import {processPostsFetched} from '@utils/post';
|
||||
import {getPostIdsForCombinedUserActivityPost} from '@utils/post_list';
|
||||
@@ -135,7 +133,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
||||
let created;
|
||||
try {
|
||||
created = await client.createPost(newPost);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
const errorPost = {
|
||||
...newPost,
|
||||
id: pendingPostId,
|
||||
@@ -148,11 +146,10 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
||||
|
||||
// If the failure was because: the root post was deleted or
|
||||
// TownSquareIsReadOnly=true then remove the post
|
||||
if (isServerError(error) && (
|
||||
error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||
if (error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||
error.server_error_id === ServerErrors.TOWN_SQUARE_READ_ONLY_ERROR ||
|
||||
error.server_error_id === ServerErrors.PLUGIN_DISMISSED_POST_ERROR
|
||||
)) {
|
||||
) {
|
||||
await removePost(serverUrl, databasePost);
|
||||
} else {
|
||||
const models = await operator.handlePosts({
|
||||
@@ -254,12 +251,11 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
|
||||
}
|
||||
}
|
||||
await operator.batchRecords(models);
|
||||
} catch (error) {
|
||||
if (isServerError(error) && (
|
||||
error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||
} catch (error: any) {
|
||||
if (error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||
error.server_error_id === ServerErrors.TOWN_SQUARE_READ_ONLY_ERROR ||
|
||||
error.server_error_id === ServerErrors.PLUGIN_DISMISSED_POST_ERROR
|
||||
)) {
|
||||
) {
|
||||
await removePost(serverUrl, post);
|
||||
} else {
|
||||
post.prepareUpdate((p) => {
|
||||
@@ -329,9 +325,12 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string) => {
|
||||
export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string, emitEvent = false) => {
|
||||
try {
|
||||
const promises = [];
|
||||
if (emitEvent) {
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, true);
|
||||
}
|
||||
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) {
|
||||
@@ -339,7 +338,13 @@ export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: C
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
if (emitEvent) {
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, false);
|
||||
}
|
||||
} catch (error) {
|
||||
if (emitEvent) {
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, false);
|
||||
}
|
||||
return {error};
|
||||
}
|
||||
|
||||
@@ -436,7 +441,7 @@ export async function fetchPostsBefore(serverUrl: string, channelId: string, pos
|
||||
|
||||
await operator.batchRecords(models);
|
||||
} catch (error) {
|
||||
logError('FETCH POSTS BEFORE ERROR', error);
|
||||
logError('FETCH AUTHORS ERROR', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,15 +549,9 @@ export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOn
|
||||
}
|
||||
|
||||
if (promises.length) {
|
||||
const authorsResult = await Promise.allSettled(promises);
|
||||
const result = authorsResult.reduce<UserProfile[][]>((acc, item) => {
|
||||
if (item.status === 'fulfilled') {
|
||||
acc.push(item.value);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const result = await Promise.all(promises);
|
||||
const authors = result.flat();
|
||||
|
||||
if (!fetchOnly && authors.length) {
|
||||
await operator.handleUsers({
|
||||
users: authors,
|
||||
@@ -584,8 +583,6 @@ export async function fetchPostThread(serverUrl: string, postId: string, options
|
||||
return {error};
|
||||
}
|
||||
|
||||
setFetchingThreadState(postId, true);
|
||||
|
||||
try {
|
||||
const isCRTEnabled = await getIsCRTEnabled(operator.database);
|
||||
|
||||
@@ -623,11 +620,9 @@ export async function fetchPostThread(serverUrl: string, postId: string, options
|
||||
}
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
setFetchingThreadState(postId, false);
|
||||
return {posts: extractRecordsForTable<PostModel>(posts, MM_TABLES.SERVER.POST)};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
setFetchingThreadState(postId, false);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
@@ -796,7 +791,7 @@ export async function fetchPostById(serverUrl: string, postId: string, fetchOnly
|
||||
if (authors?.length) {
|
||||
const users = await operator.handleUsers({
|
||||
users: authors,
|
||||
prepareRecordsOnly: true,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
models.push(...users);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export const saveFavoriteChannel = async (serverUrl: string, channelId: string,
|
||||
}
|
||||
|
||||
try {
|
||||
// Todo: @shaz I think you'll need to add the category handler here so that the channel is added/removed from the favorites category
|
||||
const userId = await getCurrentUserId(operator.database);
|
||||
const favPref: PreferenceType = {
|
||||
category: CATEGORY_FAVORITE_CHANNEL,
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {storeConfig} from '@actions/local/systems';
|
||||
import {Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPreferenceValue, getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import {selectDefaultTeam} from '@helpers/api/team';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||
import {prepareCategories, prepareCategoryChannels} from '@queries/servers/categories';
|
||||
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
|
||||
import {prepareMyPreferences, queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, getCommonSystemValues} from '@queries/servers/system';
|
||||
import {prepareMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {isDMorGM, selectDefaultChannelForTeam} from '@utils/channel';
|
||||
@@ -102,14 +101,14 @@ export async function retryInitialTeamAndChannel(serverUrl: string) {
|
||||
|
||||
const models: Model[] = (await Promise.all([
|
||||
prepareMyPreferences(operator, prefData.preferences!),
|
||||
storeConfig(serverUrl, clData.config, true),
|
||||
...prepareMyTeams(operator, teamData.teams!, teamData.memberships!),
|
||||
...await prepareMyChannelsForTeam(operator, initialTeam.id, chData!.channels!, chData!.memberships!),
|
||||
prepareCategoriesAndCategoriesChannels(operator, chData!.categories!, true),
|
||||
|
||||
prepareCategories(operator, chData!.categories!),
|
||||
prepareCategoryChannels(operator, chData!.categories!),
|
||||
prepareCommonSystemValues(
|
||||
operator,
|
||||
{
|
||||
config: clData.config!,
|
||||
license: clData.license!,
|
||||
currentTeamId: initialTeam?.id,
|
||||
currentChannelId: initialChannel?.id,
|
||||
@@ -122,7 +121,7 @@ export async function retryInitialTeamAndChannel(serverUrl: string) {
|
||||
const directChannels = chData!.channels!.filter(isDMorGM);
|
||||
const channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
if (channelsToFetchProfiles.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(prefData.preferences || [], clData.config?.LockTeammateNameDisplay, clData.config?.TeammateNameDisplay, clData.license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(prefData.preferences || [], clData.config, clData.license);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), user.locale, teammateDisplayNameSetting, user.id);
|
||||
}
|
||||
|
||||
@@ -164,8 +163,7 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) {
|
||||
user_id: p.userId,
|
||||
value: p.value,
|
||||
}));
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
|
||||
// fetch channels / channel membership for initial team
|
||||
const chData = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
|
||||
@@ -192,7 +190,8 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) {
|
||||
|
||||
const models: Model[] = (await Promise.all([
|
||||
...await prepareMyChannelsForTeam(operator, teamId, chData!.channels!, chData!.memberships!),
|
||||
prepareCategoriesAndCategoriesChannels(operator, chData!.categories!, true),
|
||||
prepareCategories(operator, chData!.categories!),
|
||||
prepareCategoryChannels(operator, chData!.categories!),
|
||||
prepareCommonSystemValues(operator, {currentChannelId: initialChannel?.id}),
|
||||
])).flat();
|
||||
|
||||
@@ -201,7 +200,7 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) {
|
||||
const directChannels = chData!.channels!.filter(isDMorGM);
|
||||
const channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
if (channelsToFetchProfiles.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config, license);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), user.locale, teammateDisplayNameSetting, user.id);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,36 +12,43 @@ import PushNotifications from '@init/push_notifications';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {getServerDisplayName} from '@queries/app/servers';
|
||||
import {getCurrentUserId, getExpiredSession, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {queryServerName} from '@queries/app/servers';
|
||||
import {getCurrentUserId, getCommonSystemValues, getExpiredSession} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {logWarning, logError} from '@utils/log';
|
||||
import {scheduleExpiredNotification} from '@utils/notification';
|
||||
import {getCSRFFromCookie} from '@utils/security';
|
||||
import {getDeviceTimezone, isTimezoneEnabled} from '@utils/timezone';
|
||||
|
||||
import {loginEntry} from './entry';
|
||||
import {fetchDataRetentionPolicy} from './systems';
|
||||
import {autoUpdateTimezone} from './user';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type {LoginArgs} from '@typings/database/database';
|
||||
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
export const completeLogin = async (serverUrl: string) => {
|
||||
export const completeLogin = async (serverUrl: string, user: UserProfile) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const {config, license}: { config: Partial<ClientConfig>; license: Partial<ClientLicense> } = await getCommonSystemValues(database);
|
||||
|
||||
if (!Object.keys(config)?.length || !license || !Object.keys(license)?.length) {
|
||||
if (!Object.keys(config)?.length || !Object.keys(license)?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set timezone
|
||||
if (isTimezoneEnabled(config)) {
|
||||
const timezone = getDeviceTimezone();
|
||||
await autoUpdateTimezone(serverUrl, {deviceTimezone: timezone, userId: user.id});
|
||||
}
|
||||
|
||||
// Data retention
|
||||
if (config?.DataRetentionEnableMessageDeletion === 'true' && license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
fetchDataRetentionPolicy(serverUrl);
|
||||
@@ -124,7 +131,7 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
|
||||
}
|
||||
|
||||
try {
|
||||
deviceToken = await getDeviceToken();
|
||||
deviceToken = await getDeviceToken(appDatabase);
|
||||
user = await client.login(
|
||||
loginId,
|
||||
password,
|
||||
@@ -158,7 +165,7 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
|
||||
|
||||
try {
|
||||
const {error, hasTeams, time} = await loginEntry({serverUrl, user});
|
||||
completeLogin(serverUrl);
|
||||
completeLogin(serverUrl, user);
|
||||
return {error: error as ClientError, failed: false, hasTeams, time};
|
||||
} catch (error) {
|
||||
return {error: error as ClientError, failed: false, time: 0};
|
||||
@@ -204,10 +211,11 @@ export const cancelSessionNotification = async (serverUrl: string) => {
|
||||
|
||||
export const scheduleSessionNotification = async (serverUrl: string) => {
|
||||
try {
|
||||
const {database: appDatabase} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const sessions = await fetchSessions(serverUrl, 'me');
|
||||
const user = await getCurrentUser(database);
|
||||
const serverName = await getServerDisplayName(serverUrl);
|
||||
const serverName = await queryServerName(appDatabase, serverUrl);
|
||||
|
||||
await cancelSessionNotification(serverUrl);
|
||||
|
||||
@@ -285,7 +293,7 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
|
||||
displayName: serverDisplayName,
|
||||
},
|
||||
});
|
||||
deviceToken = await getDeviceToken();
|
||||
deviceToken = await getDeviceToken(database);
|
||||
user = await client.getMe();
|
||||
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
||||
await server?.operator.handleSystem({
|
||||
@@ -301,7 +309,7 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
|
||||
|
||||
try {
|
||||
const {error, hasTeams, time} = await loginEntry({serverUrl, user, deviceToken});
|
||||
completeLogin(serverUrl);
|
||||
completeLogin(serverUrl, user);
|
||||
return {error: error as ClientError, failed: false, hasTeams, time};
|
||||
} catch (error) {
|
||||
return {error: error as ClientError, failed: false, time: 0};
|
||||
@@ -311,8 +319,9 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
|
||||
async function findSession(serverUrl: string, sessions: Session[]) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {database: appDatabase} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
const expiredSession = await getExpiredSession(database);
|
||||
const deviceToken = await getDeviceToken();
|
||||
const deviceToken = await getDeviceToken(appDatabase);
|
||||
|
||||
// First try and find the session by the given identifier hyqddef7jjdktqiyy36gxa8sqy
|
||||
let session = sessions.find((s) => s.id === expiredSession?.id);
|
||||
|
||||
@@ -63,7 +63,7 @@ export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false
|
||||
]);
|
||||
|
||||
if (!fetchOnly) {
|
||||
await storeConfigAndLicense(serverUrl, config, license);
|
||||
storeConfigAndLicense(serverUrl, config, license);
|
||||
}
|
||||
|
||||
return {config, license};
|
||||
|
||||
@@ -5,21 +5,15 @@ import {Model} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {removeUserFromTeam as localRemoveUserFromTeam} from '@actions/local/team';
|
||||
import {Client} from '@client/rest';
|
||||
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
||||
import {Events} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getActiveServerUrl} from '@queries/app/servers';
|
||||
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||
import {prepareCategories, prepareCategoryChannels} from '@queries/servers/categories';
|
||||
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 {dismissAllModals, popToRoot} from '@screens/navigation';
|
||||
import {addTeamToTeamHistory, prepareDeleteTeam, prepareMyTeams, getNthLastChannelFromTeam, queryTeamsById, syncTeamTable} from '@queries/servers/team';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import {fetchMyChannelsForTeam, switchToChannelById} from './channel';
|
||||
import {fetchGroupsForTeamIfConstrained} from './groups';
|
||||
@@ -59,16 +53,11 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
||||
return {error};
|
||||
}
|
||||
|
||||
let loadEventSent = false;
|
||||
try {
|
||||
EphemeralStore.startAddingToTeam(teamId);
|
||||
const team = await client.getTeam(teamId);
|
||||
const member = await client.addToTeam(teamId, userId);
|
||||
|
||||
if (!fetchOnly) {
|
||||
setTeamLoading(serverUrl, true);
|
||||
loadEventSent = true;
|
||||
|
||||
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
|
||||
const {channels, memberships: channelMembers, categories} = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -79,16 +68,14 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
||||
}];
|
||||
|
||||
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),
|
||||
prepareCategories(operator, categories || []),
|
||||
prepareCategoryChannels(operator, categories || []),
|
||||
])).flat();
|
||||
|
||||
await operator.batchRecords(models);
|
||||
setTeamLoading(serverUrl, false);
|
||||
loadEventSent = false;
|
||||
|
||||
if (await isTablet()) {
|
||||
const channel = await getDefaultChannelForTeam(operator.database, teamId);
|
||||
@@ -96,18 +83,11 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
||||
fetchPostsForChannel(serverUrl, channel.id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setTeamLoading(serverUrl, false);
|
||||
loadEventSent = false;
|
||||
}
|
||||
}
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
updateCanJoinTeams(serverUrl);
|
||||
return {member};
|
||||
} catch (error) {
|
||||
if (loadEventSent) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
@@ -198,82 +178,25 @@ export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly =
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchAllTeams = async (serverUrl: string, page = 0, perPage = PER_PAGE_DEFAULT): Promise<{teams?: Team[]; error?: any}> => {
|
||||
export const fetchAllTeams = async (serverUrl: string, fetchOnly = false): Promise<MyTeamsRequest> => {
|
||||
let client;
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const teams = await client.getTeams(page, perPage);
|
||||
return {teams};
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
const recCanJoinTeams = async (client: Client, myTeamsIds: Set<string>, page: number): Promise<boolean> => {
|
||||
const fetchedTeams = await client.getTeams(page, PER_PAGE_DEFAULT);
|
||||
if (fetchedTeams.find((t) => !myTeamsIds.has(t.id) && t.delete_at === 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fetchedTeams.length === PER_PAGE_DEFAULT) {
|
||||
return recCanJoinTeams(client, myTeamsIds, page + 1);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const LOAD_MORE_THRESHOLD = 10;
|
||||
export async function fetchTeamsForComponent(
|
||||
serverUrl: string,
|
||||
page: number,
|
||||
joinedIds?: Set<string>,
|
||||
alreadyLoaded: Team[] = [],
|
||||
): Promise<{teams: Team[]; hasMore: boolean; page: number}> {
|
||||
let hasMore = true;
|
||||
const {teams, error} = await fetchAllTeams(serverUrl, page, PER_PAGE_DEFAULT);
|
||||
if (error || !teams || teams.length < PER_PAGE_DEFAULT) {
|
||||
hasMore = false;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return {teams: alreadyLoaded, hasMore, page};
|
||||
}
|
||||
|
||||
if (teams?.length) {
|
||||
const notJoinedTeams = joinedIds ? teams.filter((t) => !joinedIds.has(t.id)) : teams;
|
||||
alreadyLoaded.push(...notJoinedTeams);
|
||||
|
||||
if (teams.length < PER_PAGE_DEFAULT) {
|
||||
hasMore = false;
|
||||
}
|
||||
|
||||
if (
|
||||
hasMore &&
|
||||
(alreadyLoaded.length > LOAD_MORE_THRESHOLD)
|
||||
) {
|
||||
return fetchTeamsForComponent(serverUrl, page + 1, joinedIds, alreadyLoaded);
|
||||
}
|
||||
|
||||
return {teams: alreadyLoaded, hasMore, page: page + 1};
|
||||
}
|
||||
|
||||
return {teams: alreadyLoaded, hasMore: false, page};
|
||||
}
|
||||
|
||||
export const updateCanJoinTeams = async (serverUrl: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const teams = await client.getTeams();
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
syncTeamTable(operator, teams);
|
||||
}
|
||||
}
|
||||
|
||||
const myTeams = await queryMyTeams(database).fetch();
|
||||
const myTeamsIds = new Set(myTeams.map((m) => m.id));
|
||||
|
||||
const canJoin = await recCanJoinTeams(client, myTeamsIds, 0);
|
||||
|
||||
EphemeralStore.setCanJoinOtherTeams(serverUrl, canJoin);
|
||||
return {};
|
||||
return {teams};
|
||||
} catch (error) {
|
||||
EphemeralStore.setCanJoinOtherTeams(serverUrl, false);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
@@ -325,16 +248,6 @@ export async function fetchTeamByName(serverUrl: string, teamName: string, fetch
|
||||
}
|
||||
}
|
||||
|
||||
export const removeCurrentUserFromTeam = async (serverUrl: string, teamId: string, fetchOnly = false) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const userId = await getCurrentUserId(database);
|
||||
return removeUserFromTeam(serverUrl, teamId, userId, fetchOnly);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const removeUserFromTeam = async (serverUrl: string, teamId: string, userId: string, fetchOnly = false) => {
|
||||
let client;
|
||||
try {
|
||||
@@ -348,7 +261,7 @@ export const removeUserFromTeam = async (serverUrl: string, teamId: string, user
|
||||
|
||||
if (!fetchOnly) {
|
||||
localRemoveUserFromTeam(serverUrl, teamId);
|
||||
updateCanJoinTeams(serverUrl);
|
||||
fetchAllTeams(serverUrl);
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
@@ -400,31 +313,3 @@ export async function handleTeamChange(serverUrl: string, teamId: string) {
|
||||
// Fetch Groups + GroupTeams
|
||||
fetchGroupsForTeamIfConstrained(serverUrl, teamId);
|
||||
}
|
||||
|
||||
export async function handleKickFromTeam(serverUrl: string, teamId: string) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
if (currentTeamId !== teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentServer = await getActiveServerUrl();
|
||||
if (currentServer === serverUrl) {
|
||||
const team = await getTeamById(database, teamId);
|
||||
DeviceEventEmitter.emit(Events.LEAVE_TEAM, team?.displayName);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
}
|
||||
|
||||
await removeTeamFromTeamHistory(operator, teamId);
|
||||
const teamToJumpTo = await getLastTeam(database, teamId);
|
||||
if (teamToJumpTo) {
|
||||
await handleTeamChange(serverUrl, teamToJumpTo);
|
||||
}
|
||||
|
||||
// Resetting to team select handled by the home screen
|
||||
} catch (error) {
|
||||
logDebug('Failed to kick user from team', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
|
||||
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 terms = await client.getTermsOfService();
|
||||
return {terms};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTermsOfServiceStatus(serverUrl: string, id: string, status: boolean): Promise<{resp?: {status: string}; error?: any}> {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const resp = await client.updateMyTermsOfServiceStatus(id, status);
|
||||
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
if (currentUser) {
|
||||
currentUser.prepareUpdate((u) => {
|
||||
if (status) {
|
||||
u.termsOfServiceCreateAt = Date.now();
|
||||
u.termsOfServiceId = id;
|
||||
} else {
|
||||
u.termsOfServiceCreateAt = 0;
|
||||
u.termsOfServiceId = '';
|
||||
}
|
||||
});
|
||||
operator.batchRecords([currentUser]);
|
||||
}
|
||||
return {resp};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,27 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {markTeamThreadsAsRead, markThreadAsViewed, processReceivedThreads, switchToThread, updateTeamThreadsSync, updateThread} from '@actions/local/thread';
|
||||
import {markTeamThreadsAsRead, markThreadAsViewed, processReceivedThreads, switchToThread, updateThread} from '@actions/local/thread';
|
||||
import {fetchPostThread} from '@actions/remote/post';
|
||||
import {General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getPostById} from '@queries/servers/post';
|
||||
import {getConfigValue, getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getIsCRTEnabled, getThreadById, getTeamThreadsSyncData} from '@queries/servers/thread';
|
||||
import {getCommonSystemValues, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getIsCRTEnabled, getNewestThreadInTeam, getThreadById} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {getThreadsListEdges} from '@utils/thread';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
|
||||
type FetchThreadsRequest = {
|
||||
error?: unknown;
|
||||
} | {
|
||||
data: GetUserThreadsResponse;
|
||||
};
|
||||
|
||||
type FetchThreadsOptions = {
|
||||
before?: string;
|
||||
@@ -30,11 +33,6 @@ type FetchThreadsOptions = {
|
||||
totalsOnly?: boolean;
|
||||
};
|
||||
|
||||
enum Direction {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string, isFromNotification = false) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
@@ -58,21 +56,59 @@ export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string,
|
||||
|
||||
await switchToThread(serverUrl, rootId, isFromNotification);
|
||||
|
||||
if (await AppsManager.isAppsEnabled(serverUrl)) {
|
||||
// Getting the post again in case we didn't had it at the beginning
|
||||
const post = await getPostById(database, rootId);
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
return {};
|
||||
};
|
||||
|
||||
if (post) {
|
||||
if (currentChannelId === post?.channelId) {
|
||||
AppsManager.copyMainBindingsToThread(serverUrl, currentChannelId);
|
||||
} else {
|
||||
AppsManager.fetchBindings(serverUrl, post.channelId, true);
|
||||
}
|
||||
}
|
||||
export const fetchThreads = async (
|
||||
serverUrl: string,
|
||||
teamId: string,
|
||||
{
|
||||
before,
|
||||
after,
|
||||
perPage = General.CRT_CHUNK_SIZE,
|
||||
deleted = false,
|
||||
unread = false,
|
||||
since,
|
||||
}: FetchThreadsOptions = {
|
||||
perPage: General.CRT_CHUNK_SIZE,
|
||||
deleted: false,
|
||||
unread: false,
|
||||
since: 0,
|
||||
},
|
||||
): Promise<FetchThreadsRequest> => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
return {};
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const {config} = await getCommonSystemValues(database);
|
||||
|
||||
const data = await client.getThreads('me', teamId, before, after, perPage, deleted, unread, since, false, config.Version);
|
||||
|
||||
const {threads} = data;
|
||||
|
||||
if (threads.length) {
|
||||
// Mark all fetched threads as following
|
||||
threads.forEach((thread: Thread) => {
|
||||
thread.is_following = true;
|
||||
});
|
||||
|
||||
await processReceivedThreads(serverUrl, threads, teamId, !unread, false);
|
||||
}
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchThread = async (serverUrl: string, teamId: string, threadId: string, extended?: boolean) => {
|
||||
@@ -86,7 +122,7 @@ export const fetchThread = async (serverUrl: string, teamId: string, threadId: s
|
||||
try {
|
||||
const thread = await client.getThread('me', teamId, threadId, extended);
|
||||
|
||||
await processReceivedThreads(serverUrl, [thread], teamId);
|
||||
await processReceivedThreads(serverUrl, [thread], teamId, false, false);
|
||||
|
||||
return {data: thread};
|
||||
} catch (error) {
|
||||
@@ -236,13 +272,17 @@ export const updateThreadFollowing = async (serverUrl: string, teamId: string, t
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchThreads = async (
|
||||
enum Direction {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
async function fetchBatchThreads(
|
||||
serverUrl: string,
|
||||
teamId: string,
|
||||
options: FetchThreadsOptions,
|
||||
direction?: Direction,
|
||||
pages?: number,
|
||||
) => {
|
||||
): Promise<{error: unknown; data?: Thread[]}> {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
|
||||
if (!operator) {
|
||||
@@ -256,40 +296,47 @@ export const fetchThreads = async (
|
||||
return {error};
|
||||
}
|
||||
|
||||
const fetchDirection = direction ?? Direction.Up;
|
||||
// if we start from the begging of time (since = 0) we need to fetch threads from newest to oldest (Direction.Down)
|
||||
// if there is another point in time, we need to fetch threads from oldest to newest (Direction.Up)
|
||||
let direction = Direction.Up;
|
||||
if (options.since === 0) {
|
||||
direction = Direction.Down;
|
||||
}
|
||||
|
||||
const currentUser = await getCurrentUser(operator.database);
|
||||
if (!currentUser) {
|
||||
return {error: 'currentUser not found'};
|
||||
}
|
||||
|
||||
const version = await getConfigValue(operator.database, 'Version');
|
||||
const threadsData: Thread[] = [];
|
||||
const {config} = await getCommonSystemValues(operator.database);
|
||||
const data: Thread[] = [];
|
||||
|
||||
let currentPage = 0;
|
||||
const fetchThreadsFunc = async (opts: FetchThreadsOptions) => {
|
||||
let page = 0;
|
||||
const {before, after, perPage = General.CRT_CHUNK_SIZE, deleted, unread, since} = opts;
|
||||
|
||||
currentPage++;
|
||||
const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, version);
|
||||
page += 1;
|
||||
const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, config.Version);
|
||||
if (threads.length) {
|
||||
// Mark all fetched threads as following
|
||||
for (const thread of threads) {
|
||||
thread.is_following = thread.is_following ?? true;
|
||||
thread.is_following = true;
|
||||
}
|
||||
|
||||
threadsData.push(...threads);
|
||||
data.push(...threads);
|
||||
|
||||
if (threads.length === perPage && (pages == null || currentPage < pages!)) {
|
||||
if (threads.length === perPage) {
|
||||
const newOptions: FetchThreadsOptions = {perPage, deleted, unread};
|
||||
if (fetchDirection === Direction.Down) {
|
||||
if (direction === Direction.Down) {
|
||||
const last = threads[threads.length - 1];
|
||||
newOptions.before = last.id;
|
||||
} else {
|
||||
const first = threads[0];
|
||||
newOptions.after = first.id;
|
||||
}
|
||||
await fetchThreadsFunc(newOptions);
|
||||
if (pages != null && page < pages) {
|
||||
fetchThreadsFunc(newOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -300,179 +347,140 @@ export const fetchThreads = async (
|
||||
if (__DEV__) {
|
||||
throw error;
|
||||
}
|
||||
return {error};
|
||||
|
||||
return {error, data};
|
||||
}
|
||||
|
||||
return {error: false, threads: threadsData};
|
||||
};
|
||||
return {error: false, data};
|
||||
}
|
||||
|
||||
export async function fetchNewThreads(
|
||||
serverUrl: string,
|
||||
teamId: string,
|
||||
prepareRecordsOnly = false,
|
||||
): Promise<{error: unknown; models?: Model[]}> {
|
||||
const options: FetchThreadsOptions = {
|
||||
unread: false,
|
||||
deleted: true,
|
||||
perPage: 60,
|
||||
};
|
||||
|
||||
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 syncData = await getTeamThreadsSyncData(operator.database, teamId);
|
||||
const syncDataUpdate = {
|
||||
id: teamId,
|
||||
} as TeamThreadsSync;
|
||||
const newestThread = await getNewestThreadInTeam(operator.database, teamId, false);
|
||||
options.since = newestThread ? newestThread.lastReplyAt : 0;
|
||||
|
||||
const threads: Thread[] = [];
|
||||
let response: {
|
||||
error: unknown;
|
||||
data?: Thread[];
|
||||
} = {
|
||||
error: undefined,
|
||||
data: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* If Syncing for the first time,
|
||||
* - Get all unread threads to show the right badges
|
||||
* - Get latest threads to show by default in the global threads screen
|
||||
* Else
|
||||
* - Get all threads since last sync
|
||||
*/
|
||||
if (!syncData || !syncData?.latest) {
|
||||
const [allUnreadThreads, latestThreads] = await Promise.all([
|
||||
fetchThreads(
|
||||
serverUrl,
|
||||
teamId,
|
||||
{unread: true},
|
||||
Direction.Down,
|
||||
),
|
||||
fetchThreads(
|
||||
serverUrl,
|
||||
teamId,
|
||||
{},
|
||||
undefined,
|
||||
1,
|
||||
),
|
||||
]);
|
||||
if (allUnreadThreads.error || latestThreads.error) {
|
||||
return {error: allUnreadThreads.error || latestThreads.error};
|
||||
}
|
||||
if (latestThreads.threads?.length) {
|
||||
// We are fetching the threads for the first time. We get "latest" and "earliest" values.
|
||||
const {earliestThread, latestThread} = getThreadsListEdges(latestThreads.threads);
|
||||
syncDataUpdate.latest = latestThread.last_reply_at;
|
||||
syncDataUpdate.earliest = earliestThread.last_reply_at;
|
||||
let loadedInGlobalThreads = true;
|
||||
|
||||
threads.push(...latestThreads.threads);
|
||||
}
|
||||
if (allUnreadThreads.threads?.length) {
|
||||
threads.push(...allUnreadThreads.threads);
|
||||
}
|
||||
} else {
|
||||
const allNewThreads = await fetchThreads(
|
||||
serverUrl,
|
||||
teamId,
|
||||
{deleted: true, since: syncData.latest},
|
||||
);
|
||||
if (allNewThreads.error) {
|
||||
return {error: allNewThreads.error};
|
||||
}
|
||||
if (allNewThreads.threads?.length) {
|
||||
// As we are syncing, we get all new threads and we will update the "latest" value.
|
||||
const {latestThread} = getThreadsListEdges(allNewThreads.threads);
|
||||
syncDataUpdate.latest = latestThread.last_reply_at;
|
||||
|
||||
threads.push(...allNewThreads.threads);
|
||||
}
|
||||
}
|
||||
|
||||
const models: Model[] = [];
|
||||
|
||||
if (threads.length) {
|
||||
const {error, models: threadModels = []} = await processReceivedThreads(serverUrl, threads, teamId, true);
|
||||
if (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
if (threadModels?.length) {
|
||||
models.push(...threadModels);
|
||||
}
|
||||
|
||||
if (syncDataUpdate.earliest || syncDataUpdate.latest) {
|
||||
const {models: updateModels} = await updateTeamThreadsSync(serverUrl, syncDataUpdate, true);
|
||||
if (updateModels?.length) {
|
||||
models.push(...updateModels);
|
||||
}
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly && models?.length) {
|
||||
try {
|
||||
await operator.batchRecords(models);
|
||||
} catch (err) {
|
||||
if (__DEV__) {
|
||||
throw err;
|
||||
}
|
||||
return {error: err};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {error: false, models};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
// if we have no threads in the DB fetch all unread ones
|
||||
if (options.since === 0) {
|
||||
// options to fetch all unread threads
|
||||
options.deleted = false;
|
||||
options.unread = true;
|
||||
loadedInGlobalThreads = false;
|
||||
}
|
||||
};
|
||||
|
||||
export const loadEarlierThreads = async (serverUrl: string, teamId: string, lastThreadId: string, prepareRecordsOnly = false) => {
|
||||
response = await fetchBatchThreads(serverUrl, teamId, options);
|
||||
|
||||
const {error: nErr, data} = response;
|
||||
|
||||
if (nErr) {
|
||||
return {error: nErr};
|
||||
}
|
||||
|
||||
if (!data?.length) {
|
||||
return {error: false, models: []};
|
||||
}
|
||||
|
||||
const {error, models} = await processReceivedThreads(serverUrl, data, teamId, loadedInGlobalThreads, true);
|
||||
|
||||
if (!error && !prepareRecordsOnly && models?.length) {
|
||||
try {
|
||||
await operator.batchRecords(models);
|
||||
} catch (err) {
|
||||
if (__DEV__) {
|
||||
throw err;
|
||||
}
|
||||
return {error: true};
|
||||
}
|
||||
}
|
||||
|
||||
return {error: false, models};
|
||||
}
|
||||
|
||||
export async function fetchRefreshThreads(
|
||||
serverUrl: string,
|
||||
teamId: string,
|
||||
unread = false,
|
||||
prepareRecordsOnly = false,
|
||||
): Promise<{error: unknown; models?: Model[]}> {
|
||||
const options: FetchThreadsOptions = {
|
||||
unread,
|
||||
deleted: true,
|
||||
perPage: 60,
|
||||
};
|
||||
|
||||
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
|
||||
*/
|
||||
const fetchedThreads = await fetchThreads(
|
||||
serverUrl,
|
||||
teamId,
|
||||
{
|
||||
before: lastThreadId,
|
||||
},
|
||||
undefined,
|
||||
1,
|
||||
);
|
||||
if (fetchedThreads.error) {
|
||||
return {error: fetchedThreads.error};
|
||||
}
|
||||
const newestThread = await getNewestThreadInTeam(operator.database, teamId, unread);
|
||||
options.since = newestThread ? newestThread.lastReplyAt : 0;
|
||||
|
||||
const models: Model[] = [];
|
||||
const threads = fetchedThreads.threads || [];
|
||||
let response: {
|
||||
error: unknown;
|
||||
data?: Thread[];
|
||||
} = {
|
||||
error: undefined,
|
||||
data: [],
|
||||
};
|
||||
|
||||
if (threads?.length) {
|
||||
const {error, models: threadModels = []} = await processReceivedThreads(serverUrl, threads, teamId, true);
|
||||
if (error) {
|
||||
return {error};
|
||||
}
|
||||
let pages;
|
||||
|
||||
if (threadModels?.length) {
|
||||
models.push(...threadModels);
|
||||
}
|
||||
|
||||
const {earliestThread} = getThreadsListEdges(threads);
|
||||
const syncDataUpdate = {
|
||||
id: teamId,
|
||||
earliest: earliestThread.last_reply_at,
|
||||
} as TeamThreadsSync;
|
||||
const {models: updateModels} = await updateTeamThreadsSync(serverUrl, syncDataUpdate, true);
|
||||
if (updateModels?.length) {
|
||||
models.push(...updateModels);
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly && models?.length) {
|
||||
try {
|
||||
await operator.batchRecords(models);
|
||||
} catch (err) {
|
||||
if (__DEV__) {
|
||||
throw err;
|
||||
}
|
||||
return {error: err};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {error: false, models, threads};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
// in the case of global threads: if we have no threads in the DB fetch just one page
|
||||
if (options.since === 0 && !unread) {
|
||||
pages = 1;
|
||||
}
|
||||
};
|
||||
|
||||
response = await fetchBatchThreads(serverUrl, teamId, options, pages);
|
||||
|
||||
const {error: nErr, data} = response;
|
||||
|
||||
if (nErr) {
|
||||
return {error: nErr};
|
||||
}
|
||||
|
||||
if (!data?.length) {
|
||||
return {error: false, models: []};
|
||||
}
|
||||
|
||||
const loadedInGlobalThreads = !unread;
|
||||
const {error, models} = await processReceivedThreads(serverUrl, data, teamId, loadedInGlobalThreads, true);
|
||||
|
||||
if (!error && !prepareRecordsOnly && models?.length) {
|
||||
try {
|
||||
await operator.batchRecords(models);
|
||||
} catch (err) {
|
||||
if (__DEV__) {
|
||||
throw err;
|
||||
}
|
||||
return {error: true};
|
||||
}
|
||||
}
|
||||
|
||||
return {error: false, models};
|
||||
}
|
||||
|
||||
@@ -15,10 +15,9 @@ import {debounce} from '@helpers/api/general';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getMembersCountByChannelsId, queryChannelsByTypes} from '@queries/servers/channel';
|
||||
import {queryGroupsByNames} from '@queries/servers/group';
|
||||
import {getConfig, getCurrentUserId} from '@queries/servers/system';
|
||||
import {getCurrentUser, prepareUsers, queryAllUsers, queryUsersById, queryUsersByIdsOrUsernames, queryUsersByUsername} from '@queries/servers/user';
|
||||
import {getCurrentTeamId, getCurrentUserId} from '@queries/servers/system';
|
||||
import {getCurrentUser, getUserById, prepareUsers, queryAllUsers, queryUsersById, queryUsersByIdsOrUsernames, queryUsersByUsername} from '@queries/servers/user';
|
||||
import {logError} from '@utils/log';
|
||||
import {getDeviceTimezone, isTimezoneEnabled} from '@utils/timezone';
|
||||
import {getUserTimezoneProps, removeUserFromList} from '@utils/user';
|
||||
|
||||
import {fetchGroupsByNames} from './groups';
|
||||
@@ -407,7 +406,7 @@ export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetc
|
||||
return {users: [], existingUsers};
|
||||
}
|
||||
const users = await client.getProfilesByIds([...new Set(usersToLoad)]);
|
||||
if (!fetchOnly && users.length) {
|
||||
if (!fetchOnly) {
|
||||
await operator.handleUsers({
|
||||
users,
|
||||
prepareRecordsOnly: false,
|
||||
@@ -451,7 +450,7 @@ export const fetchUsersByUsernames = async (serverUrl: string, usernames: string
|
||||
|
||||
const users = await client.getProfilesByUsernames([...new Set(usersToLoad)]);
|
||||
|
||||
if (users.length && !fetchOnly) {
|
||||
if (!fetchOnly) {
|
||||
await operator.handleUsers({
|
||||
users,
|
||||
prepareRecordsOnly: false,
|
||||
@@ -818,7 +817,7 @@ export const uploadUserProfileImage = async (serverUrl: string, localPath: strin
|
||||
return {error: undefined};
|
||||
};
|
||||
|
||||
export const searchUsers = async (serverUrl: string, term: string, teamId: string, channelId?: string) => {
|
||||
export const searchUsers = async (serverUrl: string, term: string, channelId?: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -832,7 +831,8 @@ export const searchUsers = async (serverUrl: string, term: string, teamId: strin
|
||||
}
|
||||
|
||||
try {
|
||||
const users = await client.autocompleteUsers(term, teamId, channelId);
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const users = await client.autocompleteUsers(term, currentTeamId, channelId);
|
||||
return {users};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
@@ -850,7 +850,7 @@ export const buildProfileImageUrl = (serverUrl: string, userId: string, timestam
|
||||
return client.getProfilePictureUrl(userId, timestamp);
|
||||
};
|
||||
|
||||
export const autoUpdateTimezone = async (serverUrl: string) => {
|
||||
export const autoUpdateTimezone = async (serverUrl: string, {deviceTimezone, userId}: {deviceTimezone: string; userId: string}) => {
|
||||
let database;
|
||||
try {
|
||||
const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
@@ -859,16 +859,12 @@ export const autoUpdateTimezone = async (serverUrl: string) => {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const config = await getConfig(database);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const currentUser = await getUserById(database, userId);
|
||||
|
||||
if (!currentUser || !config || !isTimezoneEnabled(config)) {
|
||||
if (!currentUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set timezone
|
||||
const deviceTimezone = getDeviceTimezone();
|
||||
|
||||
const currentTimezone = getUserTimezoneProps(currentUser);
|
||||
const newTimezoneExists = currentTimezone.automaticTimezone !== deviceTimezone;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {addChannelToDefaultCategory} from '@actions/local/category';
|
||||
import {
|
||||
@@ -9,18 +10,22 @@ import {
|
||||
storeMyChannelsForTeam, updateChannelInfoFromChannel, updateMyChannelFromWebsocket,
|
||||
} from '@actions/local/channel';
|
||||
import {storePostsForChannel} from '@actions/local/post';
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannel, fetchChannelStats, fetchChannelById, handleKickFromChannel} from '@actions/remote/channel';
|
||||
import {switchToGlobalThreads} from '@actions/local/thread';
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannel, fetchChannelStats, fetchChannelById, switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchPostsForChannel} from '@actions/remote/post';
|
||||
import {fetchRolesIfNeeded} from '@actions/remote/role';
|
||||
import {fetchUsersByIds, updateUsersNoLongerVisible} from '@actions/remote/user';
|
||||
import {loadCallForChannel} from '@calls/actions/calls';
|
||||
import {Events} from '@constants';
|
||||
import {Events, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryActiveServer} from '@queries/app/servers';
|
||||
import {deleteChannelMembership, getChannelById, prepareMyChannelsForTeam, getCurrentChannel} from '@queries/servers/channel';
|
||||
import {getConfig, getCurrentChannelId} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, getConfig, setCurrentChannelId, getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getNthLastChannelFromTeam} from '@queries/servers/team';
|
||||
import {getCurrentUser, getTeammateNameDisplay, getUserById} from '@queries/servers/user';
|
||||
import {dismissAllModals, popToRoot} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
|
||||
// Received when current user created a channel in a different client
|
||||
export async function handleChannelCreatedEvent(serverUrl: string, msg: any) {
|
||||
@@ -128,7 +133,7 @@ export async function handleChannelViewedEvent(serverUrl: string, msg: any) {
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
|
||||
if (activeServerUrl !== serverUrl || (currentChannelId !== channelId && !EphemeralStore.isSwitchingToChannel(channelId))) {
|
||||
await markChannelAsViewed(serverUrl, channelId);
|
||||
await markChannelAsViewed(serverUrl, channelId, false);
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
@@ -318,9 +323,12 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
|
||||
}
|
||||
|
||||
export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg: any) {
|
||||
try {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Depending on who was removed, the ids may come from one place dataset or the other.
|
||||
const userId = msg.data.user_id || msg.broadcast.user_id;
|
||||
const channelId = msg.data.channel_id || msg.broadcast.channel_id;
|
||||
@@ -329,6 +337,8 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
|
||||
return;
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const channel = await getCurrentChannel(database);
|
||||
const user = await getCurrentUser(database);
|
||||
if (!user) {
|
||||
return;
|
||||
@@ -344,11 +354,39 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
|
||||
}
|
||||
|
||||
if (user.id === userId) {
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
if (currentChannelId && currentChannelId === channelId) {
|
||||
await handleKickFromChannel(serverUrl, currentChannelId);
|
||||
}
|
||||
await removeCurrentUserFromChannel(serverUrl, channelId);
|
||||
if (channel && channel.id === channelId) {
|
||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
||||
|
||||
if (currentServer?.url === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.LEAVE_CHANNEL, channel.displayName);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
|
||||
if (await isTablet()) {
|
||||
let tId = channel.teamId;
|
||||
if (!tId) {
|
||||
tId = await getCurrentTeamId(database);
|
||||
}
|
||||
const channelToJumpTo = await getNthLastChannelFromTeam(database, tId);
|
||||
if (channelToJumpTo) {
|
||||
if (channelToJumpTo === Screens.GLOBAL_THREADS) {
|
||||
const {models: switchToGlobalThreadsModels} = await switchToGlobalThreads(serverUrl, tId, true);
|
||||
if (switchToGlobalThreadsModels) {
|
||||
models.push(...switchToGlobalThreadsModels);
|
||||
}
|
||||
} else {
|
||||
switchToChannelById(serverUrl, channelToJumpTo, tId, true);
|
||||
}
|
||||
} // TODO else jump to "join a channel" screen https://mattermost.atlassian.net/browse/MM-41051
|
||||
} else {
|
||||
const currentChannelModels = await prepareCommonSystemValues(operator, {currentChannelId: ''});
|
||||
if (currentChannelModels?.length) {
|
||||
models.push(...currentChannelModels);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const {models: deleteMemberModels} = await deleteChannelMembership(operator, userId, channelId, true);
|
||||
if (deleteMemberModels) {
|
||||
@@ -357,8 +395,8 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
|
||||
}
|
||||
|
||||
operator.batchRecords(models);
|
||||
} catch (error) {
|
||||
logDebug('cannot handle user removed from channel websocket event', error);
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,10 +428,34 @@ export async function handleChannelDeletedEvent(serverUrl: string, msg: WebSocke
|
||||
}
|
||||
|
||||
if (config?.ExperimentalViewArchivedChannels !== 'true') {
|
||||
if (currentChannel && currentChannel.id === channelId) {
|
||||
await handleKickFromChannel(serverUrl, channelId, Events.CHANNEL_ARCHIVED);
|
||||
}
|
||||
await removeCurrentUserFromChannel(serverUrl, channelId);
|
||||
|
||||
if (currentChannel && currentChannel.id === channelId) {
|
||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
||||
|
||||
if (currentServer?.url === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.CHANNEL_ARCHIVED, currentChannel.displayName);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
|
||||
if (await isTablet()) {
|
||||
let tId = currentChannel.teamId;
|
||||
if (!tId) {
|
||||
tId = await getCurrentTeamId(database);
|
||||
}
|
||||
const channelToJumpTo = await getNthLastChannelFromTeam(database, tId);
|
||||
if (channelToJumpTo) {
|
||||
if (channelToJumpTo === Screens.GLOBAL_THREADS) {
|
||||
switchToGlobalThreads(serverUrl, tId);
|
||||
return;
|
||||
}
|
||||
switchToChannelById(serverUrl, channelToJumpTo, tId);
|
||||
} // TODO else jump to "join a channel" screen
|
||||
} else {
|
||||
setCurrentChannelId(operator, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchGroupsForChannel, fetchGroupsForMember, fetchGroupsForTeam} from '@actions/remote/groups';
|
||||
import {deleteGroupChannelById, deleteGroupMembershipById, deleteGroupTeamById} from '@app/queries/servers/group';
|
||||
import {generateGroupAssociationId} from '@app/utils/groups';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {deleteGroupChannelById, deleteGroupMembershipById, deleteGroupTeamById} from '@queries/servers/group';
|
||||
import {generateGroupAssociationId} from '@utils/groups';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
type WebsocketGroupMessage = WebSocketMessage<{
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {markChannelAsViewed} from '@actions/local/channel';
|
||||
import {markChannelAsRead} from '@actions/remote/channel';
|
||||
import {handleEntryAfterLoadNavigation} from '@actions/remote/entry/common';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {deferredAppEntryActions, entry} from '@actions/remote/entry/gql_common';
|
||||
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
|
||||
import {fetchStatusByIds} from '@actions/remote/user';
|
||||
import {loadConfigAndCalls} from '@calls/actions/calls';
|
||||
import {
|
||||
handleCallChannelDisabled,
|
||||
handleCallChannelEnabled,
|
||||
handleCallEnded,
|
||||
handleCallHostChanged,
|
||||
handleCallRecordingState,
|
||||
handleCallScreenOff,
|
||||
handleCallScreenOn,
|
||||
handleCallStarted,
|
||||
@@ -21,35 +18,31 @@ import {
|
||||
handleCallUserDisconnected,
|
||||
handleCallUserMuted,
|
||||
handleCallUserRaiseHand,
|
||||
handleCallUserReacted,
|
||||
handleCallUserUnmuted,
|
||||
handleCallUserUnraiseHand,
|
||||
handleCallUserVoiceOff,
|
||||
handleCallUserVoiceOn,
|
||||
} from '@calls/connection/websocket_event_handlers';
|
||||
import {isSupportedServerCalls} from '@calls/utils';
|
||||
import {Screens, WebsocketEvents} from '@constants';
|
||||
import {Events, Screens, WebsocketEvents} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import {getActiveServerUrl, queryActiveServer} from '@queries/app/servers';
|
||||
import {getCurrentChannel} from '@queries/servers/channel';
|
||||
import {getLastPostInThread} from '@queries/servers/post';
|
||||
import {
|
||||
getCommonSystemValues,
|
||||
getConfig,
|
||||
getCurrentChannelId,
|
||||
getCurrentUserId,
|
||||
getLicense,
|
||||
getWebSocketLastDisconnected,
|
||||
resetWebSocketLastDisconnected,
|
||||
setCurrentTeamAndChannelId,
|
||||
} from '@queries/servers/system';
|
||||
import {getCurrentTeam} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {dismissAllModals, popToRoot} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug, logInfo} from '@utils/log';
|
||||
import {logInfo} from '@utils/log';
|
||||
|
||||
import {handleCategoryCreatedEvent, handleCategoryDeletedEvent, handleCategoryOrderUpdatedEvent, handleCategoryUpdatedEvent} from './category';
|
||||
import {handleChannelConvertedEvent, handleChannelCreatedEvent,
|
||||
@@ -68,7 +61,7 @@ import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePrefe
|
||||
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} from './teams';
|
||||
import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads';
|
||||
import {handleUserUpdatedEvent, handleUserTypingEvent} from './users';
|
||||
|
||||
@@ -86,7 +79,7 @@ export async function handleFirstConnect(serverUrl: string) {
|
||||
|
||||
// ESR: 5.37
|
||||
if (lastDisconnect && config?.EnableReliableWebSockets !== 'true' && alreadyConnected.has(serverUrl)) {
|
||||
await handleReconnect(serverUrl);
|
||||
handleReconnect(serverUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,8 +93,8 @@ export async function handleFirstConnect(serverUrl: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleReconnect(serverUrl: string) {
|
||||
await doReconnect(serverUrl);
|
||||
export function handleReconnect(serverUrl: string) {
|
||||
doReconnect(serverUrl);
|
||||
}
|
||||
|
||||
export async function handleClose(serverUrl: string, lastDisconnect: number) {
|
||||
@@ -138,35 +131,60 @@ async function doReconnect(serverUrl: string) {
|
||||
|
||||
const currentTeam = await getCurrentTeam(database);
|
||||
const currentChannel = await getCurrentChannel(database);
|
||||
const currentActiveServerUrl = await getActiveServerUrl(DatabaseManager.appDatabase!.database);
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, currentTeam?.id, currentChannel?.id, lastDisconnectedAt);
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
if (serverUrl === currentActiveServerUrl) {
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
|
||||
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeam?.id || '', currentChannel?.id || '', initialTeamId, initialChannelId);
|
||||
let switchedToChannel = false;
|
||||
|
||||
// if no longer a member of the current team or the current channel
|
||||
if (initialTeamId !== currentTeam?.id || initialChannelId !== currentChannel?.id) {
|
||||
const currentServer = await queryActiveServer(appDatabase);
|
||||
const isChannelScreenMounted = NavigationStore.getNavigationComponents().includes(Screens.CHANNEL);
|
||||
if (serverUrl === currentServer?.url) {
|
||||
if (currentTeam && initialTeamId !== currentTeam.id) {
|
||||
DeviceEventEmitter.emit(Events.LEAVE_TEAM, {displayName: currentTeam.displayName});
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
} else if (currentChannel && initialChannelId !== currentChannel.id && isChannelScreenMounted) {
|
||||
DeviceEventEmitter.emit(Events.LEAVE_CHANNEL, {displayName: currentChannel?.displayName});
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
}
|
||||
|
||||
const tabletDevice = await isTablet();
|
||||
|
||||
if (tabletDevice && initialChannelId) {
|
||||
switchedToChannel = true;
|
||||
switchToChannelById(serverUrl, initialChannelId, initialTeamId);
|
||||
} else {
|
||||
setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
|
||||
}
|
||||
} else {
|
||||
setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
|
||||
}
|
||||
}
|
||||
|
||||
const dt = Date.now();
|
||||
await operator.batchRecords(models);
|
||||
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
await fetchPostDataIfNeeded(serverUrl);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(database))!;
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchedToChannel ? initialChannelId : undefined);
|
||||
|
||||
if (isSupportedServerCalls(config?.Version)) {
|
||||
loadConfigAndCalls(serverUrl, currentUserId);
|
||||
}
|
||||
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
|
||||
|
||||
AppsManager.refreshAppBindings(serverUrl);
|
||||
// https://mattermost.atlassian.net/browse/MM-41520
|
||||
}
|
||||
|
||||
export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
@@ -312,14 +330,6 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
handleOpenDialogEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.DELETE_TEAM:
|
||||
handleTeamArchived(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.RESTORE_TEAM:
|
||||
handleTeamRestored(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.THREAD_UPDATED:
|
||||
handleThreadUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
@@ -380,15 +390,6 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
case WebsocketEvents.CALLS_CALL_END:
|
||||
handleCallEnded(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_REACTED:
|
||||
handleCallUserReacted(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_RECORDING_STATE:
|
||||
handleCallRecordingState(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_HOST_CHANGED:
|
||||
handleCallHostChanged(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.GROUP_RECEIVED:
|
||||
handleGroupReceivedEvent(serverUrl, msg);
|
||||
@@ -411,44 +412,3 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPostDataIfNeeded(serverUrl: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const mountedScreens = NavigationStore.getScreensInStack();
|
||||
const isChannelScreenMounted = mountedScreens.includes(Screens.CHANNEL);
|
||||
const isThreadScreenMounted = mountedScreens.includes(Screens.THREAD);
|
||||
const tabletDevice = await isTablet();
|
||||
|
||||
if (isCRTEnabled && isThreadScreenMounted) {
|
||||
// Fetch new posts in the thread only when CRT is enabled,
|
||||
// for non-CRT fetchPostsForChannel includes posts in the thread
|
||||
const rootId = EphemeralStore.getCurrentThreadId();
|
||||
if (rootId) {
|
||||
const lastPost = await getLastPostInThread(database, rootId);
|
||||
if (lastPost) {
|
||||
if (lastPost) {
|
||||
const options: FetchPaginatedThreadOptions = {};
|
||||
options.fromCreateAt = lastPost.createAt;
|
||||
options.fromPost = lastPost.id;
|
||||
options.direction = 'down';
|
||||
await fetchPostThread(serverUrl, rootId, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentChannelId && (isChannelScreenMounted || tabletDevice)) {
|
||||
await fetchPostsForChannel(serverUrl, currentChannelId);
|
||||
markChannelAsRead(serverUrl, currentChannelId);
|
||||
if (!EphemeralStore.wasNotificationTapped()) {
|
||||
markChannelAsViewed(serverUrl, currentChannelId, true);
|
||||
}
|
||||
EphemeralStore.setNotificationTapped(false);
|
||||
}
|
||||
} catch (error) {
|
||||
logDebug('could not fetch needed post after WS reconnect', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import {getActiveServerUrl} from '@queries/app/servers';
|
||||
|
||||
@@ -9,10 +9,14 @@ export async function handleOpenDialogEvent(serverUrl: string, msg: WebSocketMes
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dialog: InteractiveDialogConfig = JSON.parse(data);
|
||||
const currentServer = await getActiveServerUrl();
|
||||
const currentServer = await getActiveServerUrl(appDatabase);
|
||||
if (currentServer === serverUrl) {
|
||||
IntegrationsManager.getManager(serverUrl).setDialog(dialog);
|
||||
}
|
||||
|
||||
@@ -128,8 +128,11 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
|
||||
) {
|
||||
markAsViewed = true;
|
||||
markAsRead = false;
|
||||
} else if ((post.channel_id === currentChannelId)) {
|
||||
const isChannelScreenMounted = NavigationStore.getScreensInStack().includes(Screens.CHANNEL);
|
||||
} else if ((post.channel_id === currentChannelId)) { // TODO: THREADS && !viewingGlobalThreads) {
|
||||
// Don't mark as read if we're in global threads screen
|
||||
// the currentChannelId still refers to previously viewed channel
|
||||
|
||||
const isChannelScreenMounted = NavigationStore.getNavigationComponents().includes(Screens.CHANNEL);
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
if (isChannelScreenMounted || isTabletDevice) {
|
||||
@@ -143,7 +146,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
|
||||
markChannelAsRead(serverUrl, post.channel_id);
|
||||
} else if (markAsViewed) {
|
||||
preparedMyChannelHack(myChannel);
|
||||
const {member: viewedAt} = await markChannelAsViewed(serverUrl, post.channel_id, false, true);
|
||||
const {member: viewedAt} = await markChannelAsViewed(serverUrl, post.channel_id, true);
|
||||
if (viewedAt) {
|
||||
models.push(viewedAt);
|
||||
}
|
||||
@@ -164,13 +167,8 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
|
||||
}
|
||||
}
|
||||
|
||||
let actionType: string = ActionType.POSTS.RECEIVED_NEW;
|
||||
if (isCRTEnabled && post.root_id) {
|
||||
actionType = ActionType.POSTS.RECEIVED_IN_THREAD;
|
||||
}
|
||||
|
||||
const postModels = await operator.handlePosts({
|
||||
actionType,
|
||||
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||
order: [post.id],
|
||||
posts: [post],
|
||||
prepareRecordsOnly: true,
|
||||
@@ -208,14 +206,8 @@ export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage)
|
||||
models.push(...authorsModels);
|
||||
}
|
||||
|
||||
let actionType: string = ActionType.POSTS.RECEIVED_NEW;
|
||||
const isCRTEnabled = await getIsCRTEnabled(operator.database);
|
||||
if (isCRTEnabled && post.root_id) {
|
||||
actionType = ActionType.POSTS.RECEIVED_IN_THREAD;
|
||||
}
|
||||
|
||||
const postModels = await operator.handlePosts({
|
||||
actionType,
|
||||
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||
order: [post.id],
|
||||
posts: [post],
|
||||
prepareRecordsOnly: true,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {updateDmGmDisplayName} from '@actions/local/channel';
|
||||
import {storeConfig} from '@actions/local/systems';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
@@ -36,8 +35,10 @@ export async function handleConfigChangedEvent(serverUrl: string, msg: WebSocket
|
||||
|
||||
try {
|
||||
const config = msg.data.config;
|
||||
const systems: IdValue[] = [{id: SYSTEM_IDENTIFIERS.CONFIG, value: JSON.stringify(config)}];
|
||||
|
||||
const prevConfig = await getConfig(operator.database);
|
||||
await storeConfig(serverUrl, config);
|
||||
await operator.handleSystem({systems, prepareRecordsOnly: false});
|
||||
if (config?.LockTeammateNameDisplay && (prevConfig?.LockTeammateNameDisplay !== config.LockTeammateNameDisplay)) {
|
||||
updateDmGmDisplayName(serverUrl);
|
||||
}
|
||||
|
||||
@@ -2,106 +2,64 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {removeUserFromTeam} from '@actions/local/team';
|
||||
import {fetchMyChannelsForTeam} from '@actions/remote/channel';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchMyTeam, handleKickFromTeam, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {fetchAllTeams, handleTeamChange, fetchMyTeam} from '@actions/remote/team';
|
||||
import {updateUsersNoLongerVisible} from '@actions/remote/user';
|
||||
import Events from '@constants/events';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||
import {getActiveServerUrl} from '@queries/app/servers';
|
||||
import {prepareCategories, prepareCategoryChannels} from '@queries/servers/categories';
|
||||
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
|
||||
import {getCurrentTeam, prepareMyTeams, queryMyTeamsByIds} from '@queries/servers/team';
|
||||
import {getCurrentTeam, getLastTeam, prepareMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {dismissAllModals, popToRoot, resetToTeams} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
export async function handleTeamArchived(serverUrl: string, msg: WebSocketMessage) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const team: Team = JSON.parse(msg.data.team);
|
||||
|
||||
const membership = (await queryMyTeamsByIds(database, [team.id]).fetch())[0];
|
||||
if (membership) {
|
||||
const currentTeam = await getCurrentTeam(database);
|
||||
if (currentTeam?.id === team.id) {
|
||||
await handleKickFromTeam(serverUrl, team.id);
|
||||
}
|
||||
|
||||
await removeUserFromTeam(serverUrl, team.id);
|
||||
|
||||
const user = await getCurrentUser(database);
|
||||
if (user?.isGuest) {
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
}
|
||||
}
|
||||
updateCanJoinTeams(serverUrl);
|
||||
} catch (error) {
|
||||
logDebug('cannot handle archive team websocket event', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleTeamRestored(serverUrl: string, msg: WebSocketMessage) {
|
||||
let markedAsLoading = false;
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const team: Team = JSON.parse(msg.data.team);
|
||||
|
||||
const teamMembership = await client.getTeamMember(team.id, 'me');
|
||||
if (teamMembership && teamMembership.delete_at === 0) {
|
||||
// Ignore duplicated team join events sent by the server
|
||||
if (EphemeralStore.isAddingToTeam(team.id)) {
|
||||
return;
|
||||
}
|
||||
EphemeralStore.startAddingToTeam(team.id);
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
markedAsLoading = true;
|
||||
await fetchAndStoreJoinedTeamInfo(serverUrl, operator, team.id, [team], [teamMembership]);
|
||||
setTeamLoading(serverUrl, false);
|
||||
markedAsLoading = false;
|
||||
|
||||
EphemeralStore.finishAddingToTeam(team.id);
|
||||
}
|
||||
|
||||
updateCanJoinTeams(serverUrl);
|
||||
} catch (error) {
|
||||
if (markedAsLoading) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
logDebug('cannot handle restore team websocket event', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const database = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await getCurrentUser(database);
|
||||
if (!user) {
|
||||
return;
|
||||
const currentTeam = await getCurrentTeam(database.database);
|
||||
const user = await getCurrentUser(database.database);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {user_id: userId, team_id: teamId} = msg.data;
|
||||
if (user.id === userId) {
|
||||
await removeUserFromTeam(serverUrl, teamId);
|
||||
fetchAllTeams(serverUrl);
|
||||
|
||||
if (user.isGuest) {
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
}
|
||||
|
||||
const {user_id: userId, team_id: teamId} = msg.data;
|
||||
if (user.id === userId) {
|
||||
const currentTeam = await getCurrentTeam(database);
|
||||
if (currentTeam?.id === teamId) {
|
||||
await handleKickFromTeam(serverUrl, teamId);
|
||||
if (currentTeam?.id === teamId) {
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
let currentServer = '';
|
||||
if (appDatabase) {
|
||||
currentServer = await getActiveServerUrl(appDatabase);
|
||||
}
|
||||
|
||||
await removeUserFromTeam(serverUrl, teamId);
|
||||
updateCanJoinTeams(serverUrl);
|
||||
if (currentServer === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.LEAVE_TEAM, currentTeam?.displayName);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
}
|
||||
|
||||
if (user.isGuest) {
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
const teamToJumpTo = await getLastTeam(database.database);
|
||||
if (teamToJumpTo) {
|
||||
handleTeamChange(serverUrl, teamToJumpTo);
|
||||
} else if (currentServer === serverUrl) {
|
||||
resetToTeams();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logDebug('cannot handle leave team websocket event', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +81,10 @@ export async function handleUpdateTeamEvent(serverUrl: string, msg: WebSocketMes
|
||||
}
|
||||
|
||||
export async function handleUserAddedToTeamEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
const {team_id: teamId} = msg.data;
|
||||
|
||||
// Ignore duplicated team join events sent by the server
|
||||
@@ -131,30 +93,17 @@ export async function handleUserAddedToTeamEvent(serverUrl: string, msg: WebSock
|
||||
}
|
||||
EphemeralStore.startAddingToTeam(teamId);
|
||||
|
||||
try {
|
||||
setTeamLoading(serverUrl, true);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {teams, memberships: teamMemberships} = await fetchMyTeam(serverUrl, teamId, true);
|
||||
const {teams, memberships: teamMemberships} = await fetchMyTeam(serverUrl, teamId, true);
|
||||
|
||||
await fetchAndStoreJoinedTeamInfo(serverUrl, operator, teamId, teams, teamMemberships);
|
||||
} catch (error) {
|
||||
logDebug('could not handle user added to team websocket event');
|
||||
}
|
||||
setTeamLoading(serverUrl, false);
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
}
|
||||
|
||||
const fetchAndStoreJoinedTeamInfo = async (serverUrl: string, operator: ServerDataOperator, teamId: string, teams?: Team[], teamMemberships?: TeamMembership[]) => {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
if (teams?.length && teamMemberships?.length) {
|
||||
const {channels, memberships, categories} = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
|
||||
modelPromises.push(prepareCategoriesAndCategoriesChannels(operator, categories || [], true));
|
||||
modelPromises.push(prepareCategories(operator, categories));
|
||||
modelPromises.push(prepareCategoryChannels(operator, categories));
|
||||
modelPromises.push(...await prepareMyChannelsForTeam(operator, teamId, channels || [], memberships || []));
|
||||
|
||||
const {roles} = await fetchRoles(serverUrl, teamMemberships, memberships, undefined, true);
|
||||
if (roles?.length) {
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
|
||||
if (teams && teamMemberships) {
|
||||
@@ -163,4 +112,6 @@ const fetchAndStoreJoinedTeamInfo = async (serverUrl: string, operator: ServerDa
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
};
|
||||
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
}
|
||||
|
||||
@@ -2,22 +2,12 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {markTeamThreadsAsRead, processReceivedThreads, updateThread} from '@actions/local/thread';
|
||||
import {getCurrentTeamId} from '@app/queries/servers/system';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
export async function handleThreadUpdatedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
|
||||
try {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
const thread: Thread = JSON.parse(msg.data.thread);
|
||||
let teamId = msg.broadcast.team_id;
|
||||
|
||||
if (!teamId) {
|
||||
teamId = await getCurrentTeamId(database);
|
||||
}
|
||||
const teamId = msg.broadcast.team_id;
|
||||
|
||||
// Mark it as following
|
||||
thread.is_following = true;
|
||||
|
||||
@@ -12,7 +12,7 @@ import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {queryChannelsByTypes, queryUserChannelsByTypes} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
@@ -84,14 +84,13 @@ export async function handleUserTypingEvent(serverUrl: string, msg: WebSocketMes
|
||||
return;
|
||||
}
|
||||
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
|
||||
const {users, existingUsers} = await fetchUsersByIds(serverUrl, [msg.data.user_id]);
|
||||
const user = users?.[0] || existingUsers?.[0];
|
||||
|
||||
const namePreference = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(namePreference, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(namePreference, config, license);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const username = displayUsername(user, currentUser?.locale, teammateDisplayNameSetting);
|
||||
const data = {
|
||||
|
||||
@@ -184,8 +184,6 @@ query ${QueryNames.QUERY_ENTRY} {
|
||||
createAt
|
||||
expiresAt
|
||||
}
|
||||
termsOfServiceId
|
||||
termsOfServiceCreateAt
|
||||
}
|
||||
teamMembers(userId:"me") {
|
||||
deleteAt
|
||||
|
||||
@@ -205,16 +205,12 @@ export default class ClientBase {
|
||||
return `${this.urlVersion}/plugins`;
|
||||
}
|
||||
|
||||
getPluginRoute(id: string) {
|
||||
return `/plugins/${id}`;
|
||||
}
|
||||
|
||||
getAppsProxyRoute() {
|
||||
return this.getPluginRoute('com.mattermost.apps');
|
||||
return '/plugins/com.mattermost.apps';
|
||||
}
|
||||
|
||||
getCallsRoute() {
|
||||
return this.getPluginRoute(Calls.PluginId);
|
||||
return `/plugins/${Calls.PluginId}`;
|
||||
}
|
||||
|
||||
doFetch = async (url: string, options: ClientOptions, returnDataOnly = true) => {
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
import {ClientResponse, ClientResponseError, ProgressPromise, UploadRequestOptions} from '@mattermost/react-native-network-client';
|
||||
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
|
||||
export interface ClientFilesMix {
|
||||
getFileUrl: (fileId: string, timestamp: number) => string;
|
||||
getFileThumbnailUrl: (fileId: string, timestamp: number) => string;
|
||||
@@ -74,7 +72,7 @@ const ClientFiles = (superclass: any) => class extends superclass {
|
||||
channel_id: channelId,
|
||||
},
|
||||
},
|
||||
timeoutInterval: toMilliseconds({minutes: 3}),
|
||||
timeoutInterval: 3 * 60 * 1000, // 3 minutes
|
||||
};
|
||||
const promise = this.apiClient.upload(url, file.localPath, options) as ProgressPromise<ClientResponse>;
|
||||
promise.progress!(onProgress).then(onComplete).catch(onError);
|
||||
|
||||
@@ -15,7 +15,6 @@ import ClientFiles, {ClientFilesMix} from './files';
|
||||
import ClientGeneral, {ClientGeneralMix} from './general';
|
||||
import ClientGroups, {ClientGroupsMix} from './groups';
|
||||
import ClientIntegrations, {ClientIntegrationsMix} from './integrations';
|
||||
import ClientNPS, {ClientNPSMix} from './nps';
|
||||
import ClientPosts, {ClientPostsMix} from './posts';
|
||||
import ClientPreferences, {ClientPreferencesMix} from './preferences';
|
||||
import ClientTeams, {ClientTeamsMix} from './teams';
|
||||
@@ -41,8 +40,7 @@ interface Client extends ClientBase,
|
||||
ClientTosMix,
|
||||
ClientUsersMix,
|
||||
ClientCallsMix,
|
||||
ClientPluginsMix,
|
||||
ClientNPSMix
|
||||
ClientPluginsMix
|
||||
{}
|
||||
|
||||
class Client extends mix(ClientBase).with(
|
||||
@@ -62,7 +60,6 @@ class Client extends mix(ClientBase).with(
|
||||
ClientUsers,
|
||||
ClientCalls,
|
||||
ClientPlugins,
|
||||
ClientNPS,
|
||||
) {
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string) {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General} from '@constants';
|
||||
|
||||
export interface ClientNPSMix {
|
||||
npsGiveFeedbackAction: () => Promise<Post>;
|
||||
}
|
||||
|
||||
const ClientNPS = (superclass: any) => class extends superclass {
|
||||
npsGiveFeedbackAction = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getPluginRoute(General.NPS_PLUGIN_ID)}/api/v1/give_feedback`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientNPS;
|
||||
@@ -2,8 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export interface ClientTosMix {
|
||||
updateMyTermsOfServiceStatus: (termsOfServiceId: string, accepted: boolean) => Promise<{status: string}>;
|
||||
getTermsOfService: () => Promise<TermsOfService>;
|
||||
updateMyTermsOfServiceStatus: (termsOfServiceId: string, accepted: boolean) => Promise<any>;
|
||||
getTermsOfService: () => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientTos = (superclass: any) => class extends superclass {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {Platform} from 'react-native';
|
||||
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getConfig} from '@queries/servers/system';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {logError, logInfo, logWarning} from '@utils/log';
|
||||
|
||||
const MAX_WEBSOCKET_FAILS = 7;
|
||||
@@ -33,7 +33,6 @@ export default class WebSocketClient {
|
||||
private firstConnectCallback?: () => void;
|
||||
private missedEventsCallback?: () => void;
|
||||
private reconnectCallback?: () => void;
|
||||
private reliableReconnectCallback?: () => void;
|
||||
private errorCallback?: Function;
|
||||
private closeCallback?: (connectFailCount: number, lastDisconnect: number) => void;
|
||||
private connectingCallback?: () => void;
|
||||
@@ -76,8 +75,8 @@ export default class WebSocketClient {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await getConfig(database);
|
||||
const connectionUrl = (config.WebsocketURL || this.serverUrl) + '/api/v4/websocket';
|
||||
const system = await getCommonSystemValues(database);
|
||||
const connectionUrl = (system.config.WebsocketURL || this.serverUrl) + '/api/v4/websocket';
|
||||
|
||||
if (this.connectingCallback) {
|
||||
this.connectingCallback();
|
||||
@@ -98,7 +97,7 @@ export default class WebSocketClient {
|
||||
|
||||
this.url = connectionUrl;
|
||||
|
||||
const reliableWebSockets = config.EnableReliableWebSockets === 'true';
|
||||
const reliableWebSockets = system.config.EnableReliableWebSockets === 'true';
|
||||
if (reliableWebSockets) {
|
||||
// Add connection id, and last_sequence_number to the query param.
|
||||
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
|
||||
@@ -149,11 +148,8 @@ export default class WebSocketClient {
|
||||
logInfo('websocket re-established connection to', this.url);
|
||||
if (!reliableWebSockets && this.reconnectCallback) {
|
||||
this.reconnectCallback();
|
||||
} else if (reliableWebSockets) {
|
||||
this.reliableReconnectCallback?.();
|
||||
if (this.serverSequence && this.missedEventsCallback) {
|
||||
this.missedEventsCallback();
|
||||
}
|
||||
} else if (reliableWebSockets && this.serverSequence && this.missedEventsCallback) {
|
||||
this.missedEventsCallback();
|
||||
}
|
||||
} else if (this.firstConnectCallback) {
|
||||
logInfo('websocket connected to', this.url);
|
||||
@@ -299,10 +295,6 @@ export default class WebSocketClient {
|
||||
this.reconnectCallback = callback;
|
||||
}
|
||||
|
||||
public setReliableReconnectCallback(callback: () => void) {
|
||||
this.reliableReconnectCallback = callback;
|
||||
}
|
||||
|
||||
public setErrorCallback(callback: Function) {
|
||||
this.errorCallback = callback;
|
||||
}
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {dismissAnnouncement} from '@actions/local/systems';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import RemoveMarkdown from '@components/remove_markdown';
|
||||
import {ANNOUNCEMENT_BAR_HEIGHT} from '@constants/view';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {getMarkdownTextStyles} from '@utils/markdown';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import ExpandedAnnouncementBanner from './expanded_announcement_banner';
|
||||
|
||||
type Props = {
|
||||
bannerColor: string;
|
||||
bannerDismissed: boolean;
|
||||
bannerEnabled: boolean;
|
||||
bannerText?: string;
|
||||
bannerTextColor?: string;
|
||||
allowDismissal: boolean;
|
||||
}
|
||||
|
||||
const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
background: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
},
|
||||
bannerContainer: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 10,
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 8,
|
||||
borderRadius: 7,
|
||||
},
|
||||
wrapper: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
bannerTextContainer: {
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
marginRight: 5,
|
||||
textAlign: 'center',
|
||||
},
|
||||
bannerText: {
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const CLOSE_BUTTON_ID = 'announcement-close';
|
||||
|
||||
const BUTTON_HEIGHT = 48; // From /app/utils/buttonStyles.ts, lg button
|
||||
const TITLE_HEIGHT = 30 + 12; // typography 600 line height
|
||||
const MARGINS = 12 + 24 + 10; // (after title + after text + after content) from ./expanded_announcement_banner.tsx
|
||||
const TEXT_CONTAINER_HEIGHT = 150;
|
||||
const DISMISS_BUTTON_HEIGHT = BUTTON_HEIGHT + 10; // Top margin from ./expanded_announcement_banner.tsx
|
||||
|
||||
const SNAP_POINT_WITHOUT_DISMISS = TITLE_HEIGHT + BUTTON_HEIGHT + MARGINS + TEXT_CONTAINER_HEIGHT;
|
||||
|
||||
const AnnouncementBanner = ({
|
||||
bannerColor,
|
||||
bannerDismissed,
|
||||
bannerEnabled,
|
||||
bannerText = '',
|
||||
bannerTextColor = '#000',
|
||||
allowDismissal,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const height = useSharedValue(0);
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
const theme = useTheme();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const style = getStyle(theme);
|
||||
const markdownTextStyles = getMarkdownTextStyles(theme);
|
||||
|
||||
const renderContent = useCallback(() => (
|
||||
<ExpandedAnnouncementBanner
|
||||
allowDismissal={allowDismissal}
|
||||
bannerText={bannerText}
|
||||
/>
|
||||
), [allowDismissal, bannerText]);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
const title = intl.formatMessage({
|
||||
id: 'mobile.announcement_banner.title',
|
||||
defaultMessage: 'Announcement',
|
||||
});
|
||||
|
||||
const snapPoint = bottomSheetSnapPoint(
|
||||
1,
|
||||
SNAP_POINT_WITHOUT_DISMISS + (allowDismissal ? DISMISS_BUTTON_HEIGHT : 0),
|
||||
bottom,
|
||||
);
|
||||
|
||||
bottomSheet({
|
||||
closeButtonId: CLOSE_BUTTON_ID,
|
||||
title,
|
||||
renderContent,
|
||||
snapPoints: [1, snapPoint],
|
||||
theme,
|
||||
});
|
||||
}, [theme.sidebarHeaderTextColor, intl.locale, renderContent, allowDismissal, bottom]);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
dismissAnnouncement(serverUrl, bannerText);
|
||||
}, [serverUrl, bannerText]);
|
||||
|
||||
useEffect(() => {
|
||||
const showBanner = bannerEnabled && !bannerDismissed && Boolean(bannerText);
|
||||
setVisible(showBanner);
|
||||
}, [bannerDismissed, bannerEnabled, bannerText]);
|
||||
|
||||
useEffect(() => {
|
||||
height.value = withTiming(visible ? ANNOUNCEMENT_BAR_HEIGHT : 0, {
|
||||
duration: 200,
|
||||
});
|
||||
}, [visible]);
|
||||
|
||||
const bannerStyle = useAnimatedStyle(() => ({
|
||||
height: height.value,
|
||||
}));
|
||||
|
||||
const bannerTextContainerStyle = useMemo(() => [style.bannerTextContainer, {
|
||||
color: bannerTextColor,
|
||||
}], [style, bannerTextColor]);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[style.background, bannerStyle]}
|
||||
>
|
||||
<View
|
||||
style={[style.bannerContainer, {backgroundColor: bannerColor}]}
|
||||
>
|
||||
{visible &&
|
||||
<>
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
style={style.wrapper}
|
||||
>
|
||||
<Text
|
||||
style={bannerTextContainerStyle}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
<CompassIcon
|
||||
color={bannerTextColor}
|
||||
name='information-outline'
|
||||
size={18}
|
||||
/>
|
||||
{' '}
|
||||
<RemoveMarkdown
|
||||
value={bannerText}
|
||||
textStyle={markdownTextStyles}
|
||||
baseStyle={style.bannerText}
|
||||
/>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{allowDismissal && (
|
||||
<TouchableOpacity
|
||||
onPress={handleDismiss}
|
||||
>
|
||||
<CompassIcon
|
||||
color={changeOpacity(bannerTextColor, 0.56)}
|
||||
name='close'
|
||||
size={18}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
</>
|
||||
}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnouncementBanner;
|
||||
@@ -1,138 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BottomSheetScrollView} from '@gorhom/bottom-sheet';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ScrollView, Text, View} from 'react-native';
|
||||
import Button from 'react-native-button';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {dismissAnnouncement} from '@actions/local/systems';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Markdown from '@components/markdown';
|
||||
import {Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
allowDismissal: boolean;
|
||||
bannerText: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContainer: {
|
||||
flex: 1,
|
||||
marginTop: 12,
|
||||
marginBottom: 24,
|
||||
},
|
||||
baseTextStyle: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 100, 'Regular'),
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
dismissBottomSheet();
|
||||
};
|
||||
|
||||
const ExpandedAnnouncementBanner = ({
|
||||
allowDismissal,
|
||||
bannerText,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
const intl = useIntl();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const dismissBanner = useCallback(() => {
|
||||
dismissAnnouncement(serverUrl, bannerText);
|
||||
close();
|
||||
}, [bannerText]);
|
||||
|
||||
const buttonStyles = useMemo(() => {
|
||||
return {
|
||||
okay: {
|
||||
button: buttonBackgroundStyle(theme, 'lg', 'primary'),
|
||||
text: buttonTextStyle(theme, 'lg', 'primary'),
|
||||
},
|
||||
dismiss: {
|
||||
button: [{marginTop: 10}, buttonBackgroundStyle(theme, 'lg', 'link')],
|
||||
text: buttonTextStyle(theme, 'lg', 'link'),
|
||||
},
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
return [style.container, {marginBottom: insets.bottom + 10}];
|
||||
}, [style, insets.bottom]);
|
||||
|
||||
const Scroll = useMemo(() => (isTablet ? ScrollView : BottomSheetScrollView), [isTablet]);
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
{!isTablet && (
|
||||
<Text style={style.title}>
|
||||
{intl.formatMessage({
|
||||
id: 'mobile.announcement_banner.title',
|
||||
defaultMessage: 'Announcement',
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
<Scroll
|
||||
style={style.scrollContainer}
|
||||
>
|
||||
<Markdown
|
||||
baseTextStyle={style.baseTextStyle}
|
||||
blockStyles={getMarkdownBlockStyles(theme)}
|
||||
disableGallery={true}
|
||||
textStyles={getMarkdownTextStyles(theme)}
|
||||
value={bannerText}
|
||||
theme={theme}
|
||||
location={Screens.BOTTOM_SHEET}
|
||||
/>
|
||||
</Scroll>
|
||||
<Button
|
||||
containerStyle={buttonStyles.okay.button}
|
||||
onPress={close}
|
||||
>
|
||||
<FormattedText
|
||||
id='announcment_banner.okay'
|
||||
defaultMessage={'Okay'}
|
||||
style={buttonStyles.okay.text}
|
||||
/>
|
||||
</Button>
|
||||
{allowDismissal && (
|
||||
<Button
|
||||
containerStyle={buttonStyles.dismiss.button}
|
||||
onPress={dismissBanner}
|
||||
>
|
||||
<FormattedText
|
||||
id='announcment_banner.dismiss'
|
||||
defaultMessage={'Dismiss announcement'}
|
||||
style={buttonStyles.dismiss.text}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpandedAnnouncementBanner;
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$, combineLatest} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfigBooleanValue, observeConfigValue, observeLastDismissedAnnouncement, observeLicense} from '@queries/servers/system';
|
||||
|
||||
import AnnouncementBanner from './announcement_banner';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const lastDismissed = observeLastDismissedAnnouncement(database);
|
||||
const bannerText = observeConfigValue(database, 'BannerText');
|
||||
const allowDismissal = observeConfigBooleanValue(database, 'AllowBannerDismissal');
|
||||
|
||||
const bannerDismissed = combineLatest([lastDismissed, bannerText, allowDismissal]).pipe(
|
||||
switchMap(([ld, bt, abd]) => of$(abd && (ld === bt))),
|
||||
);
|
||||
|
||||
const license = observeLicense(database);
|
||||
const enableBannerConfig = observeConfigBooleanValue(database, 'EnableBanner');
|
||||
const bannerEnabled = combineLatest([license, enableBannerConfig]).pipe(
|
||||
switchMap(([lcs, cfg]) => of$(cfg && lcs?.IsLicensed === 'true')),
|
||||
);
|
||||
return {
|
||||
bannerColor: observeConfigValue(database, 'BannerColor'),
|
||||
bannerEnabled,
|
||||
bannerText,
|
||||
bannerTextColor: observeConfigValue(database, 'BannerTextColor'),
|
||||
bannerDismissed,
|
||||
allowDismissal,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(AnnouncementBanner));
|
||||
@@ -3,8 +3,8 @@
|
||||
exports[`@components/app_version should match snapshot 1`] = `
|
||||
<View
|
||||
animatedStyle={
|
||||
{
|
||||
"value": {
|
||||
Object {
|
||||
"value": Object {
|
||||
"opacity": 1,
|
||||
},
|
||||
}
|
||||
@@ -12,14 +12,14 @@ exports[`@components/app_version should match snapshot 1`] = `
|
||||
collapsable={false}
|
||||
pointerEvents="none"
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"marginBottom": 12,
|
||||
"marginLeft": 20,
|
||||
@@ -29,7 +29,7 @@ exports[`@components/app_version should match snapshot 1`] = `
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {debounce} from 'lodash';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {Platform, SectionList, SectionListData, SectionListRenderItemInfo, StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import {searchGroupsByName, searchGroupsByNameInChannel, searchGroupsByNameInTeam} from '@actions/local/group';
|
||||
@@ -175,35 +175,9 @@ const makeSections = (teamMembers: Array<UserProfile | UserModel>, usersInChanne
|
||||
return newSections;
|
||||
};
|
||||
|
||||
const searchGroups = async (serverUrl: string, matchTerm: string, useGroupMentions: boolean, isChannelConstrained: boolean, isTeamConstrained: boolean, channelId?: string, teamId?: string) => {
|
||||
try {
|
||||
if (useGroupMentions && matchTerm && matchTerm !== '') {
|
||||
let g = emptyGroupList;
|
||||
|
||||
if (isChannelConstrained) {
|
||||
// If the channel is constrained, we only show groups for that channel
|
||||
if (channelId) {
|
||||
g = await searchGroupsByNameInChannel(serverUrl, matchTerm, channelId);
|
||||
}
|
||||
} else if (isTeamConstrained) {
|
||||
// If there is no channel constraint, but a team constraint - only show groups for team
|
||||
g = await searchGroupsByNameInTeam(serverUrl, matchTerm, teamId!);
|
||||
} else {
|
||||
// No constraints? Search all groups
|
||||
g = await searchGroupsByName(serverUrl, matchTerm || '');
|
||||
}
|
||||
|
||||
return g.length ? g : emptyGroupList;
|
||||
}
|
||||
return emptyGroupList;
|
||||
} catch (error) {
|
||||
return emptyGroupList;
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
channelId?: string;
|
||||
teamId: string;
|
||||
teamId?: string;
|
||||
cursorPosition: number;
|
||||
isSearch: boolean;
|
||||
updateValue: (v: string) => void;
|
||||
@@ -258,22 +232,9 @@ const AtMention = ({
|
||||
const [localUsers, setLocalUsers] = useState<UserModel[]>();
|
||||
const [filteredLocalUsers, setFilteredLocalUsers] = useState(emptyUserlList);
|
||||
|
||||
const latestSearchAt = useRef(0);
|
||||
|
||||
const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, groupMentions: boolean, channelConstrained: boolean, teamConstrained: boolean, tId: string, cId?: string) => {
|
||||
const searchAt = Date.now();
|
||||
latestSearchAt.current = searchAt;
|
||||
|
||||
const [{users: receivedUsers, error}, groupsResult] = await Promise.all([
|
||||
searchUsers(sUrl, term, tId, cId),
|
||||
searchGroups(sUrl, term, groupMentions, channelConstrained, teamConstrained, cId, tId),
|
||||
]);
|
||||
|
||||
if (latestSearchAt.current > searchAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
setGroups(groupsResult);
|
||||
const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, cId?: string) => {
|
||||
setLoading(true);
|
||||
const {users: receivedUsers, error} = await searchUsers(sUrl, term, cId);
|
||||
|
||||
setUseLocal(Boolean(error));
|
||||
if (error) {
|
||||
@@ -282,10 +243,6 @@ const AtMention = ({
|
||||
fallbackUsers = await getAllUsers(sUrl);
|
||||
setLocalUsers(fallbackUsers);
|
||||
}
|
||||
if (latestSearchAt.current > searchAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredUsers = filterResults(fallbackUsers, term);
|
||||
setFilteredLocalUsers(filteredUsers.length ? filteredUsers : emptyUserlList);
|
||||
} else if (receivedUsers) {
|
||||
@@ -313,12 +270,8 @@ const AtMention = ({
|
||||
const resetState = () => {
|
||||
setUsersInChannel(emptyUserlList);
|
||||
setUsersOutOfChannel(emptyUserlList);
|
||||
setGroups(emptyGroupList);
|
||||
setFilteredLocalUsers(emptyUserlList);
|
||||
setSections(emptySectionList);
|
||||
setNoResultsTerm(null);
|
||||
latestSearchAt.current = Date.now();
|
||||
setLoading(false);
|
||||
runSearch.cancel();
|
||||
};
|
||||
|
||||
@@ -332,7 +285,7 @@ const AtMention = ({
|
||||
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
|
||||
}
|
||||
|
||||
const newCursorPosition = completedDraft.length;
|
||||
const newCursorPosition = completedDraft.length - 1;
|
||||
|
||||
if (value.length > cursorPosition) {
|
||||
completedDraft += value.substring(cursorPosition);
|
||||
@@ -344,7 +297,6 @@ const AtMention = ({
|
||||
onShowingChange(false);
|
||||
setNoResultsTerm(mention);
|
||||
setSections(emptySectionList);
|
||||
latestSearchAt.current = Date.now();
|
||||
}, [value, localCursorPosition, isSearch]);
|
||||
|
||||
const renderSpecialMentions = useCallback((item: SpecialMention) => {
|
||||
@@ -409,6 +361,39 @@ const AtMention = ({
|
||||
}
|
||||
}, [cursorPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (useGroupMentions && matchTerm && matchTerm !== '') {
|
||||
// If the channel is constrained, we only show groups for that channel
|
||||
if (isChannelConstrained && channelId) {
|
||||
searchGroupsByNameInChannel(serverUrl, matchTerm, channelId).then((g) => {
|
||||
setGroups(g.length ? g : emptyGroupList);
|
||||
}).catch(() => {
|
||||
setGroups(emptyGroupList);
|
||||
});
|
||||
}
|
||||
|
||||
// If there is no channel constraint, but a team constraint - only show groups for team
|
||||
if (isTeamConstrained && !isChannelConstrained) {
|
||||
searchGroupsByNameInTeam(serverUrl, matchTerm, teamId!).then((g) => {
|
||||
setGroups(g.length ? g : emptyGroupList);
|
||||
}).catch(() => {
|
||||
setGroups(emptyGroupList);
|
||||
});
|
||||
}
|
||||
|
||||
// No constraints? Search all groups
|
||||
if (!isTeamConstrained && !isChannelConstrained) {
|
||||
searchGroupsByName(serverUrl, matchTerm || '').then((g) => {
|
||||
setGroups(Array.isArray(g) ? g : emptyGroupList);
|
||||
}).catch(() => {
|
||||
setGroups(emptyGroupList);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setGroups(emptyGroupList);
|
||||
}
|
||||
}, [matchTerm, useGroupMentions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (matchTerm === null) {
|
||||
resetState();
|
||||
@@ -421,14 +406,10 @@ const AtMention = ({
|
||||
}
|
||||
|
||||
setNoResultsTerm(null);
|
||||
setLoading(true);
|
||||
runSearch(serverUrl, matchTerm, useGroupMentions, isChannelConstrained, isTeamConstrained, teamId, channelId);
|
||||
}, [matchTerm, teamId, useGroupMentions, isChannelConstrained, isTeamConstrained]);
|
||||
runSearch(serverUrl, matchTerm, channelId);
|
||||
}, [matchTerm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (noResultsTerm && !loading) {
|
||||
return;
|
||||
}
|
||||
const showSpecialMentions = useChannelMentions && matchTerm != null && checkSpecialMentions(matchTerm);
|
||||
const buildMemberSection = isSearch || (!channelId && teamMembers.length > 0);
|
||||
let newSections;
|
||||
|
||||
@@ -18,11 +18,8 @@ import AtMention from './at_mention';
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type OwnProps = {
|
||||
channelId?: string;
|
||||
teamId?: string;
|
||||
}
|
||||
const enhanced = withObservables(['teamId'], ({database, channelId, teamId}: WithDatabaseArgs & OwnProps) => {
|
||||
type OwnProps = {channelId?: string}
|
||||
const enhanced = withObservables([], ({database, channelId}: WithDatabaseArgs & OwnProps) => {
|
||||
const currentUser = observeCurrentUser(database);
|
||||
|
||||
const hasLicense = observeLicense(database).pipe(
|
||||
@@ -54,19 +51,20 @@ const enhanced = withObservables(['teamId'], ({database, channelId, teamId}: Wit
|
||||
useGroupMentions = of$(false);
|
||||
isChannelConstrained = of$(false);
|
||||
isTeamConstrained = of$(false);
|
||||
team = teamId ? observeTeam(database, teamId) : observeCurrentTeam(database);
|
||||
team = observeCurrentTeam(database);
|
||||
}
|
||||
|
||||
isTeamConstrained = team.pipe(
|
||||
switchMap((t) => of$(Boolean(t?.isGroupConstrained))),
|
||||
);
|
||||
const teamId = team.pipe(switchMap((t) => of$(t?.id)));
|
||||
|
||||
return {
|
||||
isChannelConstrained,
|
||||
isTeamConstrained,
|
||||
useChannelMentions,
|
||||
useGroupMentions,
|
||||
teamId: team.pipe(switchMap((t) => of$(t?.id))),
|
||||
teamId,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ type Props = {
|
||||
availableSpace: SharedValue<number>;
|
||||
inPost?: boolean;
|
||||
growDown?: boolean;
|
||||
teamId?: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
@@ -82,7 +81,6 @@ const Autocomplete = ({
|
||||
inPost = false,
|
||||
growDown = false,
|
||||
containerStyle,
|
||||
teamId,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
@@ -115,7 +113,7 @@ const Autocomplete = ({
|
||||
}, [growDown, position]);
|
||||
|
||||
const containerStyles = useMemo(() => {
|
||||
const s: StyleProp<ViewStyle> = [style.base, containerAnimatedStyle];
|
||||
const s = [style.base, containerAnimatedStyle];
|
||||
if (hasElements) {
|
||||
s.push(style.borders);
|
||||
}
|
||||
@@ -154,7 +152,6 @@ const Autocomplete = ({
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
isSearch={isSearch}
|
||||
channelId={channelId}
|
||||
teamId={teamId}
|
||||
/>
|
||||
<ChannelMention
|
||||
cursorPosition={cursorPosition}
|
||||
@@ -164,8 +161,6 @@ const Autocomplete = ({
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
isSearch={isSearch}
|
||||
channelId={channelId}
|
||||
teamId={teamId}
|
||||
/>
|
||||
{!isSearch &&
|
||||
<EmojiSuggestion
|
||||
@@ -189,7 +184,6 @@ const Autocomplete = ({
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
isAppsEnabled={isAppsEnabled}
|
||||
/>
|
||||
}
|
||||
{/* {(isSearch && enableDateSuggestion) &&
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {debounce} from 'lodash';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {Platform, SectionList, SectionListData, SectionListRenderItemInfo, StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import {searchChannels} from '@actions/remote/channel';
|
||||
@@ -11,8 +11,11 @@ import ChannelMentionItem from '@components/autocomplete/channel_mention_item';
|
||||
import {General} from '@constants';
|
||||
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import {t} from '@i18n';
|
||||
import {queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import {hasTrailingSpaces} from '@utils/helpers';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
@@ -22,6 +25,32 @@ const keyExtractor = (item: Channel) => {
|
||||
return item.id;
|
||||
};
|
||||
|
||||
const getMatchTermForChannelMention = (() => {
|
||||
let lastMatchTerm: string | null = null;
|
||||
let lastValue: string;
|
||||
let lastIsSearch: boolean;
|
||||
return (value: string, isSearch: boolean) => {
|
||||
if (value !== lastValue || isSearch !== lastIsSearch) {
|
||||
const regex = isSearch ? CHANNEL_MENTION_SEARCH_REGEX : CHANNEL_MENTION_REGEX;
|
||||
const match = value.match(regex);
|
||||
lastValue = value;
|
||||
lastIsSearch = isSearch;
|
||||
if (match) {
|
||||
if (isSearch) {
|
||||
lastMatchTerm = match[1].toLowerCase();
|
||||
} else if (match.index && match.index > 0 && value[match.index - 1] === '~') {
|
||||
lastMatchTerm = null;
|
||||
} else {
|
||||
lastMatchTerm = match[2].toLowerCase();
|
||||
}
|
||||
} else {
|
||||
lastMatchTerm = null;
|
||||
}
|
||||
}
|
||||
return lastMatchTerm;
|
||||
};
|
||||
})();
|
||||
|
||||
const reduceChannelsForSearch = (channels: Array<Channel | ChannelModel>, members: MyChannelModel[]) => {
|
||||
const memberIds = new Set(members.map((m) => m.id));
|
||||
return channels.reduce<Array<Array<Channel | ChannelModel>>>(([pubC, priC, dms], c) => {
|
||||
@@ -54,7 +83,7 @@ const reduceChannelsForAutocomplete = (channels: Array<Channel | ChannelModel>,
|
||||
}, [[], []]);
|
||||
};
|
||||
|
||||
const makeSections = (channels: Array<Channel | ChannelModel>, myMembers: MyChannelModel[], loading: boolean, isSearch = false) => {
|
||||
const makeSections = (channels: Array<Channel | ChannelModel>, myMembers: MyChannelModel[], isSearch = false) => {
|
||||
const newSections = [];
|
||||
if (isSearch) {
|
||||
const [publicChannels, privateChannels, directAndGroupMessages] = reduceChannelsForSearch(channels, myMembers);
|
||||
@@ -99,7 +128,7 @@ const makeSections = (channels: Array<Channel | ChannelModel>, myMembers: MyChan
|
||||
});
|
||||
}
|
||||
|
||||
if (otherChannels.length || (!myChannels.length && loading)) {
|
||||
if (otherChannels.length) {
|
||||
newSections.push({
|
||||
id: t('suggestion.mention.morechannels'),
|
||||
defaultMessage: 'Other Channels',
|
||||
@@ -135,11 +164,18 @@ type Props = {
|
||||
value: string;
|
||||
nestedScrollEnabled: boolean;
|
||||
listStyle: StyleProp<ViewStyle>;
|
||||
matchTerm: string;
|
||||
localChannels: ChannelModel[];
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
const getAllChannels = async (serverUrl: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const teamId = await getCurrentTeamId(database);
|
||||
return queryAllChannelsForTeam(database, teamId).fetch();
|
||||
};
|
||||
|
||||
const emptySections: Array<SectionListData<Channel>> = [];
|
||||
const emptyChannels: Array<Channel | ChannelModel> = [];
|
||||
|
||||
@@ -152,45 +188,47 @@ const ChannelMention = ({
|
||||
value,
|
||||
nestedScrollEnabled,
|
||||
listStyle,
|
||||
matchTerm,
|
||||
localChannels,
|
||||
teamId,
|
||||
}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const [sections, setSections] = useState<Array<SectionListData<(Channel | ChannelModel)>>>(emptySections);
|
||||
const [remoteChannels, setRemoteChannels] = useState<Array<ChannelModel | Channel>>(emptyChannels);
|
||||
const [channels, setChannels] = useState<Array<ChannelModel | Channel>>(emptyChannels);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [noResultsTerm, setNoResultsTerm] = useState<string|null>(null);
|
||||
const [localCursorPosition, setLocalCursorPosition] = useState(cursorPosition); // To avoid errors due to delay between value changes and cursor position changes.
|
||||
const [useLocal, setUseLocal] = useState(true);
|
||||
const [localChannels, setlocalChannels] = useState<ChannelModel[]>();
|
||||
const [filteredLocalChannels, setFilteredLocalChannels] = useState(emptyChannels);
|
||||
|
||||
const latestSearchAt = useRef(0);
|
||||
const runSearch = useMemo(() => debounce(async (sUrl: string, term: string) => {
|
||||
setLoading(true);
|
||||
const {channels: receivedChannels, error} = await searchChannels(sUrl, term, isSearch);
|
||||
setUseLocal(Boolean(error));
|
||||
|
||||
const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, tId: string) => {
|
||||
const searchAt = Date.now();
|
||||
latestSearchAt.current = searchAt;
|
||||
|
||||
const {channels: receivedChannels} = await searchChannels(sUrl, term, tId, isSearch);
|
||||
|
||||
if (latestSearchAt.current > searchAt) {
|
||||
return;
|
||||
if (error) {
|
||||
let fallbackChannels = localChannels;
|
||||
if (!fallbackChannels) {
|
||||
fallbackChannels = await getAllChannels(sUrl);
|
||||
setlocalChannels(fallbackChannels);
|
||||
}
|
||||
const filteredChannels = filterResults(fallbackChannels, term);
|
||||
setFilteredLocalChannels(filteredChannels.length ? filteredChannels : emptyChannels);
|
||||
} else if (receivedChannels) {
|
||||
let channelsToStore: Array<Channel | ChannelModel> = receivedChannels;
|
||||
if (hasTrailingSpaces(term)) {
|
||||
channelsToStore = filterResults(receivedChannels, term);
|
||||
}
|
||||
setChannels(channelsToStore.length ? channelsToStore : emptyChannels);
|
||||
}
|
||||
let channelsToStore: Array<Channel | ChannelModel> = receivedChannels || [];
|
||||
if (hasTrailingSpaces(term)) {
|
||||
channelsToStore = filterResults(receivedChannels || [], term);
|
||||
}
|
||||
setRemoteChannels(channelsToStore.length ? channelsToStore : emptyChannels);
|
||||
|
||||
setLoading(false);
|
||||
}, 200), []);
|
||||
|
||||
const matchTerm = getMatchTermForChannelMention(value.substring(0, localCursorPosition), isSearch);
|
||||
const resetState = () => {
|
||||
latestSearchAt.current = Date.now();
|
||||
setRemoteChannels(emptyChannels);
|
||||
setFilteredLocalChannels(emptyChannels);
|
||||
setChannels(emptyChannels);
|
||||
setSections(emptySections);
|
||||
setNoResultsTerm(null);
|
||||
runSearch.cancel();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const completeMention = useCallback((mention: string) => {
|
||||
@@ -226,11 +264,8 @@ const ChannelMention = ({
|
||||
}
|
||||
|
||||
onShowingChange(false);
|
||||
setLoading(false);
|
||||
setNoResultsTerm(mention);
|
||||
setSections(emptySections);
|
||||
setRemoteChannels(emptyChannels);
|
||||
latestSearchAt.current = Date.now();
|
||||
}, [value, localCursorPosition, isSearch]);
|
||||
|
||||
const renderItem = useCallback(({item}: SectionListRenderItemInfo<Channel | ChannelModel>) => {
|
||||
@@ -271,41 +306,21 @@ const ChannelMention = ({
|
||||
}
|
||||
|
||||
setNoResultsTerm(null);
|
||||
setLoading(true);
|
||||
runSearch(serverUrl, matchTerm, teamId);
|
||||
}, [matchTerm, teamId]);
|
||||
|
||||
const channels = useMemo(() => {
|
||||
const ids = new Set(localChannels.map((c) => c.id));
|
||||
return [...localChannels, ...remoteChannels.filter((c) => !ids.has(c.id))].sort((a, b) => {
|
||||
const aDisplay = 'display_name' in a ? a.display_name : a.displayName;
|
||||
const bDisplay = 'display_name' in b ? b.display_name : b.displayName;
|
||||
const displayResult = aDisplay.localeCompare(bDisplay);
|
||||
if (displayResult === 0) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
return displayResult;
|
||||
});
|
||||
}, [localChannels, remoteChannels]);
|
||||
runSearch(serverUrl, matchTerm);
|
||||
}, [matchTerm]);
|
||||
|
||||
useDidUpdate(() => {
|
||||
if (noResultsTerm && !loading) {
|
||||
return;
|
||||
}
|
||||
const newSections = makeSections(channels, myMembers, loading, isSearch);
|
||||
const newSections = makeSections(useLocal ? filteredLocalChannels : channels, myMembers, isSearch);
|
||||
const nSections = newSections.length;
|
||||
|
||||
if (!loading && !nSections && noResultsTerm == null) {
|
||||
setNoResultsTerm(matchTerm);
|
||||
}
|
||||
if (nSections) {
|
||||
setNoResultsTerm(null);
|
||||
}
|
||||
setSections(newSections.length ? newSections : emptySections);
|
||||
onShowingChange(Boolean(nSections));
|
||||
}, [channels, myMembers, loading]);
|
||||
|
||||
if (!loading && (sections.length === 0 || noResultsTerm != null)) {
|
||||
if (sections.length === 0 || noResultsTerm != null) {
|
||||
// If we are not in an active state or the mention has been completed return null so nothing is rendered
|
||||
// other components are not blocked.
|
||||
return null;
|
||||
|
||||
@@ -3,90 +3,17 @@
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
|
||||
import {observeChannel, queryAllMyChannel, queryChannelsForAutocomplete} from '@queries/servers/channel';
|
||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||
import {queryAllMyChannel} from '@queries/servers/channel';
|
||||
|
||||
import ChannelMention from './channel_mention';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
const getMatchTermForChannelMention = (() => {
|
||||
let lastMatchTerm: string | null = null;
|
||||
let lastValue: string;
|
||||
let lastIsSearch: boolean;
|
||||
return (value: string, isSearch: boolean) => {
|
||||
if (value !== lastValue || isSearch !== lastIsSearch) {
|
||||
const regex = isSearch ? CHANNEL_MENTION_SEARCH_REGEX : CHANNEL_MENTION_REGEX;
|
||||
const match = value.match(regex);
|
||||
lastValue = value;
|
||||
lastIsSearch = isSearch;
|
||||
if (match) {
|
||||
if (isSearch) {
|
||||
lastMatchTerm = match[1].toLowerCase();
|
||||
} else if (match.index && match.index > 0 && value[match.index - 1] === '~') {
|
||||
lastMatchTerm = null;
|
||||
} else {
|
||||
lastMatchTerm = match[2].toLowerCase();
|
||||
}
|
||||
} else {
|
||||
lastMatchTerm = null;
|
||||
}
|
||||
}
|
||||
return lastMatchTerm;
|
||||
};
|
||||
})();
|
||||
|
||||
type WithTeamIdProps = {
|
||||
teamId?: string;
|
||||
channelId?: string;
|
||||
} & WithDatabaseArgs;
|
||||
|
||||
type OwnProps = {
|
||||
value: string;
|
||||
isSearch: boolean;
|
||||
cursorPosition: number;
|
||||
teamId: string;
|
||||
} & WithDatabaseArgs;
|
||||
|
||||
const emptyChannelList: ChannelModel[] = [];
|
||||
|
||||
const withMembers = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
return {
|
||||
myMembers: queryAllMyChannel(database).observe(),
|
||||
};
|
||||
});
|
||||
|
||||
const withTeamId = withObservables(['teamId', 'channelId'], ({teamId, channelId, database}: WithTeamIdProps) => {
|
||||
let currentTeamId;
|
||||
if (teamId) {
|
||||
currentTeamId = of$(teamId);
|
||||
} else if (channelId) {
|
||||
currentTeamId = observeChannel(database, channelId).pipe(switchMap((c) => {
|
||||
return c?.teamId ? of$(c.teamId) : observeCurrentTeamId(database);
|
||||
}));
|
||||
} else {
|
||||
currentTeamId = observeCurrentTeamId(database);
|
||||
}
|
||||
|
||||
return {
|
||||
teamId: currentTeamId,
|
||||
};
|
||||
});
|
||||
|
||||
const enhanced = withObservables(['value', 'isSearch', 'teamId', 'cursorPosition'], ({value, isSearch, teamId, cursorPosition, database}: OwnProps) => {
|
||||
const matchTerm = getMatchTermForChannelMention(value.substring(0, cursorPosition), isSearch);
|
||||
|
||||
const localChannels = matchTerm === null ? of$(emptyChannelList) : queryChannelsForAutocomplete(database, matchTerm, isSearch, teamId).observe();
|
||||
|
||||
return {
|
||||
matchTerm: of$(matchTerm),
|
||||
localChannels,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(withMembers(withTeamId(enhanced(ChannelMention))));
|
||||
export default withDatabase(enhanced(ChannelMention));
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
|
||||
import Autocomplete from './autocomplete';
|
||||
|
||||
type OwnProps = {
|
||||
serverUrl?: string;
|
||||
}
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables(['serverUrl'], ({serverUrl}: OwnProps) => ({
|
||||
isAppsEnabled: serverUrl ? AppsManager.observeIsAppsEnabled(serverUrl) : of$(false),
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
isAppsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
|
||||
}));
|
||||
|
||||
export default enhanced(Autocomplete);
|
||||
export default withDatabase(enhanced(Autocomplete));
|
||||
|
||||
@@ -7,9 +7,9 @@ import {IntlShape} from 'react-intl';
|
||||
import {doAppFetchForm, doAppLookup} from '@actions/remote/apps';
|
||||
import {fetchChannelById, fetchChannelByName, searchChannels} from '@actions/remote/channel';
|
||||
import {fetchUsersByIds, fetchUsersByUsernames, searchUsers} from '@actions/remote/user';
|
||||
import {AppBindingLocations, AppCallResponseTypes, AppFieldTypes, COMMAND_SUGGESTION_ERROR} from '@constants/apps';
|
||||
import {AppCallResponseTypes, AppFieldTypes, COMMAND_SUGGESTION_ERROR} from '@constants/apps';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import {getChannelById, getChannelByName} from '@queries/servers/channel';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getUserById, queryUsersByUsername} from '@queries/servers/user';
|
||||
@@ -173,7 +173,7 @@ export class ParsedCommand {
|
||||
}
|
||||
|
||||
case ParseState.EndCommand: {
|
||||
const binding = bindings.find(this.findBindings, this);
|
||||
const binding = bindings.find(this.findBindings);
|
||||
if (!binding) {
|
||||
// gone as far as we could, this token doesn't match a sub-command.
|
||||
// return the state from the last matching binding
|
||||
@@ -856,13 +856,15 @@ export class AppCommandParser {
|
||||
private teamID: string;
|
||||
private rootPostID?: string;
|
||||
private intl: IntlShape;
|
||||
private theme: Theme;
|
||||
|
||||
constructor(serverUrl: string, intl: IntlShape, channelID: string, teamID = '', rootPostID = '') {
|
||||
constructor(serverUrl: string, intl: IntlShape, channelID: string, teamID = '', rootPostID = '', theme: Theme) {
|
||||
this.serverUrl = serverUrl;
|
||||
this.channelID = channelID;
|
||||
this.rootPostID = rootPostID;
|
||||
this.teamID = teamID;
|
||||
this.intl = intl;
|
||||
this.theme = theme;
|
||||
|
||||
// We are making the assumption the database is always present at this level.
|
||||
// This assumption may not be correct. Please review.
|
||||
@@ -984,9 +986,6 @@ export class AppCommandParser {
|
||||
break;
|
||||
}
|
||||
user = res.users[0] || res.existingUsers[0];
|
||||
if (!user) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
parsed.values[f.name] = user.username;
|
||||
break;
|
||||
@@ -1001,9 +1000,6 @@ export class AppCommandParser {
|
||||
break;
|
||||
}
|
||||
channel = res.channel;
|
||||
if (!channel) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
parsed.values[f.name] = channel.name;
|
||||
break;
|
||||
@@ -1026,6 +1022,7 @@ export class AppCommandParser {
|
||||
const result: AutocompleteSuggestion[] = [];
|
||||
|
||||
const bindings = this.getCommandBindings();
|
||||
|
||||
for (const binding of bindings) {
|
||||
let base = binding.label;
|
||||
if (!base) {
|
||||
@@ -1182,21 +1179,14 @@ export class AppCommandParser {
|
||||
|
||||
const errors: {[key: string]: string} = {};
|
||||
await Promise.all(parsed.resolvedForm.fields.map(async (f) => {
|
||||
const fieldValue = values[f.name];
|
||||
if (!fieldValue) {
|
||||
if (!values[f.name]) {
|
||||
return;
|
||||
}
|
||||
switch (f.type) {
|
||||
case AppFieldTypes.DYNAMIC_SELECT:
|
||||
if (f.multiselect) {
|
||||
let commandValues: string[] = [];
|
||||
if (Array.isArray(fieldValue)) {
|
||||
commandValues = fieldValue as string[];
|
||||
} else {
|
||||
commandValues = [fieldValue] as string[];
|
||||
}
|
||||
|
||||
if (f.multiselect && Array.isArray(values[f.name])) {
|
||||
const options: AppSelectOption[] = [];
|
||||
const commandValues = values[f.name] as string[];
|
||||
for (const value of commandValues) {
|
||||
if (options.find((o) => o.value === value)) {
|
||||
errors[f.name] = this.intl.formatMessage({
|
||||
@@ -1212,7 +1202,7 @@ export class AppCommandParser {
|
||||
break;
|
||||
}
|
||||
|
||||
values[f.name] = {label: fieldValue, value: fieldValue};
|
||||
values[f.name] = {label: values[f.name], value: values[f.name]};
|
||||
break;
|
||||
case AppFieldTypes.STATIC_SELECT: {
|
||||
const getOption = (value: string) => {
|
||||
@@ -1230,15 +1220,9 @@ export class AppCommandParser {
|
||||
values[f.name] = undefined;
|
||||
};
|
||||
|
||||
if (f.multiselect) {
|
||||
let commandValues: string[] = [];
|
||||
if (Array.isArray(fieldValue)) {
|
||||
commandValues = fieldValue as string[];
|
||||
} else {
|
||||
commandValues = [fieldValue] as string[];
|
||||
}
|
||||
|
||||
if (f.multiselect && Array.isArray(values[f.name])) {
|
||||
const options: AppSelectOption[] = [];
|
||||
const commandValues = values[f.name] as string[];
|
||||
for (const value of commandValues) {
|
||||
const option = getOption(value);
|
||||
if (!option) {
|
||||
@@ -1260,9 +1244,9 @@ export class AppCommandParser {
|
||||
break;
|
||||
}
|
||||
|
||||
const option = getOption(fieldValue);
|
||||
const option = getOption(values[f.name]);
|
||||
if (!option) {
|
||||
setOptionError(fieldValue);
|
||||
setOptionError(values[f.name]);
|
||||
return;
|
||||
}
|
||||
values[f.name] = option;
|
||||
@@ -1291,15 +1275,9 @@ export class AppCommandParser {
|
||||
});
|
||||
};
|
||||
|
||||
if (f.multiselect) {
|
||||
let commandValues: string[] = [];
|
||||
if (Array.isArray(fieldValue)) {
|
||||
commandValues = fieldValue as string[];
|
||||
} else {
|
||||
commandValues = [fieldValue] as string[];
|
||||
}
|
||||
|
||||
if (f.multiselect && Array.isArray(values[f.name])) {
|
||||
const options: AppSelectOption[] = [];
|
||||
const commandValues = values[f.name] as string[];
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const value of commandValues) {
|
||||
let userName = value;
|
||||
@@ -1363,15 +1341,9 @@ export class AppCommandParser {
|
||||
});
|
||||
};
|
||||
|
||||
if (f.multiselect) {
|
||||
let commandValues: string[] = [];
|
||||
if (Array.isArray(fieldValue)) {
|
||||
commandValues = fieldValue as string[];
|
||||
} else {
|
||||
commandValues = [fieldValue] as string[];
|
||||
}
|
||||
|
||||
if (f.multiselect && Array.isArray(values[f.name])) {
|
||||
const options: AppSelectOption[] = [];
|
||||
const commandValues = values[f.name] as string[];
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const value of commandValues) {
|
||||
let channelName = value;
|
||||
@@ -1463,7 +1435,11 @@ export class AppCommandParser {
|
||||
// getCommandBindings returns the commands in the redux store.
|
||||
// They are grouped by app id since each app has one base command
|
||||
private getCommandBindings = (): AppBinding[] => {
|
||||
return AppsManager.getBindings(this.serverUrl, AppBindingLocations.COMMAND, Boolean(this.rootPostID));
|
||||
const manager = IntegrationsManager.getManager(this.serverUrl);
|
||||
if (this.rootPostID) {
|
||||
return manager.getRHSCommandBindings();
|
||||
}
|
||||
return manager.getCommandBindings();
|
||||
};
|
||||
|
||||
// getChannel gets the channel in which the user is typing the command
|
||||
@@ -1562,9 +1538,10 @@ export class AppCommandParser {
|
||||
};
|
||||
|
||||
public getSubmittableForm = async (location: string, binding: AppBinding): Promise<{form?: AppForm; error?: string} | undefined> => {
|
||||
const manager = IntegrationsManager.getManager(this.serverUrl);
|
||||
const rootID = this.rootPostID || '';
|
||||
const key = `${this.channelID}-${rootID}-${location}`;
|
||||
const submittableForm = AppsManager.getCommandForm(this.serverUrl, key, Boolean(this.rootPostID));
|
||||
const submittableForm = this.rootPostID ? manager.getAppRHSCommandForm(key) : manager.getAppCommandForm(key);
|
||||
if (submittableForm) {
|
||||
return {form: submittableForm};
|
||||
}
|
||||
@@ -1578,7 +1555,11 @@ export class AppCommandParser {
|
||||
const context = await this.getAppContext(binding);
|
||||
const fetched = await this.fetchSubmittableForm(binding.form.source, context);
|
||||
if (fetched?.form) {
|
||||
AppsManager.setCommandForm(this.serverUrl, key, fetched.form, Boolean(this.rootPostID));
|
||||
if (this.rootPostID) {
|
||||
manager.setAppRHSCommandForm(key, fetched.form);
|
||||
} else {
|
||||
manager.setAppCommandForm(key, fetched.form);
|
||||
}
|
||||
}
|
||||
return fetched;
|
||||
};
|
||||
@@ -1715,13 +1696,7 @@ export class AppCommandParser {
|
||||
prefix = '';
|
||||
}
|
||||
|
||||
const applicable = parsed.resolvedForm.fields.filter((field) => (
|
||||
field.label &&
|
||||
field.label.toLowerCase().startsWith(parsed.incomplete.toLowerCase()) &&
|
||||
!parsed.values[field.name] &&
|
||||
!field.readonly &&
|
||||
field.type !== AppFieldTypes.MARKDOWN
|
||||
));
|
||||
const applicable = parsed.resolvedForm.fields.filter((field) => field.label && field.label.toLowerCase().startsWith(parsed.incomplete.toLowerCase()) && !parsed.values[field.name]);
|
||||
if (applicable) {
|
||||
return applicable.map((f) => {
|
||||
return {
|
||||
@@ -1924,7 +1899,7 @@ export class AppCommandParser {
|
||||
if (input[0] === '@') {
|
||||
input = input.substring(1);
|
||||
}
|
||||
const res = await searchUsers(this.serverUrl, input, this.teamID, this.channelID);
|
||||
const res = await searchUsers(this.serverUrl, input, this.channelID);
|
||||
return getUserSuggestions(res.users);
|
||||
};
|
||||
|
||||
@@ -1933,7 +1908,7 @@ export class AppCommandParser {
|
||||
if (input[0] === '~') {
|
||||
input = input.substring(1);
|
||||
}
|
||||
const res = await searchChannels(this.serverUrl, input, this.teamID);
|
||||
const res = await searchChannels(this.serverUrl, input);
|
||||
return getChannelSuggestions(res.channels);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function inTextMentionSuggestions(serverUrl: string, pretext: strin
|
||||
const incompleteLessLastWord = separatedWords.slice(0, -1).join(' ');
|
||||
const lastWord = separatedWords[separatedWords.length - 1];
|
||||
if (lastWord.startsWith('@')) {
|
||||
const res = await searchUsers(serverUrl, lastWord.substring(1), teamID, channelID);
|
||||
const res = await searchUsers(serverUrl, lastWord.substring(1), channelID);
|
||||
const users = await getUserSuggestions(res.users);
|
||||
users.forEach((u) => {
|
||||
let complete = incompleteLessLastWord ? incompleteLessLastWord + ' ' + u.Complete : u.Complete;
|
||||
@@ -23,7 +23,7 @@ export async function inTextMentionSuggestions(serverUrl: string, pretext: strin
|
||||
}
|
||||
|
||||
if (lastWord.startsWith('~') && !lastWord.startsWith('~~')) {
|
||||
const res = await searchChannels(serverUrl, lastWord.substring(1), teamID);
|
||||
const res = await searchChannels(serverUrl, lastWord.substring(1));
|
||||
const channels = await getChannelSuggestions(res.channels);
|
||||
channels.forEach((c) => {
|
||||
let complete = incompleteLessLastWord ? incompleteLessLastWord + ' ' + c.Complete : c.Complete;
|
||||
|
||||
@@ -10,11 +10,15 @@ import AtMentionItem from '@components/autocomplete/at_mention_item';
|
||||
import ChannelMentionItem from '@components/autocomplete/channel_mention_item';
|
||||
import {COMMAND_SUGGESTION_CHANNEL, COMMAND_SUGGESTION_USER} from '@constants/apps';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import analytics from '@managers/analytics';
|
||||
|
||||
import {AppCommandParser, ExtendedAutocompleteSuggestion} from '../app_command_parser/app_command_parser';
|
||||
import SlashSuggestionItem from '../slash_suggestion_item';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export type Props = {
|
||||
currentTeamId: string;
|
||||
isSearch?: boolean;
|
||||
@@ -28,20 +32,7 @@ export type Props = {
|
||||
listStyle: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
const keyExtractor = (item: ExtendedAutocompleteSuggestion): string => {
|
||||
switch (item.type) {
|
||||
case COMMAND_SUGGESTION_USER: {
|
||||
const user = item.item as UserProfile;
|
||||
return user.id;
|
||||
}
|
||||
case COMMAND_SUGGESTION_CHANNEL: {
|
||||
const channel = item.item as Channel;
|
||||
return channel.id;
|
||||
}
|
||||
default:
|
||||
return item.Suggestion;
|
||||
}
|
||||
};
|
||||
const keyExtractor = (item: ExtendedAutocompleteSuggestion): string => item.Suggestion + item.type + item.item;
|
||||
|
||||
const emptySuggestonList: AutocompleteSuggestion[] = [];
|
||||
|
||||
@@ -57,8 +48,9 @@ const AppSlashSuggestion = ({
|
||||
listStyle,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId));
|
||||
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme));
|
||||
const [dataSource, setDataSource] = useState<AutocompleteSuggestion[]>(emptySuggestonList);
|
||||
const active = isAppsEnabled && Boolean(dataSource.length);
|
||||
const mounted = useRef(false);
|
||||
@@ -106,34 +98,28 @@ const AppSlashSuggestion = ({
|
||||
|
||||
const renderItem = useCallback(({item}: {item: ExtendedAutocompleteSuggestion}) => {
|
||||
switch (item.type) {
|
||||
case COMMAND_SUGGESTION_USER: {
|
||||
const user = item.item as UserProfile | undefined;
|
||||
if (!user) {
|
||||
case COMMAND_SUGGESTION_USER:
|
||||
if (!item.item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AtMentionItem
|
||||
user={user}
|
||||
user={item.item as UserProfile | UserModel}
|
||||
onPress={completeIgnoringSuggestion(item.Complete)}
|
||||
testID='autocomplete.slash_suggestion.at_mention_item'
|
||||
/>
|
||||
);
|
||||
}
|
||||
case COMMAND_SUGGESTION_CHANNEL: {
|
||||
const channel = item.item as Channel | undefined;
|
||||
if (!channel) {
|
||||
case COMMAND_SUGGESTION_CHANNEL:
|
||||
if (!item.item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChannelMentionItem
|
||||
channel={channel}
|
||||
channel={item.item as Channel | ChannelModel}
|
||||
onPress={completeIgnoringSuggestion(item.Complete)}
|
||||
testID='autocomplete.slash_suggestion.channel_mention_item'
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<SlashSuggestionItem
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||
import {observeConfigBooleanValue, observeCurrentTeamId} from '@queries/servers/system';
|
||||
|
||||
import SlashSuggestion from './slash_suggestion';
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
isAppsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(SlashSuggestion));
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
|
||||
import {fetchSuggestions} from '@actions/remote/command';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import analytics from '@managers/analytics';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
|
||||
@@ -77,8 +78,9 @@ const SlashSuggestion = ({
|
||||
listStyle,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId));
|
||||
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme));
|
||||
const mounted = useRef(false);
|
||||
const [noResultsTerm, setNoResultsTerm] = useState<string|null>(null);
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
iconColor: {
|
||||
tintColor: theme.centerChannelColor,
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -107,8 +110,7 @@ const SlashSuggestionItem = ({
|
||||
|
||||
let image = (
|
||||
<FastImage
|
||||
tintColor={theme.centerChannelColor}
|
||||
style={style.slashIcon}
|
||||
style={[style.iconColor, style.slashIcon]}
|
||||
source={slashIcon}
|
||||
/>
|
||||
);
|
||||
|
||||