Compare commits
5 Commits
migrate-to
...
hh_ship-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a5da8d625 | ||
|
|
70d5cb068c | ||
|
|
c6af59b388 | ||
|
|
97cdac1d40 | ||
|
|
b9376ad429 |
@@ -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>>
|
||||
|
||||
@@ -28,7 +28,7 @@ executors:
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
macos:
|
||||
xcode: "14.0.0"
|
||||
xcode: "13.3.0"
|
||||
working_directory: ~/mattermost-mobile
|
||||
shell: /bin/bash --login -o pipefail
|
||||
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,16 +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
|
||||
node node_modules/react-native-webrtc/tools/downloadWebRTC.js
|
||||
command: NODE_ENV=development npm ci --ignore-scripts
|
||||
- save_cache:
|
||||
name: Save npm cache
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
@@ -310,13 +307,13 @@ jobs:
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
|
||||
build-android-release:
|
||||
executor: android
|
||||
steps:
|
||||
- build-android
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
# build-android-release:
|
||||
# executor: android
|
||||
# steps:
|
||||
# - build-android
|
||||
# - persist
|
||||
# - save:
|
||||
# filename: "*.apk"
|
||||
|
||||
build-android-pr:
|
||||
executor: android
|
||||
@@ -327,27 +324,27 @@ jobs:
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
|
||||
build-android-unsigned:
|
||||
executor: android
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: android
|
||||
- gradle-dependencies
|
||||
- run:
|
||||
name: Jetify Android libraries
|
||||
command: ./node_modules/.bin/jetify
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build unsigned android
|
||||
no_output_timeout: 30m
|
||||
command: bundle exec fastlane android unsigned
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
# build-android-unsigned:
|
||||
# executor: android
|
||||
# steps:
|
||||
# - checkout:
|
||||
# path: ~/mattermost-mobile
|
||||
# - npm-dependencies
|
||||
# - assets
|
||||
# - fastlane-dependencies:
|
||||
# for: android
|
||||
# - gradle-dependencies
|
||||
# - run:
|
||||
# name: Jetify Android libraries
|
||||
# command: ./node_modules/.bin/jetify
|
||||
# - run:
|
||||
# working_directory: fastlane
|
||||
# name: Run fastlane to build unsigned android
|
||||
# no_output_timeout: 30m
|
||||
# command: bundle exec fastlane android unsigned
|
||||
# - persist
|
||||
# - save:
|
||||
# filename: "*.apk"
|
||||
|
||||
build-ios-beta:
|
||||
executor:
|
||||
@@ -359,13 +356,13 @@ jobs:
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
|
||||
build-ios-release:
|
||||
executor: ios
|
||||
steps:
|
||||
- build-ios
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
# build-ios-release:
|
||||
# executor: ios
|
||||
# steps:
|
||||
# - build-ios
|
||||
# - persist
|
||||
# - save:
|
||||
# filename: "*.ipa"
|
||||
|
||||
build-ios-pr:
|
||||
executor: ios
|
||||
@@ -376,64 +373,63 @@ jobs:
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
|
||||
build-ios-unsigned:
|
||||
executor: ios
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- pods-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: ios
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build unsigned iOS
|
||||
no_output_timeout: 30m
|
||||
command: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
bundle exec fastlane ios unsigned
|
||||
- persist_to_workspace:
|
||||
root: ~/
|
||||
paths:
|
||||
- mattermost-mobile/*.ipa
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
# build-ios-unsigned:
|
||||
# executor: ios
|
||||
# steps:
|
||||
# - checkout:
|
||||
# path: ~/mattermost-mobile
|
||||
# - npm-dependencies
|
||||
# - pods-dependencies
|
||||
# - assets
|
||||
# - fastlane-dependencies:
|
||||
# for: ios
|
||||
# - run:
|
||||
# working_directory: fastlane
|
||||
# name: Run fastlane to build unsigned iOS
|
||||
# no_output_timeout: 30m
|
||||
# command: |
|
||||
# HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
# bundle exec fastlane ios unsigned
|
||||
# - persist_to_workspace:
|
||||
# root: ~/
|
||||
# paths:
|
||||
# - mattermost-mobile/*.ipa
|
||||
# - save:
|
||||
# filename: "*.ipa"
|
||||
|
||||
build-ios-simulator:
|
||||
executor: ios
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- pods-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: ios
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build unsigned x86_64 iOS app for iPhone simulator
|
||||
no_output_timeout: 30m
|
||||
command: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
bundle exec fastlane ios simulator
|
||||
- persist_to_workspace:
|
||||
root: ~/
|
||||
paths:
|
||||
- mattermost-mobile/Mattermost-simulator-x86_64.app.zip
|
||||
- save:
|
||||
filename: "Mattermost-simulator-x86_64.app.zip"
|
||||
# build-ios-simulator:
|
||||
# executor: ios
|
||||
# steps:
|
||||
# - checkout:
|
||||
# path: ~/mattermost-mobile
|
||||
# - npm-dependencies
|
||||
# - pods-dependencies
|
||||
# - assets
|
||||
# - fastlane-dependencies:
|
||||
# for: ios
|
||||
# - run:
|
||||
# working_directory: fastlane
|
||||
# name: Run fastlane to build unsigned x86_64 iOS app for iPhone simulator
|
||||
# no_output_timeout: 30m
|
||||
# command: |
|
||||
# HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
# bundle exec fastlane ios simulator
|
||||
# - persist_to_workspace:
|
||||
# root: ~/
|
||||
# paths:
|
||||
# - mattermost-mobile/Mattermost-simulator-x86_64.app.zip
|
||||
# - save:
|
||||
# filename: "Mattermost-simulator-x86_64.app.zip"
|
||||
|
||||
deploy-android-release:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- deploy-to-store:
|
||||
task: "Deploy to Google Play"
|
||||
target: android
|
||||
file: "*.apk"
|
||||
env: "SUPPLY_TRACK=beta"
|
||||
# deploy-android-release:
|
||||
# executor:
|
||||
# name: android
|
||||
# resource_class: medium
|
||||
# steps:
|
||||
# - deploy-to-store:
|
||||
# task: "Deploy to Google Play"
|
||||
# target: android
|
||||
# file: "*.apk"
|
||||
|
||||
deploy-android-beta:
|
||||
executor:
|
||||
@@ -446,14 +442,13 @@ jobs:
|
||||
file: "*.apk"
|
||||
env: "SUPPLY_TRACK=alpha"
|
||||
|
||||
deploy-ios-release:
|
||||
executor: ios
|
||||
steps:
|
||||
- deploy-to-store:
|
||||
task: "Deploy to TestFlight"
|
||||
target: ios
|
||||
file: "*.ipa"
|
||||
env: ""
|
||||
# deploy-ios-release:
|
||||
# executor: ios
|
||||
# steps:
|
||||
# - deploy-to-store:
|
||||
# task: "Deploy to TestFlight"
|
||||
# target: ios
|
||||
# file: "*.ipa"
|
||||
|
||||
deploy-ios-beta:
|
||||
executor: ios
|
||||
@@ -464,17 +459,17 @@ jobs:
|
||||
file: "*.ipa"
|
||||
env: ""
|
||||
|
||||
github-release:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run:
|
||||
name: Create GitHub release
|
||||
working_directory: fastlane
|
||||
command: bundle exec fastlane github
|
||||
# github-release:
|
||||
# executor:
|
||||
# name: android
|
||||
# resource_class: medium
|
||||
# steps:
|
||||
# - attach_workspace:
|
||||
# at: ~/
|
||||
# - run:
|
||||
# name: Create GitHub release
|
||||
# working_directory: fastlane
|
||||
# command: bundle exec fastlane github
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
@@ -486,24 +481,26 @@ workflows:
|
||||
# requires:
|
||||
# - test
|
||||
|
||||
- build-android-release:
|
||||
context: mattermost-mobile-android-release
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-release-\d+$/
|
||||
- /^build-android-release-\d+$/
|
||||
- deploy-android-release:
|
||||
context: mattermost-mobile-android-release
|
||||
requires:
|
||||
- build-android-release
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-release-\d+$/
|
||||
- /^build-android-release-\d+$/
|
||||
# - build-android-release:
|
||||
# context: mattermost-mobile-android-release
|
||||
# requires:
|
||||
# - test
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - /^build-\d+$/
|
||||
# - /^build-android-\d+$/
|
||||
# - /^build-android-release-\d+$/
|
||||
# - deploy-android-release:
|
||||
# context: mattermost-mobile-android-release
|
||||
# requires:
|
||||
# - build-android-release
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - /^build-\d+$/
|
||||
# - /^build-android-\d+$/
|
||||
# - /^build-android-release-\d+$/
|
||||
|
||||
- build-android-beta:
|
||||
context: mattermost-mobile-android-beta
|
||||
@@ -524,24 +521,26 @@ workflows:
|
||||
- /^build-android-\d+$/
|
||||
- /^build-android-beta-\d+$/
|
||||
|
||||
- build-ios-release:
|
||||
context: mattermost-mobile-ios-release
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-release-\d+$/
|
||||
- /^build-ios-release-\d+$/
|
||||
- deploy-ios-release:
|
||||
context: mattermost-mobile-ios-release
|
||||
requires:
|
||||
- build-ios-release
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-release-\d+$/
|
||||
- /^build-ios-release-\d+$/
|
||||
# - build-ios-release:
|
||||
# context: mattermost-mobile-ios-release
|
||||
# requires:
|
||||
# - test
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - /^build-\d+$/
|
||||
# - /^build-ios-\d+$/
|
||||
# - /^build-ios-release-\d+$/
|
||||
# - deploy-ios-release:
|
||||
# context: mattermost-mobile-ios-release
|
||||
# requires:
|
||||
# - build-ios-release
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - /^build-\d+$/
|
||||
# - /^build-ios-\d+$/
|
||||
# - /^build-ios-release-\d+$/
|
||||
|
||||
- build-ios-beta:
|
||||
context: mattermost-mobile-ios-beta
|
||||
@@ -577,41 +576,43 @@ workflows:
|
||||
branches:
|
||||
only: /^(build|ios)-pr-.*/
|
||||
|
||||
- build-android-unsigned:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
only: unsigned
|
||||
- build-ios-unsigned:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
only: unsigned
|
||||
- build-ios-simulator:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-ios-sim-\d+$/
|
||||
|
||||
- github-release:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- build-android-unsigned
|
||||
- build-ios-unsigned
|
||||
filters:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
only: unsigned
|
||||
# - build-android-unsigned:
|
||||
# context: mattermost-mobile-unsigned
|
||||
# requires:
|
||||
# - test
|
||||
# filters:
|
||||
# tags:
|
||||
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
# branches:
|
||||
# only: unsigned
|
||||
# - build-ios-unsigned:
|
||||
# context: mattermost-mobile-unsigned
|
||||
# requires:
|
||||
# - test
|
||||
# filters:
|
||||
# tags:
|
||||
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
# branches:
|
||||
# only: unsigned
|
||||
# - build-ios-simulator:
|
||||
# context: mattermost-mobile-unsigned
|
||||
# requires:
|
||||
# - test
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - /^build-\d+$/
|
||||
# - /^build-ios-\d+$/
|
||||
# - /^build-ios-beta-\d+$/
|
||||
# - /^build-ios-sim-\d+$/
|
||||
|
||||
# - github-release:
|
||||
# context: mattermost-mobile-unsigned
|
||||
# requires:
|
||||
# - build-android-unsigned
|
||||
# - build-ios-unsigned
|
||||
# filters:
|
||||
# tags:
|
||||
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
# branches:
|
||||
# only: unsigned
|
||||
|
||||
@@ -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": {
|
||||
@@ -68,7 +68,7 @@
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "{@(@actions|@app|@assets|@calls|@client|@components|@constants|@context|@database|@helpers|@hooks|@init|@managers|@queries|@screens|@selectors|@share|@store|@telemetry|@typings|@test|@utils)/**,@(@constants|@i18n|@notifications|@store|@websocket)}",
|
||||
"pattern": "{@(@actions|@app|@assets|@client|@components|@constants|@context|@database|@helpers|@hooks|@init|@managers|@queries|@screens|@selectors|@share|@store|@telemetry|@typings|@test|@utils)/**,@(@constants|@i18n|@notifications|@store|@websocket)}",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
|
||||
@@ -56,6 +56,7 @@ deprecated-type=warn
|
||||
unsafe-getters-setters=warn
|
||||
inexact-spread=warn
|
||||
unnecessary-invariant=warn
|
||||
signature-verification-failure=warn
|
||||
|
||||
[strict]
|
||||
deprecated-type
|
||||
@@ -67,4 +68,4 @@ untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.182.0
|
||||
^0.170.0
|
||||
|
||||
22
.github/actions/fastlane-dependencies/action.yml
vendored
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: 'fastlane-dependencies'
|
||||
description: 'Install fastlane dependencies'
|
||||
|
||||
inputs:
|
||||
caching_key:
|
||||
description: Name of the OS to generate dependencies for, to compartimentalize caching
|
||||
type: string
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: ci/cache-fastlane-dependencies
|
||||
uses: actions/cache@627f0f41f6904a5b1efbaed9f96d9eb58e92e920 # v3.2.4
|
||||
with:
|
||||
key: gems-v1-${{ inputs.os }}-${{ hashFiles('fastlane/Gemfile.lock') }}-${{ runner.arch }}
|
||||
path: fastlane/vendor/bundle
|
||||
- name: ci/get-fastlane-dependencies
|
||||
working-directory: fastlane
|
||||
shell: bash
|
||||
run: bundle install --path vendor/bundle
|
||||
21
.github/actions/generate-assets/action.yml
vendored
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: 'generate-assets'
|
||||
description: 'Generate app assets'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: ci/cache-assets
|
||||
uses: actions/cache@627f0f41f6904a5b1efbaed9f96d9eb58e92e920 # v3.2.4
|
||||
with:
|
||||
key: assets-v1-${{ hashFiles('assets/base/config.json') }}-${{ runner.arch }}
|
||||
path: dist
|
||||
- name: ci/genereate-assets
|
||||
shell: bash
|
||||
run: node ./scripts/generate-assets.js
|
||||
- name: ci/import-compass-icon
|
||||
shell: bash
|
||||
run: |
|
||||
COMPASS_ICONS="node_modules/@mattermost/compass-icons/font/compass-icons.ttf"
|
||||
cp "$COMPASS_ICONS" "assets/fonts/"
|
||||
cp "$COMPASS_ICONS" "android/app/src/main/assets/fonts"
|
||||
21
.github/actions/node-prepare/action.yml
vendored
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: 'node-prepare'
|
||||
description: 'Install node packages'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: ci/cache-npm-dependencies
|
||||
uses: actions/cache@627f0f41f6904a5b1efbaed9f96d9eb58e92e920 # v3.2.4
|
||||
with:
|
||||
key: npm-v1-${{ hashFiles('package-lock.json') }}
|
||||
path: node_modules
|
||||
- name: ci/install-npm-dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
node node_modules/\@sentry/cli/scripts/install.js
|
||||
node node_modules/react-native-webrtc/tools/downloadWebRTC.js
|
||||
- name: ci/patch-npm-dependencies
|
||||
shell: bash
|
||||
run: npx patch-package
|
||||
19
.github/actions/pods-dependencies/action.yml
vendored
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: 'pods-dependencies'
|
||||
description: 'Get cocoapods dependencies'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: ci/cache-pods-dependencies
|
||||
uses: actions/cache@627f0f41f6904a5b1efbaed9f96d9eb58e92e920 # v3.2.4
|
||||
with:
|
||||
key: cocoapods-v1-${{ inputs.os }}-${{ hashFiles('ios/Podfile.lock') }}-${{ runner.arch }}
|
||||
path: |
|
||||
ios/Pods
|
||||
~/.cocoapods
|
||||
- name: ci/get-pods-dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
npm run ios-gems
|
||||
npm run pod-install
|
||||
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.*'
|
||||
245
.github/workflows/.build-ios.yml
vendored
@@ -1,245 +0,0 @@
|
||||
---
|
||||
name: build-ios
|
||||
on:
|
||||
workflow_call:
|
||||
# If any of these is set to an empty value, its corresponding env var will not be set
|
||||
inputs:
|
||||
app_name:
|
||||
required: false
|
||||
type: string
|
||||
app_scheme:
|
||||
required: false
|
||||
type: string
|
||||
aws_bucket_name:
|
||||
required: false
|
||||
type: string
|
||||
aws_folder_name:
|
||||
required: false
|
||||
type: string
|
||||
aws_region:
|
||||
required: false
|
||||
type: string
|
||||
beta_build:
|
||||
required: false
|
||||
type: string
|
||||
build_for_release:
|
||||
required: false
|
||||
type: string
|
||||
build_pr:
|
||||
required: false
|
||||
type: string
|
||||
extension_app_identifier:
|
||||
required: false
|
||||
type: string
|
||||
branch_to_build:
|
||||
required: false
|
||||
type: string
|
||||
build_artifact_name:
|
||||
required: true
|
||||
type: string
|
||||
build_type:
|
||||
# Valid values: signed, unsigned, simulator
|
||||
required: true
|
||||
type: string
|
||||
ios_app_group:
|
||||
required: false
|
||||
type: string
|
||||
ios_build_export_method:
|
||||
required: false
|
||||
type: string
|
||||
ios_icloud_container:
|
||||
required: false
|
||||
type: string
|
||||
main_app_identifier:
|
||||
required: false
|
||||
type: string
|
||||
match_app_identifier:
|
||||
required: false
|
||||
type: string
|
||||
match_readonly:
|
||||
required: false
|
||||
type: string
|
||||
match_shallow_clone:
|
||||
required: false
|
||||
type: string
|
||||
match_skip_docs:
|
||||
required: false
|
||||
type: string
|
||||
match_type:
|
||||
required: false
|
||||
type: string
|
||||
babel_env:
|
||||
required: false
|
||||
type: string
|
||||
default: "production"
|
||||
node_env:
|
||||
required: false
|
||||
type: string
|
||||
default: "production"
|
||||
node_options:
|
||||
required: false
|
||||
type: string
|
||||
default: "--max_old_space_size=12000"
|
||||
notification_service_identifier:
|
||||
required: false
|
||||
type: string
|
||||
pilot_skip_waiting_for_build_processing:
|
||||
required: false
|
||||
type: string
|
||||
replace_assets:
|
||||
required: false
|
||||
type: string
|
||||
sentry_enabled:
|
||||
required: false
|
||||
type: string
|
||||
show_onboarding:
|
||||
required: false
|
||||
type: string
|
||||
sync_provisioning_profiles:
|
||||
required: false
|
||||
type: string
|
||||
tag:
|
||||
required: false
|
||||
type: string
|
||||
secrets:
|
||||
aws_access_key_id:
|
||||
required: false
|
||||
aws_secret_access_key:
|
||||
required: false
|
||||
fastlane_team_id:
|
||||
required: false
|
||||
gh_token:
|
||||
required: false
|
||||
ios_api_issuer_id:
|
||||
required: false
|
||||
ios_api_key:
|
||||
required: false
|
||||
ios_api_key_id:
|
||||
required: false
|
||||
match_git_url:
|
||||
required: false
|
||||
match_password:
|
||||
required: false
|
||||
mattermost_webhook_url:
|
||||
required: false
|
||||
private_deploy_key:
|
||||
required: false
|
||||
sentry_auth_token:
|
||||
required: false
|
||||
sentry_dsn_ios:
|
||||
required: false
|
||||
sentry_org:
|
||||
required: false
|
||||
sentry_project_ios:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build-ios:
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- name: ci/generate-env
|
||||
run: |
|
||||
# Using this method instead of the workflow-level `env`, to avoid setting variables with empty values
|
||||
# Note that multiline strings require a different notation: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
||||
[ -z "${{ inputs.branch_to_build }}" ] || echo 'BRANCH_TO_BUILD=${{ inputs.branch_to_build }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.app_name }}" ] || echo 'APP_NAME=${{ inputs.app_name }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.app_scheme }}" ] || echo 'APP_SCHEME=${{ inputs.app_scheme }}' >> $GITHUB_ENV
|
||||
[ -z "${{ secrets.aws_access_key_id }}" ] || echo 'AWS_ACCESS_KEY_ID=${{ secrets.aws_access_key_id }}' >> $GITHUB_ENV
|
||||
[ -z "${{ secrets.aws_secret_access_key }}" ] || echo 'AWS_SECRET_ACCESS_KEY=${{ secrets.aws_secret_access_key }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.aws_bucket_name }}" ] || echo 'AWS_BUCKET_NAME=${{ inputs.aws_bucket_name }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.aws_folder_name }}" ] || echo 'AWS_FOLDER_NAME=${{ inputs.aws_folder_name }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.aws_region }}" ] || echo 'AWS_REGION=${{ inputs.aws_region }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.beta_build }}" ] || echo 'BETA_BUILD=${{ inputs.beta_build }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.build_for_release }}" ] || echo 'BUILD_FOR_RELEASE=${{ inputs.build_for_release }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.build_pr }}" ] || echo 'BUILD_PR=${{ inputs.build_pr }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.extension_app_identifier }}" ] || echo 'EXTENSION_APP_IDENTIFIER=${{ inputs.extension_app_identifier }}' >> $GITHUB_ENV
|
||||
[ -z "${{ secrets.fastlane_team_id }}" ] || echo 'FASTLANE_TEAM_ID=${{ secrets.fastlane_team_id }}' >> $GITHUB_ENV
|
||||
[ -z "${{ secrets.gh_token }}" ] || echo 'GITHUB_TOKEN=${{ secrets.gh_token }}' >> $GITHUB_ENV
|
||||
[ -z "${{ secrets.ios_api_issuer_id }}" ] || echo 'IOS_API_ISSUER_ID=${{ secrets.ios_api_issuer_id }}' >> $GITHUB_ENV
|
||||
if [ -n "${{ secrets.ios_api_key }}" ]; then
|
||||
cat <<EOF >>$GITHUB_ENV
|
||||
IOS_API_KEY<<INNEREOF
|
||||
${{ secrets.ios_api_key }}
|
||||
INNEREOF
|
||||
EOF
|
||||
fi
|
||||
[ -z "${{ secrets.ios_api_key_id }}" ] || echo 'IOS_API_KEY_ID=${{ secrets.ios_api_key_id }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.ios_app_group }}" ] || echo 'IOS_APP_GROUP=${{ inputs.ios_app_group }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.ios_build_export_method }}" ] || echo 'IOS_BUILD_EXPORT_METHOD=${{ inputs.ios_build_export_method }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.ios_icloud_container }}" ] || echo 'IOS_ICLOUD_CONTAINER=${{ inputs.ios_icloud_container }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.main_app_identifier }}" ] || echo 'MAIN_APP_IDENTIFIER=${{ inputs.main_app_identifier }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.match_app_identifier }}" ] || echo 'MATCH_APP_IDENTIFIER=${{ inputs.match_app_identifier }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.match_readonly }}" ] || echo 'MATCH_READONLY=${{ inputs.match_readonly }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.match_shallow_clone }}" ] || echo 'MATCH_SHALLOW_CLONE=${{ inputs.match_shallow_clone }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.match_skip_docs }}" ] || echo 'MATCH_SKIP_DOCS=${{ inputs.match_skip_docs }}' >> $GITHUB_ENV
|
||||
[ -z "${{ secrets.match_git_url }}" ] || echo 'MATCH_GIT_URL=${{ secrets.match_git_url }}' >> $GITHUB_ENV
|
||||
[ -z "${{ secrets.match_password }}" ] || echo 'MATCH_PASSWORD=${{ secrets.match_password }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.match_type }}" ] || echo 'MATCH_TYPE=${{ inputs.match_type }}' >> $GITHUB_ENV
|
||||
[ -z "${{ secrets.mattermost_webhook_url }}" ] || echo 'MATTERMOST_WEBHOOK_URL=${{ secrets.mattermost_webhook_url }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.notification_service_identifier }}" ] || echo 'NOTIFICATION_SERVICE_IDENTIFIER=${{ inputs.notification_service_identifier }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.node_env }}" ] || echo 'NODE_ENV=${{ inputs.node_env }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.node_options }}" ] || echo 'NODE_OPTIONS=${{ inputs.node_options }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.babel_env }}" ] || echo 'BABEL_ENV=${{ inputs.babel_env }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.pilot_skip_waiting_for_build_processing }}" ] || echo 'PILOT_SKIP_WAITING_FOR_BUILD_PROCESSING=${{ inputs.pilot_skip_waiting_for_build_processing }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.replace_assets }}" ] || echo 'REPLACE_ASSETS=${{ inputs.replace_assets }}' >> $GITHUB_ENV
|
||||
[ -z "${{ secrets.sentry_auth_token }}" ] || echo 'SENTRY_AUTH_TOKEN=${{ secrets.sentry_auth_token }}' >> $GITHUB_ENV
|
||||
[ -z "${{ secrets.sentry_dsn_ios }}" ] || echo 'SENTRY_DSN_IOS=${{ secrets.sentry_dsn_ios }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.sentry_enabled }}" ] || echo 'SENTRY_ENABLED=${{ inputs.sentry_enabled }}' >> $GITHUB_ENV
|
||||
[ -z "${{ secrets.sentry_org }}" ] || echo 'SENTRY_ORG=${{ secrets.sentry_org }}' >> $GITHUB_ENV
|
||||
[ -z "${{ secrets.sentry_project_ios }}" ] || echo 'SENTRY_PROJECT_IOS=${{ secrets.sentry_project_ios }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.show_onboarding }}" ] || echo 'SHOW_ONBOARDING=${{ inputs.show_onboarding }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.sync_provisioning_profiles }}" ] || echo 'SYNC_PROVISIONING_PROFILES=${{ inputs.sync_provisioning_profiles }}' >> $GITHUB_ENV
|
||||
[ -z "${{ inputs.tag }}" ] || echo 'TAG=${{ inputs.tag }}' >> $GITHUB_ENV
|
||||
- name: ci/output-ssh-private-key
|
||||
run: |
|
||||
SSH_KEY_PATH=~/.ssh/id_ed25519
|
||||
if [ -n "${{ secrets.private_deploy_key }}" ]; then
|
||||
mkdir -p ~/.ssh
|
||||
echo -e '${{ secrets.private_deploy_key }}' > ${SSH_KEY_PATH}
|
||||
chmod 0600 ${SSH_KEY_PATH}
|
||||
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
|
||||
fi
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/node-prepare
|
||||
uses: ./.github/actions/node-prepare
|
||||
- name: ci/pods-dependencies
|
||||
uses: ./.github/actions/pods-dependencies
|
||||
- name: ci/generate-assets
|
||||
uses: ./.github/actions/generate-assets
|
||||
- name: ci/fastlane-dependencies
|
||||
uses: ./.github/actions/fastlane-dependencies
|
||||
with:
|
||||
caching_key: ios
|
||||
# Build iOS app, method depends on the value of `build_type`
|
||||
- name: ci/build-ios-signed
|
||||
working-directory: ./fastlane
|
||||
run: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
export TERM=xterm && bundle exec fastlane ios build
|
||||
if: "${{ inputs.build_type == 'signed' }}"
|
||||
- name: ci/build-ios-unsigned
|
||||
working-directory: ./fastlane
|
||||
run: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
bundle exec fastlane ios unsigned
|
||||
if: "${{ inputs.build_type == 'unsigned' }}"
|
||||
- name: ci/build-ios-simulator
|
||||
working-directory: ./fastlane
|
||||
run: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
bundle exec fastlane ios simulator
|
||||
if: "${{ inputs.build_type == 'simulator' }}"
|
||||
# Upload the build artifact, its path depends on the value of `build_type`
|
||||
- name: ci/upload-ios-buld
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: "${{ inputs.build_artifact_name }}"
|
||||
path: "*.ipa"
|
||||
if: "${{ inputs.build_type == 'signed' || inputs.build_type == 'unsigned' }}"
|
||||
- name: ci/upload-ios-buld-simulator
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: "${{ inputs.build_artifact_name }}"
|
||||
path: "Mattermost-simulator-x86_64.app.zip"
|
||||
if: "${{ inputs.build_type == 'simulator' }}"
|
||||
27
.github/workflows/.deploy-to-store.yml
vendored
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: 'deploy-to-store'
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
build_artifact_name:
|
||||
required: true
|
||||
type: string
|
||||
os:
|
||||
# Valid values: `ios`, `android`
|
||||
required: true
|
||||
type: string
|
||||
env:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
deploy-to-store:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ci/download-artifact
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
name: "${{ inputs.build_artifact_name }}"
|
||||
- name: ci/deploy-to-store
|
||||
working-directory: fastlane
|
||||
run: ${{ inputs.env }} bundle exec fastlane ${{ inputs.os }} deploy file:$(pwd)/../${{ ( inputs.os == 'ios' && '*.ipa' ) || '*.apk' }}
|
||||
23
.github/workflows/.test.yml
vendored
@@ -1,23 +0,0 @@
|
||||
---
|
||||
name: test
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:16.14.2-bullseye
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/node-prepare
|
||||
uses: ./.github/actions/node-prepare
|
||||
- name: ci/generate-assets
|
||||
uses: ./.github/actions/generate-assets
|
||||
- name: ci/check-styles
|
||||
run: npm run check
|
||||
- name: ci/run-tests
|
||||
run: npm test
|
||||
- name: ci/check-i18n
|
||||
run: ./scripts/precommit/i18n.sh
|
||||
60
.github/workflows/build-ios.beta.yml
vendored
@@ -1,60 +0,0 @@
|
||||
---
|
||||
name: "build-ios-beta"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# TODO: remove the `gha-` to match the current workflow again
|
||||
- build-gha-\d+$
|
||||
- build-gha-ios-\d+$
|
||||
- build-gha-ios-beta-\d+$
|
||||
|
||||
jobs:
|
||||
build-ios-pr:
|
||||
uses: ./.github/workflows/.build-ios.yml
|
||||
with:
|
||||
build_artifact_name: "ios-build-beta-${{ github.run_id }}"
|
||||
build_type: "signed"
|
||||
app_name: "Mattermost Beta"
|
||||
app_scheme: "mattermost"
|
||||
aws_bucket_name: "releases.mattermost.com"
|
||||
aws_folder_name: "mattermost-mobile-beta"
|
||||
aws_region: "us-east-1"
|
||||
beta_build: "true"
|
||||
build_for_release: "true"
|
||||
extension_app_identifier: "com.mattermost.rnbeta.MattermostShare"
|
||||
ios_app_group: "group.com.mattermost.rnbeta"
|
||||
ios_build_export_method: "app-store"
|
||||
ios_icloud_container: "iCloud.com.mattermost.rnbeta"
|
||||
main_app_identifier: "com.mattermost.rnbeta"
|
||||
match_app_identifier: "com.mattermost.rnbeta.NotificationService,com.mattermost.rnbeta.MattermostShare,com.mattermost.rnbeta"
|
||||
match_readonly: "true"
|
||||
match_shallow_clone: "true"
|
||||
match_skip_docs: "true"
|
||||
match_type: "appstore"
|
||||
notification_service_identifier: "com.mattermost.rnbeta.NotificationService"
|
||||
pilot_skip_waiting_for_build_processing: "true"
|
||||
replace_assets: "false"
|
||||
sentry_enabled: "true"
|
||||
show_onboarding: "true"
|
||||
sync_provisioning_profiles: "true"
|
||||
secrets:
|
||||
aws_access_key_id: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
|
||||
aws_secret_access_key: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
|
||||
fastlane_team_id : "${{ secrets.MM_MOBILE_FASTLANE_TEAM_ID }}"
|
||||
ios_api_issuer_id: "${{ secrets.MM_MOBILE_IOS_API_ISSUER_ID }}"
|
||||
ios_api_key: "${{ secrets.MM_MOBILE_IOS_API_KEY }}"
|
||||
ios_api_key_id: "${{ secrets.MM_MOBILE_IOS_API_KEY_ID }}"
|
||||
match_git_url: "${{ secrets.MM_MOBILE_MATCH_GIT_URL }}"
|
||||
match_password: "${{ secrets.MM_MOBILE_MATCH_PASSWORD }}"
|
||||
mattermost_webhook_url: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
|
||||
private_deploy_key: "${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}"
|
||||
sentry_auth_token: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
|
||||
sentry_dsn_ios: "${{ secrets.MM_MOBILE_SENTRY_DSN_IOS }}"
|
||||
sentry_org: "${{ secrets.MM_MOBILE_SENTRY_ORG }}"
|
||||
sentry_project_ios: "${{ secrets.MM_MOBILE_SENTRY_PROJECT_IOS }}"
|
||||
#deploy-to-store:
|
||||
# uses: ./.github/workflows/.deploy-to-store.yml
|
||||
# with:
|
||||
# build_artifact_name: "ios-build-beta-${{ github.run_id }}"
|
||||
# os: 'ios'
|
||||
# needs: [build-ios-pr]
|
||||
49
.github/workflows/build-ios.pr.yml
vendored
@@ -1,49 +0,0 @@
|
||||
---
|
||||
name: "build-ios-pr"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# TODO: remove the `gha-` to match the current workflow again
|
||||
- 'build-gha-pr-*'
|
||||
- 'ios-gha-pr-*'
|
||||
|
||||
jobs:
|
||||
build-ios-pr:
|
||||
uses: ./.github/workflows/.build-ios.yml
|
||||
with:
|
||||
branch_to_build: "${{ github.ref_name }}"
|
||||
build_artifact_name: "ios-build-pr-${{ github.run_id }}"
|
||||
build_type: "signed"
|
||||
app_name: "Mattermost Beta"
|
||||
app_scheme: "mattermost"
|
||||
aws_bucket_name: "pr-builds.mattermost.com"
|
||||
aws_folder_name: "mattermost-mobile"
|
||||
aws_region: "us-east-1"
|
||||
beta_build: "true"
|
||||
build_for_release: "true"
|
||||
build_pr: "true"
|
||||
extension_app_identifier: "com.mattermost.rnbeta.MattermostShare"
|
||||
ios_app_group: "group.com.mattermost.rnbeta"
|
||||
ios_build_export_method: "ad-hoc"
|
||||
ios_icloud_container: "iCloud.com.mattermost.rnbeta"
|
||||
main_app_identifier: "com.mattermost.rnbeta"
|
||||
match_app_identifier: "com.mattermost.rnbeta.NotificationService,com.mattermost.rnbeta.MattermostShare,com.mattermost.rnbeta"
|
||||
match_readonly: "true"
|
||||
match_shallow_clone: "true"
|
||||
match_skip_docs: "true"
|
||||
match_type: "adhoc"
|
||||
notification_service_identifier: "com.mattermost.rnbeta.NotificationService"
|
||||
replace_assets: "false"
|
||||
show_onboarding: "true"
|
||||
sync_provisioning_profiles: "true"
|
||||
secrets:
|
||||
aws_access_key_id: "${{ secrets.MM_MOBILE_PR_AWS_ACCESS_KEY_ID }}"
|
||||
aws_secret_access_key: "${{ secrets.MM_MOBILE_PR_AWS_SECRET_ACCESS_KEY }}"
|
||||
fastlane_team_id : "${{ secrets.MM_MOBILE_FASTLANE_TEAM_ID }}"
|
||||
ios_api_issuer_id: "${{ secrets.MM_MOBILE_IOS_API_ISSUER_ID }}"
|
||||
ios_api_key: "${{ secrets.MM_MOBILE_IOS_API_KEY }}"
|
||||
ios_api_key_id: "${{ secrets.MM_MOBILE_IOS_API_KEY_ID }}"
|
||||
match_git_url: "${{ secrets.MM_MOBILE_MATCH_GIT_URL }}"
|
||||
match_password: "${{ secrets.MM_MOBILE_MATCH_PASSWORD }}"
|
||||
mattermost_webhook_url: "${{ secrets.MM_MOBILE_PR_MATTERMOST_WEBHOOK_URL }}"
|
||||
private_deploy_key: "${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}"
|
||||
19
.github/workflows/build-ios.simulator.yml
vendored
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: "build-ios-simulator"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# TODO: remove the `gha-` to match the current workflow again
|
||||
- 'build-gha-*'
|
||||
- 'build-ios-sim-gha-*'
|
||||
|
||||
jobs:
|
||||
build-ios-unsigned:
|
||||
uses: ./.github/workflows/.build-ios.yml
|
||||
with:
|
||||
build_artifact_name: "ios-build-simulator-${{ github.run_id }}"
|
||||
build_type: simulator
|
||||
node_options: "--max_old_space_size=12000"
|
||||
tag: "${{ github.ref_name }}"
|
||||
secrets:
|
||||
gh_token: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
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
|
||||
|
||||
36
.github/workflows/github-release-draft.yml
vendored
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: "github-release-draft"
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
# TODO: remove the `gha-` to match the current workflow again
|
||||
- 'gha-v[0-9]+.[0-9]+.[0-9]+*'
|
||||
|
||||
jobs:
|
||||
build-ios-unsigned:
|
||||
uses: ./.github/workflows/.build-ios.yml
|
||||
with:
|
||||
build_artifact_name: "ios-build-unsigned-${{ github.run_id }}"
|
||||
build_type: unsigned
|
||||
node_options: "--max_old_space_size=12000"
|
||||
tag: "${{ github.ref_name }}"
|
||||
secrets:
|
||||
gh_token: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
# TODO add android job here. Also modify github-release to download its artifact, and to depend on it
|
||||
github-release:
|
||||
runs-on: ubuntu-20.04 # To use Ruby 2.7, instead of Ruby 3
|
||||
steps:
|
||||
- name: ci/fastlane-dependencies
|
||||
uses: ./.github/actions/fastlane-dependencies
|
||||
with:
|
||||
caching_key: github-release
|
||||
- name: ci/download-android-artifact
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
name: "ios-build-unsigned-${{ github.run_id }}"
|
||||
- run:
|
||||
name: Create GitHub release
|
||||
working_directory: fastlane
|
||||
command: bundle exec fastlane github
|
||||
# TODO depend on android job as well
|
||||
needs: [build-ios-unsigned]
|
||||
9
.github/workflows/test.yml
vendored
@@ -1,9 +0,0 @@
|
||||
---
|
||||
name: test
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/.test.yml
|
||||
35
.gitignore
vendored
@@ -16,7 +16,7 @@ env.d.ts
|
||||
|
||||
# Xcode
|
||||
#
|
||||
ios/build/*
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
@@ -36,7 +36,6 @@ DerivedData
|
||||
project.xcworkspace
|
||||
ios/Pods
|
||||
.podinstall
|
||||
ios/.xcode.env.local
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
@@ -44,13 +43,7 @@ ios/.xcode.env.local
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
android/app/bin
|
||||
android/app/build
|
||||
android/build
|
||||
.settings
|
||||
.project
|
||||
.classpath
|
||||
@@ -63,6 +56,12 @@ npm-debug.log
|
||||
yarn-error.log
|
||||
.yarninstall
|
||||
|
||||
# BUCK
|
||||
buck-out/
|
||||
\.buckd/
|
||||
android/app/libs
|
||||
*.keystore
|
||||
|
||||
# Vim
|
||||
[._]*.s[a-w][a-z]
|
||||
[._]s[a-w][a-z]
|
||||
@@ -79,11 +78,11 @@ tags
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
**/fastlane/report.xml
|
||||
**/fastlane/Preview.html
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/test_output
|
||||
**/fastlane/.env
|
||||
*/fastlane/report.xml
|
||||
*/fastlane/Preview.html
|
||||
*/fastlane/screenshots
|
||||
fastlane/.env
|
||||
fastlane/report.xml
|
||||
|
||||
# Sentry
|
||||
android/sentry.properties
|
||||
@@ -98,19 +97,11 @@ coverage
|
||||
mattermost-license.txt
|
||||
*.mattermost-license
|
||||
detox/artifacts
|
||||
detox/detox_pixel_*
|
||||
detox/detox_pixel_4_xl_api_30
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
.bundle
|
||||
|
||||
#editor-settings
|
||||
.vscode
|
||||
.scannerwork
|
||||
launch.json
|
||||
|
||||
# Notice.txt generation
|
||||
!build/notice-file
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
|
||||
72
.solidarity
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/solidaritySchema",
|
||||
"config" : {
|
||||
"output" : "moderate"
|
||||
},
|
||||
"requirements": {
|
||||
"Node": [
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "node",
|
||||
"semver": ">=16.0.0",
|
||||
"error": "install node using nvm https://github.com/nvm-sh/nvm#installing-and-updating"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "npm",
|
||||
"semver": ">=8.5.5 <9.0.0",
|
||||
"error": "install npm 8.5.5 `npm i -g npm@8.5.5"
|
||||
}
|
||||
],
|
||||
"Android": [
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "emulator"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "android"
|
||||
},
|
||||
{
|
||||
"rule": "env",
|
||||
"variable": "ANDROID_HOME",
|
||||
"error": "The ANDROID_HOME environment variable must be set to your local SDK. Refer to getting started docs for help."
|
||||
}
|
||||
],
|
||||
"iOS": [
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "watchman",
|
||||
"error": "install watchman `brew install watchman`",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "xcodebuild",
|
||||
"semver": ">=13.0",
|
||||
"error": "install xcode",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "ruby",
|
||||
"semver": ">=2.7.1 <3.0.0",
|
||||
"error": "visit rvm install https://rvm.io/rvm/install",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "pod",
|
||||
"semver": "1.11.3",
|
||||
"platform": "darwin"
|
||||
}
|
||||
],
|
||||
"Git email": [
|
||||
{
|
||||
"rule": "shell",
|
||||
"command": "git config user.email",
|
||||
"match": ".+@.+"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
6
.storybook/main.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
"stories": [
|
||||
"../app/components/**/*.stories.mdx",
|
||||
"../app/components/**/*.stories.@(js|jsx|ts|tsx)"
|
||||
],
|
||||
}
|
||||
5
.storybook/preview.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Submit feature requests to https://portal.productboard.com/mattermost/33-what-matters-to-you. File non-security related bugs here in the following format:
|
||||
Submit feature requests to http://www.mattermost.org/feature-requests/. File non-security related bugs here in the following format:
|
||||
|
||||
#### Summary
|
||||
Issue in one concise sentence.
|
||||
|
||||
1865
NOTICE.txt
18
README.md
@@ -1,10 +1,12 @@
|
||||
# Mattermost Mobile v2
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (7.1.0+)
|
||||
- **Supported iOS versions:** 12.1+
|
||||
This is a work in progress branch for the next major version of the Mattermost mobile app. Once the work is completed and ready to share, this brach will be set as the default branch in this repository.
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (5.25)
|
||||
- **Supported iOS versions:** 11+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 21 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 14 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
|
||||
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
|
||||
|
||||
@@ -49,9 +51,15 @@ You can leave the Beta testing program at any time:
|
||||
|
||||
App data is wiped from the device when a user logs out of the app. If the user is logged in when the account is deactivated, then within one minute the system logs the user out, and as a result all app data is wiped from the device.
|
||||
|
||||
### I need the code for the v1 version
|
||||
### Can I connect to multiple Mattermost servers using the mobile apps?
|
||||
|
||||
You can still access it! We have moved the code from master to the [v1 branch](https://github.com/mattermost/mattermost-mobile/tree/v1). Be aware that we will not be providing any more v1 versions or updates in the public stores.
|
||||
At the moment, we only support connecting to one server at a time. If you need to connect to multiple servers, please [upvote the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/10975938) so we can track demand for it.
|
||||
|
||||
As a work around, you can install both the released "Mattermost" app and sign up to be a [tester](#testing) for the "Mattermost Beta" app so you can connect to two servers at once.
|
||||
|
||||
### Will there be second generation apps available for tablets?
|
||||
|
||||
We plan to add support for tablets in the future, but the timeline depends on how many people have a need for it. If you're looking for a tablet version, please help us out by [upvoting the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/20082079)!
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
|
||||
65
android/app/BUCK
Normal file
@@ -0,0 +1,65 @@
|
||||
# To learn about Buck see [Docs](https://buckbuild.com/).
|
||||
# To run your application with Buck:
|
||||
# - install Buck
|
||||
# - `npm start` - to start the packager
|
||||
# - `cd android`
|
||||
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
|
||||
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
|
||||
# - `buck install -r android/app` - compile, install and run application
|
||||
#
|
||||
|
||||
lib_deps = []
|
||||
|
||||
for jarfile in glob(['libs/*.jar']):
|
||||
name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')]
|
||||
lib_deps.append(':' + name)
|
||||
prebuilt_jar(
|
||||
name = name,
|
||||
binary_jar = jarfile,
|
||||
)
|
||||
|
||||
for aarfile in glob(['libs/*.aar']):
|
||||
name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')]
|
||||
lib_deps.append(':' + name)
|
||||
android_prebuilt_aar(
|
||||
name = name,
|
||||
aar = aarfile,
|
||||
)
|
||||
|
||||
android_library(
|
||||
name = "all-libs",
|
||||
exported_deps = lib_deps,
|
||||
)
|
||||
|
||||
android_library(
|
||||
name = "app-code",
|
||||
srcs = glob([
|
||||
"src/main/java/**/*.java",
|
||||
]),
|
||||
deps = [
|
||||
":all-libs",
|
||||
":build_config",
|
||||
":res",
|
||||
],
|
||||
)
|
||||
|
||||
android_build_config(
|
||||
name = "build_config",
|
||||
package = "com.mattermost.rnbeta",
|
||||
)
|
||||
|
||||
android_resource(
|
||||
name = "res",
|
||||
package = "com.mattermost.rnbeta",
|
||||
res = "src/main/res",
|
||||
)
|
||||
|
||||
android_binary(
|
||||
name = "app",
|
||||
keystore = "//android/keystores:debug",
|
||||
manifest = "src/main/AndroidManifest.xml",
|
||||
package_type = "debug",
|
||||
deps = [
|
||||
":app-code",
|
||||
],
|
||||
)
|
||||
@@ -1,56 +1,87 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply plugin: 'kotlin-android'
|
||||
import com.android.build.OutputFile
|
||||
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
|
||||
* and bundleReleaseJsAndAssets).
|
||||
* These basically call `react-native bundle` with the correct arguments during the Android build
|
||||
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
|
||||
* bundle directly from the development server. Below you can see all the possible configurations
|
||||
* and their defaults. If you decide to add a configuration block, make sure to add it before the
|
||||
* `apply from: "../../node_modules/react-native/react.gradle"` line.
|
||||
*
|
||||
* project.ext.react = [
|
||||
* // the name of the generated asset file containing your JS bundle
|
||||
* bundleAssetName: "index.android.bundle",
|
||||
*
|
||||
* // the entry file for bundle generation. If none specified and
|
||||
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
|
||||
* // default. Can be overridden with ENTRY_FILE environment variable.
|
||||
* entryFile: "index.android.js",
|
||||
*
|
||||
* // whether to bundle JS and assets in debug mode
|
||||
* bundleInDebug: false,
|
||||
*
|
||||
* // whether to bundle JS and assets in release mode
|
||||
* bundleInRelease: true,
|
||||
*
|
||||
* // whether to bundle JS and assets in another build variant (if configured).
|
||||
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
|
||||
* // The configuration property can be in the following formats
|
||||
* // 'bundleIn${productFlavor}${buildType}'
|
||||
* // 'bundleIn${buildType}'
|
||||
* // bundleInFreeDebug: true,
|
||||
* // bundleInPaidRelease: true,
|
||||
* // bundleInBeta: true,
|
||||
*
|
||||
* // whether to disable dev mode in custom build variants (by default only disabled in release)
|
||||
* // for example: to disable dev mode in the staging build type (if configured)
|
||||
* devDisabledInStaging: true,
|
||||
* // The configuration property can be in the following formats
|
||||
* // 'devDisabledIn${productFlavor}${buildType}'
|
||||
* // 'devDisabledIn${buildType}'
|
||||
*
|
||||
* // the root of your project, i.e. where "package.json" lives
|
||||
* root: "../../",
|
||||
*
|
||||
* // where to put the JS bundle asset in debug mode
|
||||
* jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
|
||||
*
|
||||
* // where to put the JS bundle asset in release mode
|
||||
* jsBundleDirRelease: "$buildDir/intermediates/assets/release",
|
||||
*
|
||||
* // where to put drawable resources / React Native assets, e.g. the ones you use via
|
||||
* // require('./image.png')), in debug mode
|
||||
* resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
|
||||
*
|
||||
* // where to put drawable resources / React Native assets, e.g. the ones you use via
|
||||
* // require('./image.png')), in release mode
|
||||
* resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
|
||||
*
|
||||
* // by default the gradle tasks are skipped if none of the JS files or assets change; this means
|
||||
* // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
|
||||
* // date; if you have any other folders that you want to ignore for performance reasons (gradle
|
||||
* // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
|
||||
* // for example, you might want to remove it from here.
|
||||
* inputExcludes: ["android/**", "ios/**"],
|
||||
*
|
||||
* // override which node gets called and with what additional arguments
|
||||
* nodeExecutableAndArgs: ["node"],
|
||||
*
|
||||
* // supply additional arguments to the packager
|
||||
* extraPackagerArgs: []
|
||||
* ]
|
||||
*/
|
||||
|
||||
react {
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '..'
|
||||
// root = file("../")
|
||||
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
|
||||
// reactNativeDir = file("../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen
|
||||
// codegenDir = file("../node_modules/react-native-codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
|
||||
// cliFile = file("../node_modules/react-native/cli.js")
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
//
|
||||
// The command to run when bundling. By default is 'bundle'
|
||||
// bundleCommand = "ram-bundle"
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
entryFile = file("../../index.ts")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
}
|
||||
project.ext.react = [
|
||||
entryFile: "index.ts",
|
||||
bundleConfig: "metro.config.js",
|
||||
bundleCommand: "bundle",
|
||||
enableHermes: true,
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
|
||||
if (System.getenv("SENTRY_ENABLED") == "true") {
|
||||
@@ -63,35 +94,33 @@ if (System.getenv("SENTRY_ENABLED") == "true") {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true to create four separate APKs instead of one,
|
||||
* one for each native architecture. This is useful if you don't
|
||||
* use App Bundles (https://developer.android.com/guide/app-bundle/)
|
||||
* and want to have separate APKs to upload to the Play Store
|
||||
* Set this to true to create two separate APKs instead of one:
|
||||
* - An APK that only works on ARM devices
|
||||
* - An APK that only works on x86 devices
|
||||
* The advantage is the size of the APK is reduced by about 4MB.
|
||||
* Upload all the APKs to the Play Store and people will download
|
||||
* the correct one based on the CPU architecture of their device.
|
||||
*/
|
||||
def enableSeparateBuildPerCPUArchitecture = project.hasProperty('separateApk') ? project.property('separateApk').toBoolean() : false
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
* Run Proguard to shrink the Java bytecode in release builds.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc-intl:+'
|
||||
|
||||
/**
|
||||
* Private function to get the list of Native Architectures you want to build.
|
||||
* This reads the value from reactNativeArchitectures in your gradle.properties
|
||||
* file and works together with the --active-arch-only flag of react-native run-android.
|
||||
* Whether to enable the Hermes VM.
|
||||
*
|
||||
* This should be set on project.ext.react and that value will be read here. If it is not set
|
||||
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
|
||||
* and the benefits of using Hermes will therefore be sharply reduced.
|
||||
*/
|
||||
def enableHermes = project.ext.react.get("enableHermes", false);
|
||||
|
||||
/**
|
||||
* Architectures to build native code for.
|
||||
*/
|
||||
def reactNativeArchitectures() {
|
||||
def value = project.getProperties().get("reactNativeArchitectures")
|
||||
@@ -101,21 +130,86 @@ def reactNativeArchitectures() {
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
namespace "com.mattermost.rnbeta"
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst '**/libc++_shared.so'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 454
|
||||
versionCode 393
|
||||
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 NDK build only if you decide to opt-in for the New Architecture.
|
||||
externalNativeBuild {
|
||||
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"
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
// We configure the NDK build only if you decide to opt-in for the New Architecture.
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path "$projectDir/src/main/jni/Android.mk"
|
||||
}
|
||||
}
|
||||
def reactAndroidProjectDir = project(':ReactAndroid').projectDir
|
||||
def packageReactNdkDebugLibs = tasks.register("packageReactNdkDebugLibs", Copy) {
|
||||
dependsOn(":ReactAndroid:packageReactNdkDebugLibsForBuck")
|
||||
from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib")
|
||||
into("$buildDir/react-ndk/exported")
|
||||
}
|
||||
def packageReactNdkReleaseLibs = tasks.register("packageReactNdkReleaseLibs", Copy) {
|
||||
dependsOn(":ReactAndroid:packageReactNdkReleaseLibsForBuck")
|
||||
from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib")
|
||||
into("$buildDir/react-ndk/exported")
|
||||
}
|
||||
afterEvaluate {
|
||||
// If you wish to add a custom TurboModule or component locally,
|
||||
// you should uncomment this line.
|
||||
// preBuild.dependsOn("generateCodegenArtifactsFromSchema")
|
||||
preDebugBuild.dependsOn(packageReactNdkDebugLibs)
|
||||
preReleaseBuild.dependsOn(packageReactNdkReleaseLibs)
|
||||
|
||||
// Due to a bug inside AGP, we have to explicitly set a dependency
|
||||
// between configureNdkBuild* tasks and the preBuild tasks.
|
||||
// This can be removed once this is solved: https://issuetracker.google.com/issues/207403732
|
||||
configureNdkBuildRelease.dependsOn(preReleaseBuild)
|
||||
configureNdkBuildDebug.dependsOn(preDebugBuild)
|
||||
reactNativeArchitectures().each { architecture ->
|
||||
tasks.findByName("configureNdkBuildDebug[${architecture}]")?.configure {
|
||||
dependsOn("preDebugBuild")
|
||||
}
|
||||
tasks.findByName("configureNdkBuildRelease[${architecture}]")?.configure {
|
||||
dependsOn("preReleaseBuild")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -188,40 +282,67 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
|
||||
//noinspection GradleDynamicVersio
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
|
||||
implementation project(':lottie-react-native')
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||
exclude group:'com.facebook.flipper'
|
||||
}
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
|
||||
if (enableHermes) {
|
||||
def hermesPath = "../../node_modules/hermes-engine/android/";
|
||||
debugImplementation files(hermesPath + "hermes-debug.aar")
|
||||
releaseImplementation files(hermesPath + "hermes-release.aar")
|
||||
unsignedImplementation files(hermesPath + "hermes-release.aar")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
|
||||
implementation 'androidx.window:window-rxjava3:1.0.0'
|
||||
implementation 'androidx.window:window:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
|
||||
implementation project(':reactnativenotifications')
|
||||
implementation "com.google.firebase:firebase-messaging:$firebaseVersion"
|
||||
|
||||
// For animated GIF support
|
||||
implementation 'com.facebook.fresco:fresco:2.0.0'
|
||||
implementation 'com.facebook.fresco:animated-gif:2.0.0'
|
||||
// For WebP support, including animated WebP
|
||||
implementation 'com.facebook.fresco:animated-webp:2.0.0'
|
||||
implementation 'com.facebook.fresco:webpsupport:2.0.0'
|
||||
|
||||
androidTestImplementation('com.wix:detox:+')
|
||||
implementation project(':reactnativenotifications')
|
||||
|
||||
implementation project(':watermelondb-jsi')
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
if (isNewArchitectureEnabled()) {
|
||||
// If new architecture is enabled, we let you build RN from source
|
||||
// Otherwise we fallback to a prebuilt .aar bundled in the NPM package.
|
||||
// This will be applied to all the imported transtitive dependency.
|
||||
resolutionStrategy.dependencySubstitution {
|
||||
substitute(module("com.facebook.react:react-native"))
|
||||
.using(project(":ReactAndroid")).because("On New Architecture we're building React Native from source")
|
||||
}
|
||||
}
|
||||
resolutionStrategy {
|
||||
force "com.facebook.soloader:soloader:0.10.1"
|
||||
eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.name == 'play-services-base') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
@@ -236,13 +357,13 @@ configurations.all {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'okhttp') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.2'
|
||||
}
|
||||
if (details.requested.name == 'okhttp-tls') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.2'
|
||||
}
|
||||
if (details.requested.name == 'okhttp-urlconnection') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.2'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,3 +378,11 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
|
||||
def isNewArchitectureEnabled() {
|
||||
// To opt-in for the New Architecture, you can either:
|
||||
// - Set `newArchEnabled` to true inside the `gradle.properties` file
|
||||
// - Invoke gradle with `-newArchEnabled=true`
|
||||
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
|
||||
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.wix.detox.Detox;
|
||||
import com.wix.detox.config.DetoxConfig;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -20,11 +19,10 @@ public class DetoxTest {
|
||||
|
||||
@Test
|
||||
public void runDetoxTests() {
|
||||
DetoxConfig detoxConfig = new DetoxConfig();
|
||||
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
|
||||
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
|
||||
detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60);
|
||||
Detox.DetoxIdlePolicyConfig idlePolicyConfig = new Detox.DetoxIdlePolicyConfig();
|
||||
idlePolicyConfig.masterTimeoutSec = 60;
|
||||
idlePolicyConfig.idleResourceTimeoutSec = 30;
|
||||
|
||||
Detox.runTests(mActivityRule, detoxConfig);
|
||||
Detox.runTests(mActivityRule, idlePolicyConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.mattermost.flipper;
|
||||
package com.rn;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
@@ -17,22 +17,19 @@ import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.react.ReactInstanceEventListener;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.network.NetworkingModule;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the debug
|
||||
* flavor of it. Here you can add your own plugins and customize the Flipper setup.
|
||||
*/
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (FlipperUtils.shouldEnableFlipper(context)) {
|
||||
final FlipperClient client = AndroidFlipperClient.getInstance(context);
|
||||
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
|
||||
client.addPlugin(new ReactFlipperPlugin());
|
||||
client.addPlugin(new DatabasesFlipperPlugin(context));
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
@@ -1,25 +1,13 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.mattermost.rnbeta">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
||||
android:maxSdkVersion="30" />
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -83,23 +71,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>
|
||||
|
||||
@@ -46,9 +46,6 @@ public class CustomPushNotificationHelper {
|
||||
public static final int MESSAGE_NOTIFICATION_ID = 435345;
|
||||
public static final String NOTIFICATION_ID = "notificationId";
|
||||
public static final String NOTIFICATION = "notification";
|
||||
public static final String PUSH_TYPE_MESSAGE = "message";
|
||||
public static final String PUSH_TYPE_CLEAR = "clear";
|
||||
public static final String PUSH_TYPE_SESSION = "session";
|
||||
|
||||
private static NotificationChannel mHighImportanceChannel;
|
||||
private static NotificationChannel mMinImportanceChannel;
|
||||
@@ -57,34 +54,40 @@ public class CustomPushNotificationHelper {
|
||||
String message = bundle.getString("message", bundle.getString("body"));
|
||||
String senderId = bundle.getString("sender_id");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
String type = bundle.getString("type");
|
||||
String urlOverride = bundle.getString("override_icon_url");
|
||||
if (senderId == null) {
|
||||
senderId = "sender_id";
|
||||
}
|
||||
Bundle userInfoBundle = bundle.getBundle("userInfo");
|
||||
String senderName = getSenderName(bundle);
|
||||
if (userInfoBundle != null) {
|
||||
boolean localPushNotificationTest = userInfoBundle.getBoolean("test");
|
||||
if (localPushNotificationTest) {
|
||||
senderName = "Test";
|
||||
}
|
||||
}
|
||||
|
||||
if (conversationTitle == null || !android.text.TextUtils.isEmpty(senderName.trim())) {
|
||||
message = removeSenderNameFromMessage(message, senderName);
|
||||
}
|
||||
|
||||
long timestamp = new Date().getTime();
|
||||
Person.Builder sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName(senderName);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messagingStyle.addMessage(message, timestamp, senderName);
|
||||
} else {
|
||||
Person.Builder sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName(senderName);
|
||||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
if (serverUrl != null) {
|
||||
try {
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, senderId))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
messagingStyle.addMessage(message, timestamp, sender.build());
|
||||
messagingStyle.addMessage(message, timestamp, sender.build());
|
||||
}
|
||||
}
|
||||
|
||||
private static void addNotificationExtras(NotificationCompat.Builder notification, Bundle bundle) {
|
||||
@@ -98,26 +101,6 @@ public class CustomPushNotificationHelper {
|
||||
userInfoBundle.putString("channel_id", channelId);
|
||||
}
|
||||
|
||||
String postId = bundle.getString("post_id");
|
||||
if (postId != null) {
|
||||
userInfoBundle.putString("post_id", postId);
|
||||
}
|
||||
|
||||
String rootId = bundle.getString("root_id");
|
||||
if (rootId != null) {
|
||||
userInfoBundle.putString("root_id", rootId);
|
||||
}
|
||||
|
||||
String crtEnabled = bundle.getString("is_crt_enabled");
|
||||
if (crtEnabled != null) {
|
||||
userInfoBundle.putString("is_crt_enabled", crtEnabled);
|
||||
}
|
||||
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
if (serverUrl != null) {
|
||||
userInfoBundle.putString("server_url", serverUrl);
|
||||
}
|
||||
|
||||
notification.addExtras(userInfoBundle);
|
||||
}
|
||||
|
||||
@@ -134,20 +117,12 @@ public class CustomPushNotificationHelper {
|
||||
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
|
||||
replyIntent.putExtra(NOTIFICATION, bundle);
|
||||
|
||||
PendingIntent replyPendingIntent;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
replyPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
notificationId,
|
||||
replyIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
} else {
|
||||
replyPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
notificationId,
|
||||
replyIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
notificationId,
|
||||
replyIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
|
||||
.setLabel("Reply")
|
||||
@@ -170,19 +145,15 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
String channelId = bundle.getString("channel_id");
|
||||
String postId = bundle.getString("post_id");
|
||||
String rootId = bundle.getString("root_id");
|
||||
int notificationId = postId != null ? postId.hashCode() : MESSAGE_NOTIFICATION_ID;
|
||||
|
||||
boolean is_crt_enabled = bundle.containsKey("is_crt_enabled") && bundle.getString("is_crt_enabled").equals("true");
|
||||
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
|
||||
|
||||
addNotificationExtras(notification, bundle);
|
||||
setNotificationIcons(context, notification, bundle);
|
||||
setNotificationMessagingStyle(context, notification, bundle);
|
||||
setNotificationGroup(notification, groupId, createSummary);
|
||||
setNotificationGroup(notification, channelId, createSummary);
|
||||
setNotificationBadgeType(notification);
|
||||
|
||||
setNotificationChannel(context, notification, bundle);
|
||||
setNotificationChannel(notification, bundle);
|
||||
setNotificationDeleteIntent(context, notification, bundle, notificationId);
|
||||
addNotificationReplyAction(context, notification, bundle, notificationId);
|
||||
|
||||
@@ -247,10 +218,6 @@ public class CustomPushNotificationHelper {
|
||||
title = bundle.getString("sender_name");
|
||||
}
|
||||
|
||||
if (android.text.TextUtils.isEmpty(title)) {
|
||||
title = bundle.getString("title", "");
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
@@ -264,27 +231,26 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
private static NotificationCompat.MessagingStyle getMessagingStyle(Context context, Bundle bundle) {
|
||||
NotificationCompat.MessagingStyle messagingStyle;
|
||||
final String senderId = "me";
|
||||
String senderId = "me";
|
||||
final String serverUrl = bundle.getString("server_url");
|
||||
final String type = bundle.getString("type");
|
||||
String urlOverride = bundle.getString("override_icon_url");
|
||||
|
||||
Person.Builder sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName("Me");
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messagingStyle = new NotificationCompat.MessagingStyle("Me");
|
||||
} else {
|
||||
Person.Builder sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName("Me");
|
||||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, serverUrl, "me", urlOverride);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
if (serverUrl != null) {
|
||||
try {
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, "me"))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
messagingStyle = new NotificationCompat.MessagingStyle(sender.build());
|
||||
messagingStyle = new NotificationCompat.MessagingStyle(sender.build());
|
||||
}
|
||||
|
||||
String conversationTitle = getConversationTitle(bundle);
|
||||
setMessagingStyleConversationTitle(messagingStyle, conversationTitle, bundle);
|
||||
@@ -365,16 +331,26 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static void setNotificationChannel(Context context, NotificationCompat.Builder notification, Bundle bundle) {
|
||||
private static void setNotificationChannel(NotificationCompat.Builder notification, Bundle bundle) {
|
||||
// If Android Oreo or above we need to register a channel
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mHighImportanceChannel == null) {
|
||||
createNotificationChannels(context);
|
||||
}
|
||||
NotificationChannel notificationChannel = mHighImportanceChannel;
|
||||
|
||||
boolean testNotification = false;
|
||||
boolean localNotification = false;
|
||||
Bundle userInfoBundle = bundle.getBundle("userInfo");
|
||||
if (userInfoBundle != null) {
|
||||
testNotification = userInfoBundle.getBoolean("test");
|
||||
localNotification = userInfoBundle.getBoolean("local");
|
||||
}
|
||||
|
||||
if (testNotification || localNotification) {
|
||||
notificationChannel = mMinImportanceChannel;
|
||||
}
|
||||
|
||||
notification.setChannelId(notificationChannel.getId());
|
||||
}
|
||||
|
||||
@@ -385,7 +361,7 @@ public class CustomPushNotificationHelper {
|
||||
delIntent.putExtra(NOTIFICATION_ID, notificationId);
|
||||
delIntent.putExtra(PUSH_NOTIFICATION_EXTRA_NAME, bundle);
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
PendingIntent deleteIntent = PendingIntent.getService(context, (int) System.currentTimeMillis(), delIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
|
||||
PendingIntent deleteIntent = PendingIntent.getService(context, (int) System.currentTimeMillis(), delIntent, PendingIntent.FLAG_ONE_SHOT);
|
||||
notification.setDeleteIntent(deleteIntent);
|
||||
}
|
||||
|
||||
@@ -408,7 +384,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);
|
||||
@@ -416,46 +391,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 (!TextUtils.isEmpty(urlOverride)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,22 +16,18 @@ import java.lang.Exception
|
||||
import java.util.*
|
||||
|
||||
class DatabaseHelper {
|
||||
private var defaultDatabase: Database? = null
|
||||
var defaultDatabase: Database? = null
|
||||
private set
|
||||
|
||||
val onlyServerUrl: String?
|
||||
get() {
|
||||
try {
|
||||
val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''"
|
||||
val cursor = defaultDatabase!!.rawQuery(query)
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val url = cursor.getString(0)
|
||||
cursor.close()
|
||||
return url
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''"
|
||||
val cursor = defaultDatabase!!.rawQuery(query)
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val url = cursor.getString(0)
|
||||
cursor.close()
|
||||
return url
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -43,19 +39,14 @@ class DatabaseHelper {
|
||||
}
|
||||
|
||||
fun getServerUrlForIdentifier(identifier: String): String? {
|
||||
try {
|
||||
val args: Array<Any?> = arrayOf(identifier)
|
||||
val query = "SELECT url FROM Servers WHERE identifier=?"
|
||||
val cursor = defaultDatabase!!.rawQuery(query, args)
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val url = cursor.getString(0)
|
||||
cursor.close()
|
||||
return url
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
val args: Array<Any?> = arrayOf(identifier)
|
||||
val query = "SELECT url FROM Servers WHERE identifier=?"
|
||||
val cursor = defaultDatabase!!.rawQuery(query, args)
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val url = cursor.getString(0)
|
||||
cursor.close()
|
||||
return url
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -73,40 +64,31 @@ class DatabaseHelper {
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getDatabaseForServer(context: Context?, serverUrl: String): Database? {
|
||||
try {
|
||||
val args: Array<Any?> = arrayOf(serverUrl)
|
||||
val query = "SELECT db_path FROM Servers WHERE url=?"
|
||||
val cursor = defaultDatabase!!.rawQuery(query, args)
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val databasePath = cursor.getString(0)
|
||||
cursor.close()
|
||||
return Database(databasePath, context!!)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
val args: Array<Any?> = arrayOf(serverUrl)
|
||||
val query = "SELECT db_path FROM Servers WHERE url=?"
|
||||
val cursor = defaultDatabase!!.rawQuery(query, args)
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val databasePath = cursor.getString(0)
|
||||
cursor.close()
|
||||
return Database(databasePath, context!!)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryIds(db: Database, tableName: String, ids: Array<String>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(ids).map { "?" }.toArray())
|
||||
val args = TextUtils.join(",", Arrays.stream(ids).map { value: String? -> "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("select distinct id from $tableName where id IN ($args)", ids as Array<Any?>).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex("id")
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
list.add(cursor.getString(cursor.getColumnIndex("id")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,15 +100,12 @@ class DatabaseHelper {
|
||||
|
||||
fun queryByColumn(db: Database, tableName: String, columnName: String, values: Array<Any?>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(values).map { "?" }.toArray())
|
||||
val args = TextUtils.join(",", Arrays.stream(values).map { value: Any? -> "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("select distinct $columnName from $tableName where $columnName IN ($args)", values).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex(columnName)
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
list.add(cursor.getString(cursor.getColumnIndex(columnName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +120,7 @@ class DatabaseHelper {
|
||||
return result.getString("value")
|
||||
}
|
||||
|
||||
private fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
|
||||
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
|
||||
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
|
||||
@@ -163,87 +142,54 @@ class DatabaseHelper {
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
|
||||
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
|
||||
if (cursor1.count == 1) {
|
||||
cursor1.moveToFirst()
|
||||
val lastFetchedAt = cursor1.getDouble(0)
|
||||
cursor1.close()
|
||||
if (lastFetchedAt == 0.0) {
|
||||
return queryLastPostCreateAt(db, channelId)
|
||||
}
|
||||
return lastFetchedAt
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun handlePosts(db: Database, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
|
||||
fun handlePosts(db: Database, postsData: ReadableMap?, channelId: String) {
|
||||
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
|
||||
if (postsData != null) {
|
||||
val ordered = postsData.getArray("order")?.toArrayList()
|
||||
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
|
||||
val previousPostId = postsData.getString("prev_post_id")
|
||||
val postsInThread = hashMapOf<String, List<JSONObject>>()
|
||||
val postList = posts.toList()
|
||||
var earliest = 0.0
|
||||
var latest = 0.0
|
||||
var lastFetchedAt = 0.0
|
||||
|
||||
if (ordered != null && posts.isNotEmpty()) {
|
||||
val firstId = ordered.first()
|
||||
val lastId = ordered.last()
|
||||
lastFetchedAt = postList.fold(0.0) { acc, next ->
|
||||
val post = next.second as Map<*, *>
|
||||
val createAt = post["create_at"] as Double
|
||||
val updateAt = post["update_at"] as Double
|
||||
val deleteAt = post["delete_at"] as Double
|
||||
val value = maxOf(createAt, updateAt, deleteAt)
|
||||
|
||||
maxOf(value, acc)
|
||||
}
|
||||
var prevPostId = ""
|
||||
|
||||
val sortedPosts = postList.sortedBy { (_, value) ->
|
||||
((value as Map<*, *>)["create_at"] as Double)
|
||||
val sortedPosts = posts.toList().sortedBy { (_, value) ->
|
||||
((value as Map<*, *>).get("create_at") as Double)
|
||||
}
|
||||
|
||||
sortedPosts.forEachIndexed { index, it ->
|
||||
val key = it.first
|
||||
if (it.second != null) {
|
||||
val post = it.second as MutableMap<String, Any?>
|
||||
val post = (it.second as MutableMap<String, Any?>)
|
||||
|
||||
if (index == 0) {
|
||||
post.putIfAbsent("prev_post_id", previousPostId)
|
||||
} else if (prevPostId.isNotEmpty()) {
|
||||
} else if (!prevPostId.isNullOrEmpty()) {
|
||||
post.putIfAbsent("prev_post_id", prevPostId)
|
||||
}
|
||||
|
||||
if (lastId == key) {
|
||||
earliest = post["create_at"] as Double
|
||||
earliest = post.get("create_at") as Double
|
||||
}
|
||||
if (firstId == key) {
|
||||
latest = post["create_at"] as Double
|
||||
latest = post.get("create_at") as Double
|
||||
}
|
||||
|
||||
val jsonPost = JSONObject(post)
|
||||
val rootId = post["root_id"] as? String
|
||||
val rootId = post.get("root_id") as? String
|
||||
|
||||
if (!rootId.isNullOrEmpty()) {
|
||||
var thread = postsInThread[rootId]?.toMutableList()
|
||||
var thread = postsInThread.get(rootId)?.toMutableList()
|
||||
if (thread == null) {
|
||||
thread = mutableListOf()
|
||||
}
|
||||
|
||||
thread.add(jsonPost)
|
||||
postsInThread[rootId] = thread.toList()
|
||||
postsInThread.put(rootId, thread.toList())
|
||||
}
|
||||
|
||||
if (find(db, "Post", key) == null) {
|
||||
@@ -259,39 +205,11 @@ class DatabaseHelper {
|
||||
}
|
||||
}
|
||||
|
||||
if (!receivingThreads) {
|
||||
handlePostsInChannel(db, channelId, earliest, latest)
|
||||
updateMyChannelLastFetchedAt(db, channelId, lastFetchedAt)
|
||||
}
|
||||
handlePostsInChannel(db, channelId, earliest, latest)
|
||||
handlePostsInThread(db, postsInThread)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleThreads(db: Database, threads: ReadableArray) {
|
||||
for (i in 0 until threads.size()) {
|
||||
val thread = threads.getMap(i)
|
||||
val threadId = thread.getString("id")
|
||||
|
||||
// Insert/Update the thread
|
||||
val existingRecord = find(db, "Thread", threadId)
|
||||
if (existingRecord == null) {
|
||||
insertThread(db, thread)
|
||||
} else {
|
||||
updateThread(db, thread, existingRecord)
|
||||
}
|
||||
|
||||
// Delete existing and insert thread participants
|
||||
val participants = thread.getArray("participants")
|
||||
if (participants != null) {
|
||||
db.execute("delete from ThreadParticipant where thread_id = ?", arrayOf(threadId))
|
||||
|
||||
if (participants.size() > 0) {
|
||||
insertThreadParticipants(db, threadId!!, participants)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUsers(db: Database, users: ReadableArray) {
|
||||
for (i in 0 until users.size()) {
|
||||
val user = users.getMap(i)
|
||||
@@ -302,8 +220,6 @@ class DatabaseHelper {
|
||||
false
|
||||
}
|
||||
|
||||
val lastPictureUpdate = try { user.getDouble("last_picture_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
|
||||
db.execute(
|
||||
"insert into User (id, auth_service, update_at, delete_at, email, first_name, is_bot, is_guest, " +
|
||||
@@ -319,7 +235,7 @@ class DatabaseHelper {
|
||||
isBot,
|
||||
roles.contains("system_guest"),
|
||||
user.getString("last_name"),
|
||||
lastPictureUpdate,
|
||||
user.getDouble("last_picture_update"),
|
||||
user.getString("locale"),
|
||||
user.getString("nickname"),
|
||||
user.getString("position"),
|
||||
@@ -341,7 +257,7 @@ class DatabaseHelper {
|
||||
}
|
||||
|
||||
private fun insertPost(db: Database, post: JSONObject) {
|
||||
var metadata: JSONObject?
|
||||
var metadata: JSONObject? = null
|
||||
var reactions: JSONArray? = null
|
||||
var customEmojis: JSONArray? = null
|
||||
var files: JSONArray? = null
|
||||
@@ -395,7 +311,7 @@ class DatabaseHelper {
|
||||
}
|
||||
|
||||
private fun updatePost(db: Database, post: JSONObject) {
|
||||
var metadata: JSONObject?
|
||||
var metadata: JSONObject? = null
|
||||
var reactions: JSONArray? = null
|
||||
var customEmojis: JSONArray? = null
|
||||
|
||||
@@ -444,71 +360,6 @@ class DatabaseHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertThread(db: Database, thread: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { false }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"insert into Thread " +
|
||||
"(id, last_reply_at, last_fetched_at, last_viewed_at, reply_count, is_following, unread_replies, unread_mentions, _status)" +
|
||||
" values (?, ?, 0, ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
thread.getString("id"),
|
||||
lastReplyAt,
|
||||
lastViewedAt,
|
||||
replyCount,
|
||||
isFollowing,
|
||||
unreadReplies,
|
||||
unreadMentions
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateThread(db: Database, thread: ReadableMap, existingRecord: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { existingRecord.getInt("is_following") == 1 }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"update Thread SET last_reply_at = ?, last_viewed_at = ?, reply_count = ?, is_following = ?, unread_replies = ?, unread_mentions = ?, _status = 'updated' where id = ?",
|
||||
arrayOf(
|
||||
lastReplyAt,
|
||||
lastViewedAt,
|
||||
replyCount,
|
||||
isFollowing,
|
||||
unreadReplies,
|
||||
unreadMentions,
|
||||
thread.getString("id")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun insertThreadParticipants(db: Database, threadId: String, participants: ReadableArray) {
|
||||
for (i in 0 until participants.size()) {
|
||||
val participant = participants.getMap(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"insert into ThreadParticipant " +
|
||||
"(id, thread_id, user_id, _status)" +
|
||||
" values (?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
id,
|
||||
threadId,
|
||||
participant.getString("id")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertCustomEmojis(db: Database, customEmojis: JSONArray) {
|
||||
for (i in 0 until customEmojis.length()) {
|
||||
val emoji = customEmojis.getJSONObject(i)
|
||||
@@ -527,9 +378,9 @@ class DatabaseHelper {
|
||||
private fun insertFiles(db: Database, files: JSONArray) {
|
||||
for (i in 0 until files.length()) {
|
||||
val file = files.getJSONObject(i)
|
||||
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" }
|
||||
val height = try { file.getInt("height") } catch (e: JSONException) { 0 }
|
||||
val width = try { file.getInt("width") } catch (e: JSONException) { 0 }
|
||||
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" };
|
||||
val height = try { file.getInt("height") } catch (e: JSONException) { 0 };
|
||||
val width = try { file.getInt("width") } catch (e: JSONException) { 0 };
|
||||
db.execute(
|
||||
"insert into File (id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _status) " +
|
||||
"values (?, ?, ?, ?, '', ?, ?, ?, ?, ?, 'created')",
|
||||
@@ -599,25 +450,15 @@ class DatabaseHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMyChannelLastFetchedAt(db: Database, channelId: String, lastFetchedAt: Double) {
|
||||
db.execute(
|
||||
"UPDATE MyChannel SET last_fetched_at = ?, _status = 'updated' WHERE id = ?",
|
||||
arrayOf(
|
||||
lastFetchedAt,
|
||||
channelId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? {
|
||||
for (i in 0 until chunks.size()) {
|
||||
val chunk = chunks.getMap(i)
|
||||
if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) {
|
||||
return chunk
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
private fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap {
|
||||
@@ -675,7 +516,7 @@ class DatabaseHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.toMap(): Map<String, *> = keys().asSequence().associateWith { it ->
|
||||
private fun JSONObject.toMap(): Map<String, *> = keys().asSequence().associateWith {
|
||||
when (val value = this[it])
|
||||
{
|
||||
is JSONArray ->
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.mattermost.helpers;
|
||||
|
||||
import com.facebook.react.bridge.Dynamic;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* KeysReadableArray: Helper class that abstracts boilerplate
|
||||
*/
|
||||
public class KeysReadableArray implements ReadableArray {
|
||||
@Override
|
||||
public int size() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNull(int index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(int index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(int index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(int index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableArray getArray(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableMap getMap(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dynamic getDynamic(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableType getType(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<Object> toArrayList() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
package com.mattermost.helpers;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class NotificationHelper {
|
||||
public static final String PUSH_NOTIFICATIONS = "PUSH_NOTIFICATIONS";
|
||||
public static final String NOTIFICATIONS_IN_GROUP = "notificationsInGroup";
|
||||
private static final String VERSION_PREFERENCE = "VERSION_PREFERENCE";
|
||||
|
||||
public static void cleanNotificationPreferencesIfNeeded(Context context) {
|
||||
try {
|
||||
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
||||
String version = String.valueOf(pInfo.versionCode);
|
||||
String storedVersion = null;
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(VERSION_PREFERENCE, Context.MODE_PRIVATE);
|
||||
if (pSharedPref != null) {
|
||||
storedVersion = pSharedPref.getString("Version", "");
|
||||
}
|
||||
|
||||
if (!version.equals(storedVersion)) {
|
||||
if (pSharedPref != null) {
|
||||
SharedPreferences.Editor editor = pSharedPref.edit();
|
||||
editor.putString("Version", version);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
Map<String, JSONObject> inputMap = new HashMap<>();
|
||||
saveMap(context, inputMap);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static int getNotificationId(Bundle notification) {
|
||||
final String postId = notification.getString("post_id");
|
||||
final String channelId = notification.getString("channel_id");
|
||||
|
||||
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
if (postId != null) {
|
||||
notificationId = postId.hashCode();
|
||||
} else if (channelId != null) {
|
||||
notificationId = channelId.hashCode();
|
||||
}
|
||||
|
||||
return notificationId;
|
||||
}
|
||||
|
||||
public static StatusBarNotification[] getDeliveredNotifications(Context context) {
|
||||
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
return notificationManager.getActiveNotifications();
|
||||
}
|
||||
|
||||
public static boolean addNotificationToPreferences(Context context, int notificationId, Bundle notification) {
|
||||
try {
|
||||
boolean createSummary = true;
|
||||
final String serverUrl = notification.getString("server_url");
|
||||
final String channelId = notification.getString("channel_id");
|
||||
final String rootId = notification.getString("root_id");
|
||||
final boolean isCRTEnabled = notification.containsKey("is_crt_enabled") && notification.getString("is_crt_enabled").equals("true");
|
||||
|
||||
final boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
final String groupId = isThreadNotification ? rootId : channelId;
|
||||
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
|
||||
if (notificationsInServer == null) {
|
||||
notificationsInServer = new JSONObject();
|
||||
}
|
||||
|
||||
JSONObject notificationsInGroup = notificationsInServer.optJSONObject(groupId);
|
||||
if (notificationsInGroup == null) {
|
||||
notificationsInGroup = new JSONObject();
|
||||
}
|
||||
|
||||
if (notificationsInGroup.length() > 0) {
|
||||
createSummary = false;
|
||||
}
|
||||
|
||||
notificationsInGroup.put(String.valueOf(notificationId), false);
|
||||
|
||||
if (createSummary) {
|
||||
// Add the summary notification id as well
|
||||
notificationsInGroup.put(String.valueOf(notificationId + 1), true);
|
||||
}
|
||||
notificationsInServer.put(groupId, notificationsInGroup);
|
||||
notificationsPerServer.put(serverUrl, notificationsInServer);
|
||||
saveMap(context, notificationsPerServer);
|
||||
|
||||
return createSummary;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void dismissNotification(Context context, Bundle notification) {
|
||||
final boolean isCRTEnabled = notification.containsKey("is_crt_enabled") && notification.getString("is_crt_enabled").equals("true");
|
||||
final String serverUrl = notification.getString("server_url");
|
||||
final String channelId = notification.getString("channel_id");
|
||||
final String rootId = notification.getString("root_id");
|
||||
|
||||
int notificationId = getNotificationId(notification);
|
||||
|
||||
if (!android.text.TextUtils.isEmpty(serverUrl) && !android.text.TextUtils.isEmpty(channelId)) {
|
||||
boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
String notificationIdStr = String.valueOf(notificationId);
|
||||
String groupId = isThreadNotification ? rootId : channelId;
|
||||
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
|
||||
if (notificationsInServer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
JSONObject notificationsInGroup = notificationsInServer.optJSONObject(groupId);
|
||||
if (notificationsInGroup == null) {
|
||||
return;
|
||||
}
|
||||
boolean isSummary = notificationsInGroup.optBoolean(notificationIdStr);
|
||||
notificationsInGroup.remove(notificationIdStr);
|
||||
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
notificationManager.cancel(notificationId);
|
||||
StatusBarNotification[] statusNotifications = getDeliveredNotifications(context);
|
||||
boolean hasMore = false;
|
||||
|
||||
for (final StatusBarNotification status : statusNotifications) {
|
||||
Bundle bundle = status.getNotification().extras;
|
||||
if (isThreadNotification) {
|
||||
hasMore = bundle.containsKey("root_id") && bundle.getString("root_id").equals(rootId);
|
||||
} else {
|
||||
hasMore = bundle.containsKey("channel_id") && bundle.getString("channel_id").equals(channelId);
|
||||
}
|
||||
if (hasMore) break;
|
||||
}
|
||||
|
||||
if (!hasMore || isSummary) {
|
||||
notificationsInServer.remove(groupId);
|
||||
} else {
|
||||
try {
|
||||
notificationsInServer.put(groupId, notificationsInGroup);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
notificationsPerServer.put(serverUrl, notificationsInServer);
|
||||
saveMap(context, notificationsPerServer);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeChannelNotifications(Context context, String serverUrl, String channelId) {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
|
||||
|
||||
if (notificationsInServer != null) {
|
||||
notificationsInServer.remove(channelId);
|
||||
notificationsPerServer.put(serverUrl, notificationsInServer);
|
||||
saveMap(context, notificationsPerServer);
|
||||
}
|
||||
|
||||
StatusBarNotification[] notifications = getDeliveredNotifications(context);
|
||||
for (StatusBarNotification sbn:notifications) {
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String cId = bundle.getString("channel_id");
|
||||
String rootId = bundle.getString("root_id");
|
||||
boolean isCRTEnabled = bundle.containsKey("is_crt_enabled") && bundle.getString("is_crt_enabled").equals("true");
|
||||
boolean skipThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
if (Objects.equals(cId, channelId) && !skipThreadNotification) {
|
||||
notificationManager.cancel(sbn.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeThreadNotifications(Context context, String serverUrl, String threadId) {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
|
||||
|
||||
StatusBarNotification[] notifications = getDeliveredNotifications(context);
|
||||
for (StatusBarNotification sbn:notifications) {
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String rootId = bundle.getString("root_id");
|
||||
String postId = bundle.getString("post_id");
|
||||
if (Objects.equals(rootId, threadId)) {
|
||||
notificationManager.cancel(sbn.getId());
|
||||
}
|
||||
|
||||
if (Objects.equals(postId, threadId)) {
|
||||
String channelId = bundle.getString("channel_id");
|
||||
int id = sbn.getId();
|
||||
if (notificationsInServer != null && channelId != null) {
|
||||
JSONObject notificationsInChannel = notificationsInServer.optJSONObject(channelId);
|
||||
if (notificationsInChannel != null) {
|
||||
notificationsInChannel.remove(String.valueOf(id));
|
||||
try {
|
||||
notificationsInServer.put(channelId, notificationsInChannel);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
notificationManager.cancel(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationsInServer != null) {
|
||||
notificationsInServer.remove(threadId);
|
||||
notificationsPerServer.put(serverUrl, notificationsInServer);
|
||||
saveMap(context, notificationsPerServer);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeServerNotifications(Context context, String serverUrl) {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
notificationsPerServer.remove(serverUrl);
|
||||
saveMap(context, notificationsPerServer);
|
||||
StatusBarNotification[] notifications = getDeliveredNotifications(context);
|
||||
for (StatusBarNotification sbn:notifications) {
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String url = bundle.getString("server_url");
|
||||
if (Objects.equals(url, serverUrl)) {
|
||||
notificationManager.cancel(sbn.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearChannelOrThreadNotifications(Context context, Bundle notification) {
|
||||
final String serverUrl = notification.getString("server_url");
|
||||
final String channelId = notification.getString("channel_id");
|
||||
final String rootId = notification.getString("root_id");
|
||||
if (channelId != null) {
|
||||
final boolean isCRTEnabled = notification.containsKey("is_crt_enabled") && notification.getString("is_crt_enabled").equals("true");
|
||||
// rootId is available only when CRT is enabled & clearing the thread
|
||||
final boolean isClearThread = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
|
||||
if (isClearThread) {
|
||||
removeThreadNotifications(context, serverUrl, rootId);
|
||||
} else {
|
||||
removeChannelNotifications(context, serverUrl, channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Map Structure
|
||||
*
|
||||
* { serverUrl: { groupId: { notification1: true, notification2: false } } }
|
||||
* summary notification has a value of true
|
||||
*
|
||||
*/
|
||||
|
||||
private static void saveMap(Context context, Map<String, JSONObject> inputMap) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
if (pSharedPref != null) {
|
||||
JSONObject json = new JSONObject(inputMap);
|
||||
String jsonString = json.toString();
|
||||
SharedPreferences.Editor editor = pSharedPref.edit();
|
||||
editor.remove(NOTIFICATIONS_IN_GROUP).apply();
|
||||
editor.putString(NOTIFICATIONS_IN_GROUP, jsonString);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, JSONObject> loadMap(Context context) {
|
||||
Map<String, JSONObject> outputMap = new HashMap<>();
|
||||
if (context != null) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
try {
|
||||
if (pSharedPref != null) {
|
||||
String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_GROUP, (new JSONObject()).toString());
|
||||
JSONObject json = new JSONObject(jsonString);
|
||||
Iterator<String> servers = json.keys();
|
||||
|
||||
while (servers.hasNext()) {
|
||||
String serverUrl = servers.next();
|
||||
JSONObject notificationGroup = json.getJSONObject(serverUrl);
|
||||
outputMap.put(serverUrl, notificationGroup);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return outputMap;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,9 @@ package com.mattermost.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.WritableNativeArray
|
||||
import com.nozbe.watermelondb.Database
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executors
|
||||
@@ -26,17 +24,13 @@ class PushNotificationDataHelper(private val context: Context) {
|
||||
|
||||
class PushNotificationDataRunnable {
|
||||
companion object {
|
||||
private val specialMentions = listOf("all", "here", "channel")
|
||||
|
||||
private val specialMentions = listOf<String>("all", "here", "channel")
|
||||
@Synchronized
|
||||
suspend fun start(context: Context, initialData: Bundle) {
|
||||
try {
|
||||
val serverUrl: String = initialData.getString("server_url") ?: return
|
||||
val channelId = initialData.getString("channel_id")
|
||||
val rootId = initialData.getString("root_id")
|
||||
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
|
||||
val db = DatabaseHelper.instance!!.getDatabaseForServer(context, serverUrl)
|
||||
Log.i("ReactNative", "Start fetching notification data in server="+serverUrl+" for channel="+channelId)
|
||||
|
||||
if (db != null) {
|
||||
var postData: ReadableMap?
|
||||
@@ -44,19 +38,12 @@ class PushNotificationDataRunnable {
|
||||
var userIdsToLoad: ReadableArray? = null
|
||||
var usernamesToLoad: ReadableArray? = null
|
||||
|
||||
var threads: ReadableArray? = null
|
||||
var usersFromThreads: ReadableArray? = null
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
|
||||
coroutineScope {
|
||||
if (channelId != null) {
|
||||
postData = fetchPosts(db, serverUrl, channelId, isCRTEnabled, rootId)
|
||||
|
||||
postData = fetchPosts(db, serverUrl, channelId)
|
||||
posts = postData?.getMap("posts")
|
||||
userIdsToLoad = postData?.getArray("userIdsToLoad")
|
||||
usernamesToLoad = postData?.getArray("usernamesToLoad")
|
||||
threads = postData?.getArray("threads")
|
||||
usersFromThreads = postData?.getArray("usersFromThreads")
|
||||
|
||||
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
|
||||
val users = fetchUsersById(serverUrl, userIdsToLoad!!)
|
||||
@@ -72,11 +59,7 @@ class PushNotificationDataRunnable {
|
||||
|
||||
db.transaction {
|
||||
if (posts != null && channelId != null) {
|
||||
DatabaseHelper.instance!!.handlePosts(db, posts!!.getMap("data"), channelId, receivingThreads)
|
||||
}
|
||||
|
||||
if (threads != null) {
|
||||
DatabaseHelper.instance!!.handleThreads(db, threads!!)
|
||||
DatabaseHelper.instance!!.handlePosts(db, posts!!.getMap("data"), channelId)
|
||||
}
|
||||
|
||||
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
|
||||
@@ -86,61 +69,37 @@ class PushNotificationDataRunnable {
|
||||
if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, usernamesToLoad!!)
|
||||
}
|
||||
|
||||
if (usersFromThreads != null) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, usersFromThreads!!)
|
||||
}
|
||||
}
|
||||
|
||||
db.close()
|
||||
Log.i("ReactNative", "Done processing push notification="+serverUrl+" for channel="+channelId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchPosts(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean, rootId: String?): ReadableMap? {
|
||||
private suspend fun fetchPosts(db: Database, serverUrl: String, channelId: String): ReadableMap? {
|
||||
val regex = Regex("""\B@(([a-z0-9-._]*[a-z0-9_])[.-]*)""", setOf(RegexOption.IGNORE_CASE))
|
||||
val since = DatabaseHelper.instance!!.queryPostSinceForChannel(db, channelId)
|
||||
val currentUserId = DatabaseHelper.instance!!.queryCurrentUserId(db)?.removeSurrounding("\"")
|
||||
val currentUser = DatabaseHelper.instance!!.find(db, "User", currentUserId)
|
||||
val currentUsername = currentUser?.getString("username")
|
||||
|
||||
var additionalParams = ""
|
||||
if (isCRTEnabled) {
|
||||
additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true"
|
||||
}
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val endpoint = if (receivingThreads) {
|
||||
val queryParams = "?skipFetchThreads=false&perPage=60&fromCreatedAt=0&direction=up"
|
||||
"/api/v4/posts/$rootId/thread$queryParams$additionalParams"
|
||||
} else {
|
||||
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
|
||||
"/api/v4/channels/$channelId/posts$queryParams$additionalParams"
|
||||
}
|
||||
|
||||
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
|
||||
val endpoint = "/api/v4/channels/$channelId/posts$queryParams"
|
||||
val postsResponse = fetch(serverUrl, endpoint)
|
||||
val results = Arguments.createMap()
|
||||
|
||||
if (postsResponse != null) {
|
||||
val data = ReadableMapUtils.toMap(postsResponse)
|
||||
results.putMap("posts", postsResponse)
|
||||
val postsData = data["data"] as? Map<*, *>
|
||||
val postsData = data.get("data") as? Map<*, *>
|
||||
if (postsData != null) {
|
||||
val postsMap = postsData["posts"]
|
||||
val postsMap = postsData.get("posts")
|
||||
if (postsMap != null) {
|
||||
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Any>)
|
||||
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Object>)
|
||||
val iterator = posts.keySetIterator()
|
||||
val userIds = mutableListOf<String>()
|
||||
val usernames = mutableListOf<String>()
|
||||
|
||||
val threads = WritableNativeArray()
|
||||
val threadParticipantUserIds = mutableListOf<String>() // Used to exclude the "userIds" present in the thread participants
|
||||
val threadParticipantUsernames = mutableListOf<String>() // Used to exclude the "usernames" present in the thread participants
|
||||
val threadParticipantUsers = HashMap<String, ReadableMap>() // All unique users from thread participants are stored here
|
||||
|
||||
while(iterator.hasNextKey()) {
|
||||
val key = iterator.nextKey()
|
||||
val post = posts.getMap(key)
|
||||
@@ -158,38 +117,6 @@ class PushNotificationDataRunnable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCRTEnabled) {
|
||||
// Add root post as a thread
|
||||
val threadId = post?.getString("root_id")
|
||||
if (threadId.isNullOrEmpty()) {
|
||||
threads.pushMap(post!!)
|
||||
}
|
||||
|
||||
// Add participant userIds and usernames to exclude them from getting fetched again
|
||||
val participants = post.getArray("participants")
|
||||
if (participants != null) {
|
||||
for (i in 0 until participants.size()) {
|
||||
val participant = participants.getMap(i)
|
||||
|
||||
val participantId = participant.getString("id")
|
||||
if (participantId != currentUserId && participantId != null) {
|
||||
if (!threadParticipantUserIds.contains(participantId)) {
|
||||
threadParticipantUserIds.add(participantId)
|
||||
}
|
||||
|
||||
if (!threadParticipantUsers.containsKey(participantId)) {
|
||||
threadParticipantUsers[participantId] = participant
|
||||
}
|
||||
}
|
||||
|
||||
val username = participant.getString("username")
|
||||
if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) {
|
||||
threadParticipantUsernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val existingUserIds = DatabaseHelper.instance!!.queryIds(db, "User", userIds.toTypedArray())
|
||||
@@ -197,27 +124,6 @@ class PushNotificationDataRunnable {
|
||||
userIds.removeAll { it in existingUserIds }
|
||||
usernames.removeAll { it in existingUsernames }
|
||||
|
||||
if (threadParticipantUserIds.size > 0) {
|
||||
// Do not fetch users found in thread participants as we get the user's data in the posts response already
|
||||
userIds.removeAll { it in threadParticipantUserIds }
|
||||
usernames.removeAll { it in threadParticipantUsernames }
|
||||
|
||||
// Get users from thread participants
|
||||
val existingThreadParticipantUserIds = DatabaseHelper.instance!!.queryIds(db, "User", threadParticipantUserIds.toTypedArray())
|
||||
|
||||
// Exclude the thread participants already present in the DB from getting inserted again
|
||||
val usersFromThreads = WritableNativeArray()
|
||||
threadParticipantUsers.forEach{ (userId, user) ->
|
||||
if (!existingThreadParticipantUserIds.contains(userId)) {
|
||||
usersFromThreads.pushMap(user)
|
||||
}
|
||||
}
|
||||
|
||||
if (usersFromThreads.size() > 0) {
|
||||
results.putArray("usersFromThreads", usersFromThreads)
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.size > 0) {
|
||||
results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray()))
|
||||
}
|
||||
@@ -225,13 +131,11 @@ class PushNotificationDataRunnable {
|
||||
if (usernames.size > 0) {
|
||||
results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray()))
|
||||
}
|
||||
|
||||
if (threads.size() > 0) {
|
||||
results.putArray("threads", threads)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -239,14 +143,14 @@ class PushNotificationDataRunnable {
|
||||
val endpoint = "api/v4/users/ids"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds)))
|
||||
return fetchWithPost(serverUrl, endpoint, options)
|
||||
return fetchWithPost(serverUrl, endpoint, options);
|
||||
}
|
||||
|
||||
private suspend fun fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableMap? {
|
||||
val endpoint = "api/v4/users/usernames"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames)))
|
||||
return fetchWithPost(serverUrl, endpoint, options)
|
||||
return fetchWithPost(serverUrl, endpoint, options);
|
||||
}
|
||||
|
||||
private suspend fun fetch(serverUrl: String, endpoint: String): ReadableMap? {
|
||||
|
||||
@@ -5,15 +5,15 @@ import kotlin.math.floor
|
||||
class RandomId {
|
||||
companion object {
|
||||
private const val alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
private const val alphabetLength = alphabet.length
|
||||
private const val idLength = 16
|
||||
private const val alphabetLenght = alphabet.length
|
||||
private const val idLenght = 16
|
||||
|
||||
fun generate(): String {
|
||||
var id = ""
|
||||
for (i in 1.rangeTo((idLength / 2))) {
|
||||
val random = floor(Math.random() * alphabetLength * alphabetLength)
|
||||
id += alphabet[floor(random / alphabetLength).toInt()]
|
||||
id += alphabet[(random % alphabetLength).toInt()]
|
||||
for (i in 1.rangeTo((idLenght / 2))) {
|
||||
val random = floor(Math.random() * alphabetLenght * alphabetLenght)
|
||||
id += alphabet[floor(random / alphabetLenght).toInt()]
|
||||
id += alphabet[(random % alphabetLenght).toInt()]
|
||||
}
|
||||
|
||||
return id
|
||||
|
||||
@@ -99,17 +99,23 @@ public class ReadableArrayUtils {
|
||||
for (Object value : array) {
|
||||
if (value == null) {
|
||||
writableArray.pushNull();
|
||||
} else if (value instanceof Boolean) {
|
||||
}
|
||||
if (value instanceof Boolean) {
|
||||
writableArray.pushBoolean((Boolean) value);
|
||||
} else if (value instanceof Double) {
|
||||
}
|
||||
if (value instanceof Double) {
|
||||
writableArray.pushDouble((Double) value);
|
||||
} else if (value instanceof Integer) {
|
||||
}
|
||||
if (value instanceof Integer) {
|
||||
writableArray.pushInt((Integer) value);
|
||||
} else if (value instanceof String) {
|
||||
}
|
||||
if (value instanceof String) {
|
||||
writableArray.pushString((String) value);
|
||||
} else if (value instanceof Map) {
|
||||
}
|
||||
if (value instanceof Map) {
|
||||
writableArray.pushMap(ReadableMapUtils.toWritableMap((Map<String, Object>) value));
|
||||
} else if (value.getClass().isArray()) {
|
||||
}
|
||||
if (value.getClass().isArray()) {
|
||||
writableArray.pushArray(ReadableArrayUtils.toWritableArray((Object[]) value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.mattermost.helpers;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
@@ -39,16 +38,10 @@ public class ReadableMapUtils {
|
||||
jsonObject.put(key, readableMap.getString(key));
|
||||
break;
|
||||
case Map:
|
||||
ReadableMap map = readableMap.getMap(key);
|
||||
if (map != null) {
|
||||
jsonObject.put(key, ReadableMapUtils.toJSONObject(map));
|
||||
}
|
||||
jsonObject.put(key, ReadableMapUtils.toJSONObject(readableMap.getMap(key)));
|
||||
break;
|
||||
case Array:
|
||||
ReadableArray array = readableMap.getArray(key);
|
||||
if (array != null) {
|
||||
jsonObject.put(key, ReadableArrayUtils.toJSONArray(array));
|
||||
}
|
||||
jsonObject.put(key, ReadableArrayUtils.toJSONArray(readableMap.getArray(key)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -99,16 +92,10 @@ public class ReadableMapUtils {
|
||||
map.put(key, readableMap.getString(key));
|
||||
break;
|
||||
case Map:
|
||||
ReadableMap obj = readableMap.getMap(key);
|
||||
if (obj != null) {
|
||||
map.put(key, ReadableMapUtils.toMap(obj));
|
||||
}
|
||||
map.put(key, ReadableMapUtils.toMap(readableMap.getMap(key)));
|
||||
break;
|
||||
case Array:
|
||||
ReadableArray array = readableMap.getArray(key);
|
||||
if (array != null) {
|
||||
map.put(key, ReadableArrayUtils.toArray(array));
|
||||
}
|
||||
map.put(key, ReadableArrayUtils.toArray(readableMap.getArray(key)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -118,26 +105,26 @@ public class ReadableMapUtils {
|
||||
|
||||
public static WritableMap toWritableMap(Map<String, Object> map) {
|
||||
WritableMap writableMap = Arguments.createMap();
|
||||
Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
|
||||
Iterator iterator = map.entrySet().iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, Object> pair = iterator.next();
|
||||
Map.Entry pair = (Map.Entry)iterator.next();
|
||||
Object value = pair.getValue();
|
||||
|
||||
if (value == null) {
|
||||
writableMap.putNull(pair.getKey());
|
||||
writableMap.putNull((String) pair.getKey());
|
||||
} else if (value instanceof Boolean) {
|
||||
writableMap.putBoolean(pair.getKey(), (Boolean) value);
|
||||
writableMap.putBoolean((String) pair.getKey(), (Boolean) value);
|
||||
} else if (value instanceof Double) {
|
||||
writableMap.putDouble(pair.getKey(), (Double) value);
|
||||
writableMap.putDouble((String) pair.getKey(), (Double) value);
|
||||
} else if (value instanceof Integer) {
|
||||
writableMap.putInt(pair.getKey(), (Integer) value);
|
||||
writableMap.putInt((String) pair.getKey(), (Integer) value);
|
||||
} else if (value instanceof String) {
|
||||
writableMap.putString(pair.getKey(), (String) value);
|
||||
} else if (value instanceof Map)
|
||||
writableMap.putMap(pair.getKey(), ReadableMapUtils.toWritableMap((Map<String, Object>) value));
|
||||
else if (value.getClass().isArray()) {
|
||||
writableMap.putArray(pair.getKey(), ReadableArrayUtils.toWritableArray((Object[]) value));
|
||||
writableMap.putString((String) pair.getKey(), (String) value);
|
||||
} else if (value instanceof Map) {
|
||||
writableMap.putMap((String) pair.getKey(), ReadableMapUtils.toWritableMap((Map<String, Object>) value));
|
||||
} else if (value.getClass() != null && value.getClass().isArray()) {
|
||||
writableMap.putArray((String) pair.getKey(), ReadableArrayUtils.toWritableArray((Object[]) value));
|
||||
}
|
||||
|
||||
iterator.remove();
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.mattermost.helpers;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
@@ -17,14 +18,16 @@ import android.os.ParcelFileDescriptor;
|
||||
import java.io.*;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
// Class based on DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
|
||||
// Class based on the steveevers DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
|
||||
|
||||
public class RealPathUtil {
|
||||
public static final String CACHE_DIR_NAME = "mmShare";
|
||||
public static String getRealPathFromURI(final Context context, final Uri uri) {
|
||||
|
||||
final boolean isKitKatOrNewer = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
// DocumentProvider
|
||||
if (DocumentsContract.isDocumentUri(context, uri)) {
|
||||
if (isKitKatOrNewer && DocumentsContract.isDocumentUri(context, uri)) {
|
||||
// ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
@@ -45,7 +48,7 @@ public class RealPathUtil {
|
||||
try {
|
||||
return getPathFromSavingTempFile(context, uri);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri);
|
||||
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -70,12 +73,7 @@ public class RealPathUtil {
|
||||
split[1]
|
||||
};
|
||||
|
||||
String name = getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
if (!TextUtils.isEmpty(name)) {
|
||||
return name;
|
||||
}
|
||||
|
||||
return getPathFromSavingTempFile(context, uri);
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +95,7 @@ public class RealPathUtil {
|
||||
|
||||
public static String getPathFromSavingTempFile(Context context, final Uri uri) {
|
||||
File tmpFile;
|
||||
String fileName = "";
|
||||
String fileName = null;
|
||||
|
||||
if (uri == null || uri.isRelative()) {
|
||||
return null;
|
||||
@@ -110,14 +108,13 @@ public class RealPathUtil {
|
||||
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
returnCursor.moveToFirst();
|
||||
fileName = sanitizeFilename(returnCursor.getString(nameIndex));
|
||||
returnCursor.close();
|
||||
} catch (Exception e) {
|
||||
// just continue to get the filename with the last segment of the path
|
||||
}
|
||||
|
||||
try {
|
||||
if (TextUtils.isEmpty(fileName)) {
|
||||
fileName = sanitizeFilename(uri.getLastPathSegment().trim());
|
||||
fileName = sanitizeFilename(uri.getLastPathSegment().toString().trim());
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +123,7 @@ public class RealPathUtil {
|
||||
cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
String mimeType = getMimeType(uri.getPath());
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
tmpFile.createNewFile();
|
||||
|
||||
@@ -183,14 +181,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,7 +209,7 @@ public class RealPathUtil {
|
||||
return getMimeType(file);
|
||||
}
|
||||
|
||||
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
|
||||
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
|
||||
try {
|
||||
ContentResolver cR = context.getContentResolver();
|
||||
return cR.getType(uri);
|
||||
@@ -237,13 +229,9 @@ public class RealPathUtil {
|
||||
}
|
||||
|
||||
private static void deleteRecursive(File fileOrDirectory) {
|
||||
if (fileOrDirectory.isDirectory()) {
|
||||
File[] files = fileOrDirectory.listFiles();
|
||||
if (files != null) {
|
||||
for (File child : files)
|
||||
deleteRecursive(child);
|
||||
}
|
||||
}
|
||||
if (fileOrDirectory.isDirectory())
|
||||
for (File child : fileOrDirectory.listFiles())
|
||||
deleteRecursive(child);
|
||||
|
||||
fileOrDirectory.delete();
|
||||
}
|
||||
@@ -257,21 +245,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.mattermost.helpers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
@@ -20,7 +18,7 @@ public class ResolvePromise implements Promise {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, @NonNull WritableMap map) {
|
||||
public void reject(String code, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@@ -50,7 +48,7 @@ public class ResolvePromise implements Promise {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message, @NonNull WritableMap map) {
|
||||
public void reject(String code, String message, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.mattermost.newarchitecture;
|
||||
|
||||
import android.app.Application;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.ReactPackageTurboModuleManagerDelegate;
|
||||
import com.facebook.react.bridge.JSIModulePackage;
|
||||
import com.facebook.react.bridge.JSIModuleProvider;
|
||||
import com.facebook.react.bridge.JSIModuleSpec;
|
||||
import com.facebook.react.bridge.JSIModuleType;
|
||||
import com.facebook.react.bridge.JavaScriptContextHolder;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.UIManager;
|
||||
import com.facebook.react.fabric.ComponentFactory;
|
||||
import com.facebook.react.fabric.CoreComponentsRegistry;
|
||||
import com.facebook.react.fabric.EmptyReactNativeConfig;
|
||||
import com.facebook.react.fabric.FabricJSIModuleProvider;
|
||||
import com.facebook.react.uimanager.ViewManagerRegistry;
|
||||
import com.mattermost.rnbeta.BuildConfig;
|
||||
import com.mattermost.newarchitecture.components.MainComponentsRegistry;
|
||||
import com.mattermost.newarchitecture.modules.MainApplicationTurboModuleManagerDelegate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link ReactNativeHost} that helps you load everything needed for the New Architecture, both
|
||||
* TurboModule delegates and the Fabric Renderer.
|
||||
*
|
||||
* <p>Please note that this class is used ONLY if you opt-in for the New Architecture (see the
|
||||
* `newArchEnabled` property). Is ignored otherwise.
|
||||
*/
|
||||
public class MainApplicationReactNativeHost extends ReactNativeHost {
|
||||
public MainApplicationReactNativeHost(Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
// TurboModules must also be loaded here providing a valid TurboReactPackage implementation:
|
||||
// packages.add(new TurboReactPackage() { ... });
|
||||
// If you have custom Fabric Components, their ViewManagers should also be loaded here
|
||||
// inside a ReactPackage.
|
||||
return packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected ReactPackageTurboModuleManagerDelegate.Builder
|
||||
getReactPackageTurboModuleManagerDelegateBuilder() {
|
||||
// Here we provide the ReactPackageTurboModuleManagerDelegate Builder. This is necessary
|
||||
// for the new architecture and to use TurboModules correctly.
|
||||
return new MainApplicationTurboModuleManagerDelegate.Builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JSIModulePackage getJSIModulePackage() {
|
||||
return new JSIModulePackage() {
|
||||
@Override
|
||||
public List<JSIModuleSpec> getJSIModules(
|
||||
final ReactApplicationContext reactApplicationContext,
|
||||
final JavaScriptContextHolder jsContext) {
|
||||
final List<JSIModuleSpec> specs = new ArrayList<>();
|
||||
|
||||
// Here we provide a new JSIModuleSpec that will be responsible of providing the
|
||||
// custom Fabric Components.
|
||||
specs.add(
|
||||
new JSIModuleSpec() {
|
||||
@Override
|
||||
public JSIModuleType getJSIModuleType() {
|
||||
return JSIModuleType.UIManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSIModuleProvider<UIManager> getJSIModuleProvider() {
|
||||
final ComponentFactory componentFactory = new ComponentFactory();
|
||||
CoreComponentsRegistry.register(componentFactory);
|
||||
|
||||
// Here we register a Components Registry.
|
||||
// The one that is generated with the template contains no components
|
||||
// and just provides you the one from React Native core.
|
||||
MainComponentsRegistry.register(componentFactory);
|
||||
|
||||
final ReactInstanceManager reactInstanceManager = getReactInstanceManager();
|
||||
|
||||
ViewManagerRegistry viewManagerRegistry =
|
||||
new ViewManagerRegistry(
|
||||
reactInstanceManager.getOrCreateViewManagers(reactApplicationContext));
|
||||
|
||||
return new FabricJSIModuleProvider(
|
||||
reactApplicationContext,
|
||||
componentFactory,
|
||||
new EmptyReactNativeConfig(),
|
||||
viewManagerRegistry);
|
||||
}
|
||||
});
|
||||
return specs;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.mattermost.newarchitecture.components;
|
||||
|
||||
import com.facebook.jni.HybridData;
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.react.fabric.ComponentFactory;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
/**
|
||||
* Class responsible to load the custom Fabric Components. This class has native methods and needs a
|
||||
* corresponding C++ implementation/header file to work correctly (already placed inside the jni/
|
||||
* folder for you).
|
||||
*
|
||||
* <p>Please note that this class is used ONLY if you opt-in for the New Architecture (see the
|
||||
* `newArchEnabled` property). Is ignored otherwise.
|
||||
*/
|
||||
@DoNotStrip
|
||||
public class MainComponentsRegistry {
|
||||
static {
|
||||
SoLoader.loadLibrary("fabricjni");
|
||||
}
|
||||
|
||||
@DoNotStrip private final HybridData mHybridData;
|
||||
|
||||
@DoNotStrip
|
||||
private native HybridData initHybrid(ComponentFactory componentFactory);
|
||||
|
||||
@DoNotStrip
|
||||
private MainComponentsRegistry(ComponentFactory componentFactory) {
|
||||
mHybridData = initHybrid(componentFactory);
|
||||
}
|
||||
|
||||
@DoNotStrip
|
||||
public static MainComponentsRegistry register(ComponentFactory componentFactory) {
|
||||
return new MainComponentsRegistry(componentFactory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.mattermost.newarchitecture.modules;
|
||||
|
||||
import com.facebook.jni.HybridData;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.ReactPackageTurboModuleManagerDelegate;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class responsible to load the TurboModules. This class has native methods and needs a
|
||||
* corresponding C++ implementation/header file to work correctly (already placed inside the jni/
|
||||
* folder for you).
|
||||
*
|
||||
* <p>Please note that this class is used ONLY if you opt-in for the New Architecture (see the
|
||||
* `newArchEnabled` property). Is ignored otherwise.
|
||||
*/
|
||||
public class MainApplicationTurboModuleManagerDelegate
|
||||
extends ReactPackageTurboModuleManagerDelegate {
|
||||
|
||||
private static volatile boolean sIsSoLibraryLoaded;
|
||||
|
||||
protected MainApplicationTurboModuleManagerDelegate(
|
||||
ReactApplicationContext reactApplicationContext, List<ReactPackage> packages) {
|
||||
super(reactApplicationContext, packages);
|
||||
}
|
||||
|
||||
protected native HybridData initHybrid();
|
||||
|
||||
native boolean canCreateTurboModule(String moduleName);
|
||||
|
||||
public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder {
|
||||
protected MainApplicationTurboModuleManagerDelegate build(
|
||||
ReactApplicationContext context, List<ReactPackage> packages) {
|
||||
return new MainApplicationTurboModuleManagerDelegate(context, packages);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void maybeLoadOtherSoLibraries() {
|
||||
if (!sIsSoLibraryLoaded) {
|
||||
// If you change the name of your application .so file in the Android.mk file,
|
||||
// make sure you update the name here as well.
|
||||
SoLoader.loadLibrary("rndiffapp_appmodules");
|
||||
sIsSoLibraryLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,34 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.mattermost.helpers.CustomPushNotificationHelper;
|
||||
import com.mattermost.helpers.DatabaseHelper;
|
||||
import com.mattermost.helpers.Network;
|
||||
import com.mattermost.helpers.NotificationHelper;
|
||||
import com.mattermost.helpers.PushNotificationDataHelper;
|
||||
import com.mattermost.helpers.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;
|
||||
@@ -26,22 +36,105 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
private static final String PUSH_NOTIFICATIONS = "PUSH_NOTIFICATIONS";
|
||||
private static final String VERSION_PREFERENCE = "VERSION_PREFERENCE";
|
||||
private static final String PUSH_TYPE_MESSAGE = "message";
|
||||
private static final String PUSH_TYPE_CLEAR = "clear";
|
||||
private static final String PUSH_TYPE_SESSION = "session";
|
||||
private static final String NOTIFICATIONS_IN_CHANNEL = "notificationsInChannel";
|
||||
private final PushNotificationDataHelper dataHelper;
|
||||
|
||||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
||||
CustomPushNotificationHelper.createNotificationChannels(context);
|
||||
dataHelper = new PushNotificationDataHelper(context);
|
||||
|
||||
try {
|
||||
Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).init(context);
|
||||
Network.init(context);
|
||||
NotificationHelper.cleanNotificationPreferencesIfNeeded(context);
|
||||
} catch (Exception e) {
|
||||
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
||||
String version = String.valueOf(pInfo.versionCode);
|
||||
String storedVersion = null;
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(VERSION_PREFERENCE, Context.MODE_PRIVATE);
|
||||
if (pSharedPref != null) {
|
||||
storedVersion = pSharedPref.getString("Version", "");
|
||||
}
|
||||
|
||||
if (!version.equals(storedVersion)) {
|
||||
if (pSharedPref != null) {
|
||||
SharedPreferences.Editor editor = pSharedPref.edit();
|
||||
editor.putString("Version", version);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
Map<String, List<Integer>> inputMap = new HashMap<>();
|
||||
saveNotificationsMap(context, inputMap);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void cancelNotification(Context context, String channelId, Integer notificationId) {
|
||||
if (!android.text.TextUtils.isEmpty(channelId)) {
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
|
||||
List<Integer> notifications = notificationsInChannel.get(channelId);
|
||||
if (notifications == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
notificationManager.cancel(notificationId);
|
||||
notifications.remove(notificationId);
|
||||
final StatusBarNotification[] statusNotifications = notificationManager.getActiveNotifications();
|
||||
boolean hasMore = false;
|
||||
for (final StatusBarNotification status : statusNotifications) {
|
||||
if (status.getNotification().extras.getString("channel_id").equals(channelId)) {
|
||||
hasMore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMore) {
|
||||
notificationsInChannel.remove(channelId);
|
||||
}
|
||||
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearChannelNotifications(Context context, String channelId) {
|
||||
if (!android.text.TextUtils.isEmpty(channelId)) {
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
|
||||
List<Integer> notifications = notificationsInChannel.get(channelId);
|
||||
if (notifications == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
notificationsInChannel.remove(channelId);
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
|
||||
for (final Integer notificationId : notifications) {
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
notificationManager.cancel(notificationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearAllNotifications(Context context) {
|
||||
if (context != null) {
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
|
||||
notificationsInChannel.clear();
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
notificationManager.cancelAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceived() {
|
||||
final Bundle initialData = mNotificationProps.asBundle();
|
||||
@@ -50,7 +143,12 @@ public class CustomPushNotification extends PushNotification {
|
||||
final String postId = initialData.getString("post_id");
|
||||
final String channelId = initialData.getString("channel_id");
|
||||
final boolean isIdLoaded = initialData.getString("id_loaded") != null && initialData.getString("id_loaded").equals("true");
|
||||
int notificationId = NotificationHelper.getNotificationId(initialData);
|
||||
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
if (postId != null) {
|
||||
notificationId = postId.hashCode();
|
||||
} else if (channelId != null) {
|
||||
notificationId = channelId.hashCode();
|
||||
}
|
||||
|
||||
String serverUrl = addServerUrlToBundle(initialData);
|
||||
boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
|
||||
@@ -63,9 +161,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
Bundle response = (Bundle) value;
|
||||
if (value != null) {
|
||||
response.putString("server_url", serverUrl);
|
||||
Bundle current = mNotificationProps.asBundle();
|
||||
current.putAll(response);
|
||||
mNotificationProps = createProps(current);
|
||||
mNotificationProps = createProps(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,37 +174,47 @@ 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")) {
|
||||
boolean createSummary = type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE);
|
||||
if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) {
|
||||
case PUSH_TYPE_MESSAGE:
|
||||
case PUSH_TYPE_SESSION:
|
||||
boolean createSummary = type.equals(PUSH_TYPE_MESSAGE);
|
||||
if (!mAppLifecycleFacade.isAppVisible()) {
|
||||
if (type.equals(PUSH_TYPE_MESSAGE)) {
|
||||
if (channelId != null) {
|
||||
Bundle notificationBundle = mNotificationProps.asBundle();
|
||||
if (serverUrl != null && !isReactInit) {
|
||||
// We will only fetch the data related to the notification on the native side
|
||||
// as updating the data directly to the db removes the wal & shm files needed
|
||||
// by watermelonDB, if the DB is updated while WDB is running it causes WDB to
|
||||
// detect the database as malformed, thus the app stop working and a restart is required.
|
||||
// Data will be fetch from within the JS context instead.
|
||||
dataHelper.fetchAndStoreDataForPushNotification(notificationBundle);
|
||||
dataHelper.fetchAndStoreDataForPushNotification(mNotificationProps.asBundle());
|
||||
}
|
||||
createSummary = NotificationHelper.addNotificationToPreferences(
|
||||
mContext,
|
||||
notificationId,
|
||||
notificationBundle
|
||||
);
|
||||
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
|
||||
List<Integer> list = notificationsInChannel.get(channelId);
|
||||
if (list == null) {
|
||||
list = Collections.synchronizedList(new ArrayList<>(0));
|
||||
}
|
||||
|
||||
list.add(0, notificationId);
|
||||
if (list.size() > 1) {
|
||||
createSummary = false;
|
||||
}
|
||||
|
||||
if (createSummary) {
|
||||
// Add the summary notification id as well
|
||||
list.add(0, notificationId + 1);
|
||||
}
|
||||
|
||||
notificationsInChannel.put(channelId, list);
|
||||
saveNotificationsMap(mContext, notificationsInChannel);
|
||||
}
|
||||
}
|
||||
|
||||
buildNotification(notificationId, createSummary);
|
||||
}
|
||||
break;
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_CLEAR:
|
||||
NotificationHelper.clearChannelOrThreadNotifications(mContext, mNotificationProps.asBundle());
|
||||
case PUSH_TYPE_CLEAR:
|
||||
clearChannelNotifications(mContext, channelId);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -119,11 +225,25 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
@Override
|
||||
public void onOpened() {
|
||||
if (mNotificationProps != null) {
|
||||
digestNotification();
|
||||
digestNotification();
|
||||
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
final String channelId = data.getString("channel_id");
|
||||
final String postId = data.getString("post_id");
|
||||
Integer notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
|
||||
if (postId != null) {
|
||||
notificationId = postId.hashCode();
|
||||
}
|
||||
|
||||
if (channelId != null) {
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
|
||||
List<Integer> notifications = notificationsInChannel.get(channelId);
|
||||
if (notifications != null) {
|
||||
notifications.remove(notificationId);
|
||||
}
|
||||
saveNotificationsMap(mContext, notificationsInChannel);
|
||||
clearChannelNotifications(mContext, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,4 +292,43 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
return serverUrl;
|
||||
}
|
||||
|
||||
private static void saveNotificationsMap(Context context, Map<String, List<Integer>> inputMap) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
if (pSharedPref != null) {
|
||||
JSONObject json = new JSONObject(inputMap);
|
||||
String jsonString = json.toString();
|
||||
SharedPreferences.Editor editor = pSharedPref.edit();
|
||||
editor.remove(NOTIFICATIONS_IN_CHANNEL).apply();
|
||||
editor.putString(NOTIFICATIONS_IN_CHANNEL, jsonString);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, List<Integer>> loadNotificationsMap(Context context) {
|
||||
Map<String, List<Integer>> outputMap = new HashMap<>();
|
||||
if (context != null) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
try {
|
||||
if (pSharedPref != null) {
|
||||
String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_CHANNEL, (new JSONObject()).toString());
|
||||
JSONObject json = new JSONObject(jsonString);
|
||||
Iterator<String> keysItr = json.keys();
|
||||
while (keysItr.hasNext()) {
|
||||
String key = keysItr.next();
|
||||
JSONArray array = json.getJSONArray(key);
|
||||
List<Integer> values = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); ++i) {
|
||||
values.add(array.getInt(i));
|
||||
}
|
||||
outputMap.put(key, values);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return outputMap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer;
|
||||
|
||||
public class CustomPushNotificationDrawer extends PushNotificationsDrawer {
|
||||
final protected Context mContext;
|
||||
final protected AppLaunchHelper mAppLaunchHelper;
|
||||
|
||||
protected CustomPushNotificationDrawer(Context context, AppLaunchHelper appLaunchHelper) {
|
||||
super(context, appLaunchHelper);
|
||||
mContext = context;
|
||||
mAppLaunchHelper = appLaunchHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppInit() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppVisible() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationOpened() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelAllLocalNotifications() {
|
||||
CustomPushNotification.clearAllNotifications(mContext);
|
||||
cancelAllScheduledNotifications();
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package com.mattermost.rnbeta
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.window.layout.FoldingFeature
|
||||
import androidx.window.layout.WindowInfoTracker
|
||||
import androidx.window.layout.WindowLayoutInfo
|
||||
import androidx.window.rxjava3.layout.windowLayoutInfoObservable
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
|
||||
class FoldableObserver(private val activity: Activity) {
|
||||
private var disposable: Disposable? = null
|
||||
private lateinit var observable: Observable<WindowLayoutInfo>
|
||||
|
||||
public fun onCreate() {
|
||||
observable = WindowInfoTracker.getOrCreate(activity)
|
||||
.windowLayoutInfoObservable(activity)
|
||||
}
|
||||
|
||||
public fun onStart() {
|
||||
if (disposable?.isDisposed == true) {
|
||||
onCreate()
|
||||
}
|
||||
disposable = observable.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { layoutInfo ->
|
||||
val splitViewModule = SplitViewModule.getInstance()
|
||||
val foldingFeature = layoutInfo.displayFeatures
|
||||
.filterIsInstance<FoldingFeature>()
|
||||
.firstOrNull()
|
||||
when {
|
||||
foldingFeature?.state === FoldingFeature.State.FLAT ->
|
||||
splitViewModule?.setDeviceFolded(false)
|
||||
isTableTopPosture(foldingFeature) ->
|
||||
splitViewModule?.setDeviceFolded(false)
|
||||
isBookPosture(foldingFeature) ->
|
||||
splitViewModule?.setDeviceFolded(false)
|
||||
else -> {
|
||||
splitViewModule?.setDeviceFolded(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun onStop() {
|
||||
disposable?.dispose()
|
||||
}
|
||||
|
||||
private fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
|
||||
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
|
||||
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
|
||||
}
|
||||
|
||||
private fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
|
||||
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
|
||||
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
|
||||
}
|
||||
}
|
||||
@@ -6,56 +6,23 @@ import androidx.annotation.Nullable;
|
||||
import android.view.KeyEvent;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
// import com.facebook.react.ReactActivityDelegate;
|
||||
// import com.facebook.react.ReactRootView;
|
||||
import com.reactnativenavigation.NavigationActivity;
|
||||
import com.github.emilioicai.hwkeyboardevent.HWKeyboardEventModule;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate;
|
||||
|
||||
public class MainActivity extends NavigationActivity {
|
||||
private boolean HWKeyboardConnected = false;
|
||||
private FoldableObserver foldableObserver = new FoldableObserver(this);
|
||||
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "Mattermost";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
|
||||
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
|
||||
* (aka React 18) with two boolean flags.
|
||||
*/
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new DefaultReactActivityDelegate(
|
||||
this,
|
||||
getMainComponentName(),
|
||||
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
|
||||
// If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
|
||||
DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(null);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.launch_screen);
|
||||
setHWKeyboardConnected();
|
||||
foldableObserver.onCreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
foldableObserver.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
foldableObserver.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,12 +36,6 @@ public class MainActivity extends NavigationActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
getReactGateway().onWindowFocusChanged(hasFocus);
|
||||
}
|
||||
|
||||
/*
|
||||
https://mattermost.atlassian.net/browse/MM-10601
|
||||
Required by react-native-hw-keyboard-event
|
||||
@@ -82,19 +43,10 @@ public class MainActivity extends NavigationActivity {
|
||||
*/
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (HWKeyboardConnected) {
|
||||
int keyCode = event.getKeyCode();
|
||||
int keyAction = event.getAction();
|
||||
if (keyAction == KeyEvent.ACTION_UP) {
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||
String keyPressed = event.isShiftPressed() ? "shift-enter" : "enter";
|
||||
HWKeyboardEventModule.getInstance().keyPressed(keyPressed);
|
||||
return true;
|
||||
} else if (keyCode == KeyEvent.KEYCODE_K && event.isCtrlPressed()) {
|
||||
HWKeyboardEventModule.getInstance().keyPressed("find-channels");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (HWKeyboardConnected && event.getKeyCode() == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
|
||||
String keyPressed = event.isShiftPressed() ? "shift-enter" : "enter";
|
||||
HWKeyboardEventModule.getInstance().keyPressed(keyPressed);
|
||||
return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
};
|
||||
@@ -102,4 +54,25 @@ public class MainActivity extends NavigationActivity {
|
||||
private void setHWKeyboardConnected() {
|
||||
HWKeyboardConnected = getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instance of the {@link ReactActivityDelegate}. There the RootView is created and
|
||||
* you can specify the rendered you wish to use (Fabric or the older renderer).
|
||||
*/
|
||||
// @Override
|
||||
// protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
// return new MainActivityDelegate(this, getMainComponentName());
|
||||
// }
|
||||
// public static class MainActivityDelegate extends ReactActivityDelegate {
|
||||
// public MainActivityDelegate(ReactActivity activity, String mainComponentName) {
|
||||
// super(activity, mainComponentName);
|
||||
// }
|
||||
// @Override
|
||||
// protected ReactRootView createRootView() {
|
||||
// ReactRootView reactRootView = new ReactRootView(getContext());
|
||||
// // If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
// reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED);
|
||||
// return reactRootView;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.facebook.react.bridge.JSIModuleSpec;
|
||||
import com.facebook.react.bridge.JavaScriptContextHolder;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.airbnb.android.react.lottie.LottiePackage;
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
import com.mattermost.share.ShareModule;
|
||||
import com.wix.reactnativenotifications.RNNotificationsPackage;
|
||||
|
||||
import com.reactnativenavigation.NavigationApplication;
|
||||
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
|
||||
import com.wix.reactnativenotifications.core.notification.IPushNotification;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.INotificationsDrawerApplication;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost;
|
||||
import com.facebook.react.config.ReactFeatureFlags;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.TurboReactPackage;
|
||||
import com.facebook.react.bridge.JSIModuleSpec;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.JSIModulePackage;
|
||||
@@ -36,16 +39,18 @@ import com.facebook.react.module.model.ReactModuleInfoProvider;
|
||||
import com.facebook.react.modules.network.OkHttpClientProvider;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.mattermost.flipper.ReactNativeFlipper;
|
||||
import com.mattermost.networkclient.RCTOkHttpClientFactory;
|
||||
import com.mattermost.newarchitecture.MainApplicationReactNativeHost;
|
||||
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
||||
import com.nozbe.watermelondb.jsi.WatermelonDBJSIPackage;
|
||||
|
||||
public class MainApplication extends NavigationApplication implements INotificationsApplication {
|
||||
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
|
||||
public static MainApplication instance;
|
||||
|
||||
public Boolean sharedExtensionIsOpened = false;
|
||||
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
new DefaultReactNativeHost(this) {
|
||||
new ReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
@@ -57,6 +62,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
packages.add(new RNNotificationsPackage(MainApplication.this));
|
||||
packages.add(new LottiePackage());
|
||||
|
||||
|
||||
packages.add(
|
||||
@@ -66,14 +72,10 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
switch (name) {
|
||||
case "MattermostManaged":
|
||||
return MattermostManagedModule.getInstance(reactContext);
|
||||
case "MattermostShare":
|
||||
return ShareModule.getInstance(reactContext);
|
||||
case "Notifications":
|
||||
return NotificationsModule.getInstance(instance, reactContext);
|
||||
case "SplitView":
|
||||
return SplitViewModule.Companion.getInstance(reactContext);
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not find module " + name);
|
||||
case "NotificationPreferences":
|
||||
return NotificationPreferencesModule.getInstance(instance, reactContext);
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not find module " + name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +84,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
return () -> {
|
||||
Map<String, ReactModuleInfo> map = new HashMap<>();
|
||||
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
|
||||
map.put("MattermostShare", new ReactModuleInfo("MattermostShare", "com.mattermost.share.ShareModule", false, false, true, false, false));
|
||||
map.put("Notifications", new ReactModuleInfo("Notifications", "com.mattermost.rnbeta.NotificationsModule", false, false, false, false, false));
|
||||
map.put("SplitView", new ReactModuleInfo("SplitView", "com.mattermost.rnbeta.SplitViewModule", false, false, false, false, false));
|
||||
map.put("NotificationPreferences", new ReactModuleInfo("NotificationPreferences", "com.mattermost.rnbeta.NotificationPreferencesModule", false, false, false, false, false));
|
||||
return map;
|
||||
};
|
||||
}
|
||||
@@ -96,11 +96,18 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
|
||||
@Override
|
||||
protected JSIModulePackage getJSIModulePackage() {
|
||||
return (reactApplicationContext, jsContext) -> {
|
||||
List<JSIModuleSpec> modules = Collections.emptyList();
|
||||
modules.addAll(new WatermelonDBJSIPackage().getJSIModules(reactApplicationContext, jsContext));
|
||||
return new JSIModulePackage() {
|
||||
@Override
|
||||
public List<JSIModuleSpec> getJSIModules(
|
||||
final ReactApplicationContext reactApplicationContext,
|
||||
final JavaScriptContextHolder jsContext
|
||||
) {
|
||||
List<JSIModuleSpec> modules = Arrays.asList();
|
||||
modules.addAll(new WatermelonDBJSIPackage().getJSIModules(reactApplicationContext, jsContext));
|
||||
modules.addAll(new ReanimatedJSIModulePackage().getJSIModules(reactApplicationContext, jsContext));
|
||||
|
||||
return modules;
|
||||
return modules;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,26 +115,30 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isNewArchEnabled() {
|
||||
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
}
|
||||
@Override
|
||||
protected Boolean isHermesEnabled() {
|
||||
return BuildConfig.IS_HERMES_ENABLED;
|
||||
}
|
||||
};
|
||||
|
||||
private final ReactNativeHost mNewArchitectureNativeHost =
|
||||
new MainApplicationReactNativeHost(this);
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
return mReactNativeHost;
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
return mNewArchitectureNativeHost;
|
||||
} else {
|
||||
return mReactNativeHost;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
|
||||
// If you opted-in for the New Architecture, we enable the TurboModule system
|
||||
ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
|
||||
Context context = getApplicationContext();
|
||||
|
||||
// Delete any previous temp files created by the app
|
||||
@@ -139,13 +150,6 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
// with a cookie jar defined in APIClientModule and an interceptor to intercept all
|
||||
// requests that originate from React Native's OKHttpClient
|
||||
OkHttpClientProvider.setOkHttpClientFactory(new RCTOkHttpClientFactory());
|
||||
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
DefaultNewArchitectureEntryPoint.load();
|
||||
}
|
||||
ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -158,4 +162,31 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
new JsIOHelper()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) {
|
||||
return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
|
||||
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
*/
|
||||
private static void initializeFlipper(
|
||||
Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
try {
|
||||
/*
|
||||
We use reflection here to pick up the class that initializes Flipper,
|
||||
since Flipper library is not available in release mode
|
||||
*/
|
||||
Class<?> aClass = Class.forName("com.rn.ReactNativeFlipper");
|
||||
aClass
|
||||
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
|
||||
.invoke(null, context, reactInstanceManager);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -21,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 {
|
||||
@@ -117,20 +111,16 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getFilePath(String filePath, Promise promise) {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
WritableMap map = Arguments.createMap();
|
||||
|
||||
if (currentActivity != null) {
|
||||
Uri uri = Uri.parse(filePath);
|
||||
String path = RealPathUtil.getRealPathFromURI(currentActivity, uri);
|
||||
if (path != null) {
|
||||
String text = "file://" + path;
|
||||
map.putString("filePath", text);
|
||||
}
|
||||
public void isRunningInSplitView(final Promise promise) {
|
||||
WritableMap result = Arguments.createMap();
|
||||
Activity current = getCurrentActivity();
|
||||
if (current != null) {
|
||||
result.putBoolean("isSplitView", current.isInMultiWindowMode());
|
||||
} else {
|
||||
result.putBoolean("isSplitView", false);
|
||||
}
|
||||
|
||||
promise.resolve(map);
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
@@ -199,30 +189,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;
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.app.IntentService;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import com.mattermost.helpers.NotificationHelper;
|
||||
import com.mattermost.helpers.CustomPushNotificationHelper;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
|
||||
public class NotificationDismissService extends IntentService {
|
||||
@@ -18,8 +18,16 @@ public class NotificationDismissService extends IntentService {
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
final Context context = getApplicationContext();
|
||||
final Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
|
||||
final String channelId = bundle.getString("channel_id");
|
||||
final String postId = bundle.getString("post_id");
|
||||
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
if (postId != null) {
|
||||
notificationId = postId.hashCode();
|
||||
} else if (channelId != null) {
|
||||
notificationId = channelId.hashCode();
|
||||
}
|
||||
|
||||
NotificationHelper.dismissNotification(context, bundle);
|
||||
CustomPushNotification.cancelNotification(context, channelId, notificationId);
|
||||
Log.i("ReactNative", "Dismiss notification");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
|
||||
private static NotificationPreferencesModule instance;
|
||||
private final MainApplication mApplication;
|
||||
|
||||
private NotificationPreferencesModule(MainApplication application, ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mApplication = application;
|
||||
Context context = mApplication.getApplicationContext();
|
||||
}
|
||||
|
||||
public static NotificationPreferencesModule getInstance(MainApplication application, ReactApplicationContext reactContext) {
|
||||
if (instance == null) {
|
||||
instance = new NotificationPreferencesModule(application, reactContext);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static NotificationPreferencesModule getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "NotificationPreferences";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getDeliveredNotifications(final Promise promise) {
|
||||
final Context context = mApplication.getApplicationContext();
|
||||
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
StatusBarNotification[] statusBarNotifications = notificationManager.getActiveNotifications();
|
||||
WritableArray result = Arguments.createArray();
|
||||
for (StatusBarNotification sbn:statusBarNotifications) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String channelId = bundle.getString("channel_id");
|
||||
map.putString("channel_id", channelId);
|
||||
result.pushMap(map);
|
||||
}
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeDeliveredNotifications(String channelId) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
CustomPushNotification.clearChannelNotifications(context, channelId);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.mattermost.helpers.NotificationHelper;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class NotificationsModule extends ReactContextBaseJavaModule {
|
||||
private static NotificationsModule instance;
|
||||
private final MainApplication mApplication;
|
||||
|
||||
private NotificationsModule(MainApplication application, ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mApplication = application;
|
||||
}
|
||||
|
||||
public static NotificationsModule getInstance(MainApplication application, ReactApplicationContext reactContext) {
|
||||
if (instance == null) {
|
||||
instance = new NotificationsModule(application, reactContext);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Notifications";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getDeliveredNotifications(final Promise promise) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
StatusBarNotification[] notifications = NotificationHelper.getDeliveredNotifications(context);
|
||||
WritableArray result = Arguments.createArray();
|
||||
for (StatusBarNotification sbn:notifications) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
Set<String> keys = bundle.keySet();
|
||||
for (String key: keys) {
|
||||
map.putString(key, bundle.getString(key));
|
||||
}
|
||||
result.pushMap(map);
|
||||
}
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeChannelNotifications(String serverUrl, String channelId) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
NotificationHelper.removeChannelNotifications(context, serverUrl, channelId);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeThreadNotifications(String serverUrl, String threadId) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
NotificationHelper.removeThreadNotifications(context, serverUrl, threadId);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeServerNotifications(String serverUrl) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
NotificationHelper.removeServerNotifications(context, serverUrl);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package com.mattermost.rnbeta
|
||||
|
||||
import com.facebook.react.bridge.*
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
|
||||
import com.learnium.RNDeviceInfo.resolver.DeviceTypeResolver
|
||||
|
||||
class SplitViewModule(private var reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
||||
private var isDeviceFolded: Boolean = false
|
||||
private var listenerCount = 0
|
||||
|
||||
companion object {
|
||||
private var instance: SplitViewModule? = null
|
||||
|
||||
fun getInstance(reactContext: ReactApplicationContext): SplitViewModule {
|
||||
if (instance == null) {
|
||||
instance = SplitViewModule(reactContext)
|
||||
} else {
|
||||
instance!!.reactContext = reactContext
|
||||
}
|
||||
|
||||
return instance!!
|
||||
}
|
||||
|
||||
fun getInstance(): SplitViewModule? {
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun getName() = "SplitView"
|
||||
|
||||
fun sendEvent(eventName: String,
|
||||
params: WritableMap?) {
|
||||
reactContext
|
||||
.getJSModule(RCTDeviceEventEmitter::class.java)
|
||||
.emit(eventName, params)
|
||||
}
|
||||
|
||||
private fun getSplitViewResults(folded: Boolean) : WritableMap? {
|
||||
if (currentActivity != null) {
|
||||
val deviceResolver = DeviceTypeResolver(this.reactContext)
|
||||
val map = Arguments.createMap()
|
||||
map.putBoolean("isSplitView", currentActivity!!.isInMultiWindowMode || folded)
|
||||
map.putBoolean("isTablet", deviceResolver.isTablet)
|
||||
return map
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun setDeviceFolded(folded: Boolean) {
|
||||
val map = getSplitViewResults(folded)
|
||||
if (listenerCount > 0 && isDeviceFolded != folded) {
|
||||
sendEvent("SplitViewChanged", map)
|
||||
}
|
||||
isDeviceFolded = folded
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun isRunningInSplitView(promise: Promise) {
|
||||
promise.resolve(getSplitViewResults(isDeviceFolded))
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun addListener(eventName: String) {
|
||||
listenerCount += 1
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun removeListeners(count: Int) {
|
||||
listenerCount -= count
|
||||
}
|
||||
}
|
||||
@@ -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,115 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ShareUtils {
|
||||
public static ReadableMap getTextItem(String text) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putString("value", text);
|
||||
map.putString("type", "");
|
||||
map.putBoolean("isString", true);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static ReadableMap getFileItem(Activity activity, Uri uri) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
String filePath = RealPathUtil.getRealPathFromURI(activity, uri);
|
||||
if (filePath == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File file = new File(filePath);
|
||||
String type = RealPathUtil.getMimeTypeFromUri(activity, uri);
|
||||
if (type != null) {
|
||||
if (type.startsWith("image/")) {
|
||||
BitmapFactory.Options bitMapOption = ShareUtils.getImageDimensions(filePath);
|
||||
map.putInt("height", bitMapOption.outHeight);
|
||||
map.putInt("width", bitMapOption.outWidth);
|
||||
|
||||
} else if (type.startsWith("video/")) {
|
||||
File cacheDir = new File(activity.getCacheDir(), RealPathUtil.CACHE_DIR_NAME);
|
||||
addVideoThumbnailToMap(cacheDir, activity.getApplicationContext(), map, "file://" + filePath);
|
||||
}
|
||||
} else {
|
||||
type = "application/octet-stream";
|
||||
}
|
||||
|
||||
map.putString("value", "file://" + filePath);
|
||||
map.putDouble("size", (double) file.length());
|
||||
map.putString("filename", file.getName());
|
||||
map.putString("type", type);
|
||||
map.putString("extension", RealPathUtil.getExtension(filePath).replaceFirst(".", ""));
|
||||
map.putBoolean("isString", false);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static BitmapFactory.Options getImageDimensions(String filePath) {
|
||||
BitmapFactory.Options bitMapOption = new BitmapFactory.Options();
|
||||
bitMapOption.inJustDecodeBounds=true;
|
||||
BitmapFactory.decodeFile(filePath, bitMapOption);
|
||||
return bitMapOption;
|
||||
}
|
||||
|
||||
private static void addVideoThumbnailToMap(File cacheDir, Context context, WritableMap map, String filePath) {
|
||||
String fileName = ("thumb-" + UUID.randomUUID().toString()) + ".png";
|
||||
OutputStream fOut = null;
|
||||
|
||||
try {
|
||||
File file = new File(cacheDir, fileName);
|
||||
Bitmap image = getBitmapAtTime(context, filePath, 1);
|
||||
if (file.createNewFile()) {
|
||||
fOut = new FileOutputStream(file);
|
||||
image.compress(Bitmap.CompressFormat.PNG, 100, fOut);
|
||||
fOut.flush();
|
||||
fOut.close();
|
||||
|
||||
map.putString("videoThumb", "file://" + file.getAbsolutePath());
|
||||
map.putInt("width", image.getWidth());
|
||||
map.putInt("height", image.getHeight());
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap getBitmapAtTime(Context context, String filePath, int time) {
|
||||
try {
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
if (URLUtil.isFileUrl(filePath)) {
|
||||
String decodedPath;
|
||||
try {
|
||||
decodedPath = URLDecoder.decode(filePath, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
decodedPath = filePath;
|
||||
}
|
||||
|
||||
retriever.setDataSource(decodedPath.replace("file://", ""));
|
||||
} else if (filePath.contains("content://")) {
|
||||
retriever.setDataSource(context, Uri.parse(filePath));
|
||||
}
|
||||
|
||||
Bitmap image = retriever.getFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
|
||||
retriever.release();
|
||||
return image;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("File doesn't exist or not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
49
android/app/src/main/jni/Android.mk
Normal file
@@ -0,0 +1,49 @@
|
||||
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_futures \
|
||||
libfolly_json \
|
||||
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)
|
||||
24
android/app/src/main/jni/MainApplicationModuleProvider.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include "MainApplicationModuleProvider.h"
|
||||
|
||||
#include <rncore.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
const std::string moduleName,
|
||||
const JavaTurboModule::InitParams ¶ms) {
|
||||
// Here you can provide your own module provider for TurboModules coming from
|
||||
// either your application or from external libraries. The approach to follow
|
||||
// is similar to the following (for a library called `samplelibrary`:
|
||||
//
|
||||
// auto module = samplelibrary_ModuleProvider(moduleName, params);
|
||||
// if (module != nullptr) {
|
||||
// return module;
|
||||
// }
|
||||
// return rncore_ModuleProvider(moduleName, params);
|
||||
return rncore_ModuleProvider(moduleName, params);
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
16
android/app/src/main/jni/MainApplicationModuleProvider.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <ReactCommon/JavaTurboModule.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
const std::string moduleName,
|
||||
const JavaTurboModule::InitParams ¶ms);
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "MainApplicationTurboModuleManagerDelegate.h"
|
||||
#include "MainApplicationModuleProvider.h"
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
jni::local_ref<MainApplicationTurboModuleManagerDelegate::jhybriddata>
|
||||
MainApplicationTurboModuleManagerDelegate::initHybrid(
|
||||
jni::alias_ref<jhybridobject>) {
|
||||
return makeCxxInstance();
|
||||
}
|
||||
|
||||
void MainApplicationTurboModuleManagerDelegate::registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod(
|
||||
"initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid),
|
||||
makeNativeMethod(
|
||||
"canCreateTurboModule",
|
||||
MainApplicationTurboModuleManagerDelegate::canCreateTurboModule),
|
||||
});
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule>
|
||||
MainApplicationTurboModuleManagerDelegate::getTurboModule(
|
||||
const std::string name,
|
||||
const std::shared_ptr<CallInvoker> jsInvoker) {
|
||||
// Not implemented yet: provide pure-C++ NativeModules here.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule>
|
||||
MainApplicationTurboModuleManagerDelegate::getTurboModule(
|
||||
const std::string name,
|
||||
const JavaTurboModule::InitParams ¶ms) {
|
||||
return MainApplicationModuleProvider(name, params);
|
||||
}
|
||||
|
||||
bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule(
|
||||
std::string name) {
|
||||
return getTurboModule(name, nullptr) != nullptr ||
|
||||
getTurboModule(name, {.moduleName = name}) != nullptr;
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
@@ -0,0 +1,38 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <ReactCommon/TurboModuleManagerDelegate.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
class MainApplicationTurboModuleManagerDelegate
|
||||
: public jni::HybridClass<
|
||||
MainApplicationTurboModuleManagerDelegate,
|
||||
TurboModuleManagerDelegate> {
|
||||
public:
|
||||
// Adapt it to the package you used for your Java class.
|
||||
static constexpr auto kJavaDescriptor =
|
||||
"Lcom/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;
|
||||
std::shared_ptr<TurboModule> getTurboModule(
|
||||
const std::string name,
|
||||
const JavaTurboModule::InitParams ¶ms) override;
|
||||
|
||||
/**
|
||||
* Test-only method. Allows user to verify whether a TurboModule can be
|
||||
* created by instances of this class.
|
||||
*/
|
||||
bool canCreateTurboModule(std::string name);
|
||||
};
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
61
android/app/src/main/jni/MainComponentsRegistry.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#include "MainComponentsRegistry.h"
|
||||
|
||||
#include <CoreComponentsRegistry.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
||||
#include <react/renderer/components/rncore/ComponentDescriptors.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {}
|
||||
|
||||
std::shared_ptr<ComponentDescriptorProviderRegistry const>
|
||||
MainComponentsRegistry::sharedProviderRegistry() {
|
||||
auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry();
|
||||
|
||||
// Custom Fabric Components go here. You can register custom
|
||||
// components coming from your App or from 3rd party libraries here.
|
||||
//
|
||||
// providerRegistry->add(concreteComponentDescriptorProvider<
|
||||
// AocViewerComponentDescriptor>());
|
||||
return providerRegistry;
|
||||
}
|
||||
|
||||
jni::local_ref<MainComponentsRegistry::jhybriddata>
|
||||
MainComponentsRegistry::initHybrid(
|
||||
jni::alias_ref<jclass>,
|
||||
ComponentFactory *delegate) {
|
||||
auto instance = makeCxxInstance(delegate);
|
||||
|
||||
auto buildRegistryFunction =
|
||||
[](EventDispatcher::Weak const &eventDispatcher,
|
||||
ContextContainer::Shared const &contextContainer)
|
||||
-> ComponentDescriptorRegistry::Shared {
|
||||
auto registry = MainComponentsRegistry::sharedProviderRegistry()
|
||||
->createComponentDescriptorRegistry(
|
||||
{eventDispatcher, contextContainer});
|
||||
|
||||
auto mutableRegistry =
|
||||
std::const_pointer_cast<ComponentDescriptorRegistry>(registry);
|
||||
|
||||
mutableRegistry->setFallbackComponentDescriptor(
|
||||
std::make_shared<UnimplementedNativeViewComponentDescriptor>(
|
||||
ComponentDescriptorParameters{
|
||||
eventDispatcher, contextContainer, nullptr}));
|
||||
|
||||
return registry;
|
||||
};
|
||||
|
||||
delegate->buildRegistryFunction = buildRegistryFunction;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void MainComponentsRegistry::registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
32
android/app/src/main/jni/MainComponentsRegistry.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <ComponentFactory.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorRegistry.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
class MainComponentsRegistry
|
||||
: public facebook::jni::HybridClass<MainComponentsRegistry> {
|
||||
public:
|
||||
// Adapt it to the package you used for your Java class.
|
||||
constexpr static auto kJavaDescriptor =
|
||||
"Lcom/mattermost/newarchitecture/components/MainComponentsRegistry;";
|
||||
|
||||
static void registerNatives();
|
||||
|
||||
MainComponentsRegistry(ComponentFactory *delegate);
|
||||
|
||||
private:
|
||||
static std::shared_ptr<ComponentDescriptorProviderRegistry const>
|
||||
sharedProviderRegistry();
|
||||
|
||||
static jni::local_ref<jhybriddata> initHybrid(
|
||||
jni::alias_ref<jclass>,
|
||||
ComponentFactory *delegate);
|
||||
};
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
11
android/app/src/main/jni/OnLoad.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include <fbjni/fbjni.h>
|
||||
#include "MainApplicationTurboModuleManagerDelegate.h"
|
||||
#include "MainComponentsRegistry.h"
|
||||
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
|
||||
return facebook::jni::initialize(vm, [] {
|
||||
facebook::react::MainApplicationTurboModuleManagerDelegate::
|
||||
registerNatives();
|
||||
facebook::react::MainComponentsRegistry::registerNatives();
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 3.0 MiB |
@@ -2,5 +2,5 @@
|
||||
<resources>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
<color name="splashscreen_bg">#090A0B</color>
|
||||
<color name="splashscreen_bg">#1E325C</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<files-path name="files" path="." />
|
||||
<external-files-path name="external_files" path="." />
|
||||
<external-path name="external" path="." />
|
||||
<cache-path name="cache" path="." />
|
||||
<root-path name="root" path="." />
|
||||
</paths>
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.mattermost.flipper;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the release
|
||||
* flavor of it so it's empty as we don't want to load Flipper.
|
||||
*/
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
// Do nothing as we don't want to initialize Flipper on Release.
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,16 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "33.0.0"
|
||||
buildToolsVersion = "31.0.0"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
supportLibVersion = "33.0.0"
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
ndkVersion = "21.4.7075529"
|
||||
supportLibVersion = "30.0.0"
|
||||
kotlinVersion = "1.5.30"
|
||||
kotlin_version = "1.5.30"
|
||||
firebaseVersion = "21.0.0"
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
|
||||
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
|
||||
ndkVersion = "23.1.7779620"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -21,9 +19,10 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.3.1")
|
||||
classpath("com.android.tools.build:gradle:7.0.4")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath('com.google.gms:google-services:4.3.14')
|
||||
classpath("de.undercouch:gradle-download-task:4.1.2")
|
||||
classpath('com.google.gms:google-services:4.3.10')
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
@@ -33,25 +32,34 @@ buildscript {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url("$rootDir/../node_modules/react-native/android")
|
||||
|
||||
// Replace AAR from original RN with AAR from react-native-v8
|
||||
// url("$rootDir/../node_modules/react-native-v8/dist")
|
||||
}
|
||||
maven {
|
||||
// Local Maven repo containing AARs with JSC library built for Android
|
||||
url("$rootDir/../node_modules/jsc-android/dist")
|
||||
|
||||
// prebuilt libv8android.so
|
||||
// url("$rootDir/../node_modules/v8-android/dist")
|
||||
}
|
||||
maven {
|
||||
url "https://www.jitpack.io"
|
||||
}
|
||||
maven {
|
||||
url "$rootDir/../node_modules/detox/Detox-android"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
afterEvaluate { subproject ->
|
||||
if(subproject['name'] == 'react-native-create-thumbnail'){
|
||||
def myAttribute = Attribute.of("com.android.build.api.attributes.BuildTypeAttr", String)
|
||||
dependencies.attributesSchema {
|
||||
attribute(myAttribute)
|
||||
}
|
||||
configurations {
|
||||
implementation {
|
||||
attributes {
|
||||
attribute(myAttribute, "release")
|
||||
}
|
||||
}
|
||||
mavenCentral {
|
||||
// We don't want to fetch react-native from Maven Central as there are
|
||||
// older versions over there.
|
||||
content {
|
||||
excludeGroup "com.facebook.react"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,4 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=false
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
newArchEnabled=false
|
||||
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
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
rootProject.name = 'Mattermost'
|
||||
include ':app'
|
||||
includeBuild('../node_modules/react-native-gradle-plugin')
|
||||
if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") {
|
||||
include(":ReactAndroid")
|
||||
project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid')
|
||||
}
|
||||
|
||||
include ':lottie-react-native'
|
||||
project(':lottie-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/lottie-react-native/src/android')
|
||||
include ':reactnativenotifications'
|
||||
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/lib/android/app')
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
@@ -7,4 +15,3 @@ include ':react-native-video'
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
|
||||
include ':watermelondb-jsi'
|
||||
project(':watermelondb-jsi').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android-jsi')
|
||||
includeBuild('../node_modules/react-native-gradle-plugin')
|
||||
|
||||
@@ -1,52 +1,31 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Tutorial} from '@constants';
|
||||
import {GLOBAL_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export const storeGlobal = async (id: string, value: unknown, prepareRecordsOnly = false) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
return operator.handleGlobal({
|
||||
globals: [{id, value}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('storeGlobal', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const storeDeviceToken = async (token: string, prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.DEVICE_TOKEN, token, prepareRecordsOnly);
|
||||
};
|
||||
const operator = DatabaseManager.appDatabase?.operator;
|
||||
|
||||
export const storeOnboardingViewedValue = async (value = true) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.ONBOARDING, value, false);
|
||||
if (!operator) {
|
||||
return {error: 'No App database found'};
|
||||
}
|
||||
|
||||
return operator.handleGlobal({
|
||||
globals: [{id: GLOBAL_IDENTIFIERS.DEVICE_TOKEN, value: token}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
};
|
||||
|
||||
export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(Tutorial.MULTI_SERVER, 'true', prepareRecordsOnly);
|
||||
};
|
||||
const operator = DatabaseManager.appDatabase?.operator;
|
||||
|
||||
export const storeProfileLongPressTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(Tutorial.PROFILE_LONG_PRESS, 'true', prepareRecordsOnly);
|
||||
};
|
||||
if (!operator) {
|
||||
return {error: 'No App database found'};
|
||||
}
|
||||
|
||||
export const storeSkinEmojiSelectorTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(Tutorial.EMOJI_SKIN_SELECTOR, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeDontAskForReview = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.DONT_ASK_FOR_REVIEW, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeLastAskForReview = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.LAST_ASK_FOR_REVIEW, Date.now(), prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeFirstLaunch = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.FIRST_LAUNCH, Date.now(), prepareRecordsOnly);
|
||||
return operator.handleGlobal({
|
||||
globals: [{id: GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, value: 'true'}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {CHANNELS_CATEGORY, DMS_CATEGORY} from '@constants/categories';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById, prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {queryMyTeams} from '@queries/servers/team';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {logError} from '@utils/log';
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCategories, prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById} from '@queries/servers/categories';
|
||||
import {pluckUnique} from '@utils/helpers';
|
||||
|
||||
export const deleteCategory = async (serverUrl: string, categoryId: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const category = await getCategoryById(database, categoryId);
|
||||
|
||||
if (category) {
|
||||
@@ -25,34 +24,74 @@ export const deleteCategory = async (serverUrl: string, categoryId: string) => {
|
||||
|
||||
return {category};
|
||||
} catch (error) {
|
||||
logError('FAILED TO DELETE CATEGORY', categoryId);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO DELETE CATEGORY', categoryId);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
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 operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
|
||||
if (prepareRecordsOnly) {
|
||||
return {models};
|
||||
}
|
||||
|
||||
if (models.length > 0) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
} catch (error) {
|
||||
logError('FAILED TO STORE CATEGORIES', error);
|
||||
return {error};
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
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 {database} = operator;
|
||||
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: flattenedModels};
|
||||
}
|
||||
|
||||
if (flattenedModels?.length > 0) {
|
||||
try {
|
||||
await operator.batchRecords(flattenedModels);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CATEGORIES', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
return {models: flattenedModels};
|
||||
}
|
||||
|
||||
export const toggleCollapseCategory = async (serverUrl: string, categoryId: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl].database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const category = await getCategoryById(database, categoryId);
|
||||
|
||||
if (category) {
|
||||
@@ -65,52 +104,8 @@ export const toggleCollapseCategory = async (serverUrl: string, categoryId: stri
|
||||
|
||||
return {category};
|
||||
} catch (error) {
|
||||
logError('FAILED TO COLLAPSE CATEGORY', categoryId, error);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO COLLAPSE CATEGORY', categoryId, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export async function addChannelToDefaultCategory(serverUrl: string, channel: Channel | ChannelModel, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const teamId = 'teamId' in channel ? channel.teamId : channel.team_id;
|
||||
const userId = await getCurrentUserId(database);
|
||||
|
||||
if (!userId) {
|
||||
return {error: 'no current user id'};
|
||||
}
|
||||
|
||||
const models: Model[] = [];
|
||||
const categoriesWithChannels: CategoryWithChannels[] = [];
|
||||
|
||||
if (isDMorGM(channel)) {
|
||||
const allTeamIds = await queryMyTeams(database).fetchIds();
|
||||
const categories = await queryCategoriesByTeamIds(database, allTeamIds).fetch();
|
||||
const channelCategories = categories.filter((c) => c.type === DMS_CATEGORY);
|
||||
for await (const cc of channelCategories) {
|
||||
const cwc = await cc.toCategoryWithChannels();
|
||||
cwc.channel_ids.unshift(channel.id);
|
||||
categoriesWithChannels.push(cwc);
|
||||
}
|
||||
} else {
|
||||
const categories = await queryCategoriesByTeamIds(database, [teamId]).fetch();
|
||||
const channelCategory = categories.find((c) => c.type === CHANNELS_CATEGORY);
|
||||
if (channelCategory) {
|
||||
const cwc = await channelCategory.toCategoryWithChannels();
|
||||
cwc.channel_ids.unshift(channel.id);
|
||||
categoriesWithChannels.push(cwc);
|
||||
}
|
||||
|
||||
const ccModels = await prepareCategoryChannels(operator, categoriesWithChannels);
|
||||
models.push(...ccModels);
|
||||
}
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {Navigation} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import {getMyChannel} from '@queries/servers/channel';
|
||||
import {getCommonSystemValues, getTeamHistory} from '@queries/servers/system';
|
||||
import {getTeamChannelHistory} from '@queries/servers/team';
|
||||
@@ -13,9 +15,6 @@ import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@scr
|
||||
|
||||
import {switchToChannel} from './channel';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
|
||||
let mockIsTablet: jest.Mock;
|
||||
const now = new Date('2020-01-01').getTime();
|
||||
|
||||
@@ -65,7 +64,7 @@ describe('switchToChannel', () => {
|
||||
} as ChannelMembership;
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
|
||||
operator = DatabaseManager.serverDatabases[serverUrl].operator;
|
||||
spyNow = jest.spyOn(Date, 'now').mockImplementation(() => now);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,43 +1,45 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {General, Navigation as NavigationConstants, Preferences, Screens} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {CHANNELS_CATEGORY, DMS_CATEGORY} from '@constants/categories';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import {extractChannelDisplayName} from '@helpers/database';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import {
|
||||
prepareDeleteChannel, prepareMyChannelsForTeam, queryAllMyChannel,
|
||||
getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes,
|
||||
} from '@queries/servers/channel';
|
||||
import {prepareDeleteChannel, prepareMyChannelsForTeam, queryAllMyChannel, getMyChannel, getChannelById, queryUsersOnChannel} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, removeChannelFromTeamHistory} from '@queries/servers/team';
|
||||
import {getCurrentUser, queryUsersById} from '@queries/servers/user';
|
||||
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, queryMyTeams, removeChannelFromTeamHistory} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {makeCategoryChannelId, makeCategoryId} from '@utils/categories';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logError, logInfo} from '@utils/log';
|
||||
import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme';
|
||||
import {displayGroupMessageName, displayUsername, getUserIdFromChannelName} from '@utils/user';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export async function switchToChannel(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false, prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const models: Model[] = [];
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
let models: Model[] = [];
|
||||
const dt = Date.now();
|
||||
const isTabletDevice = await isTablet();
|
||||
const system = await getCommonSystemValues(database);
|
||||
const member = await getMyChannel(database, channelId);
|
||||
|
||||
EphemeralStore.addSwitchingToChannel(channelId);
|
||||
|
||||
if (member) {
|
||||
const channel = await member.channel.fetch();
|
||||
if (channel) {
|
||||
@@ -55,9 +57,9 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
|
||||
await setCurrentChannelId(operator, '');
|
||||
}
|
||||
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
if (system.currentTeamId !== toTeamId) {
|
||||
modelPromises.push(addTeamToTeamHistory(operator, toTeamId, true));
|
||||
const history = await addTeamToTeamHistory(operator, toTeamId, true);
|
||||
models.push(...history);
|
||||
}
|
||||
|
||||
const commonValues: PrepareCommonSystemValuesArgs = {
|
||||
@@ -69,14 +71,17 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
|
||||
commonValues.currentTeamId = system.currentTeamId === toTeamId ? undefined : toTeamId;
|
||||
}
|
||||
|
||||
modelPromises.push(prepareCommonSystemValues(operator, commonValues));
|
||||
|
||||
if (system.currentChannelId !== channelId || system.currentTeamId !== toTeamId) {
|
||||
modelPromises.push(addChannelToTeamHistory(operator, toTeamId, channelId, true));
|
||||
const common = await prepareCommonSystemValues(operator, commonValues);
|
||||
if (common) {
|
||||
models.push(...common);
|
||||
}
|
||||
|
||||
models = (await Promise.all(modelPromises)).flat();
|
||||
const {member: viewedAt} = await markChannelAsViewed(serverUrl, channelId, false, true);
|
||||
if (system.currentChannelId !== channelId || system.currentTeamId !== toTeamId) {
|
||||
const history = await addChannelToTeamHistory(operator, toTeamId, channelId, true);
|
||||
models.push(...history);
|
||||
}
|
||||
|
||||
const {member: viewedAt} = await markChannelAsViewed(serverUrl, channelId, true);
|
||||
if (viewedAt) {
|
||||
models.push(viewedAt);
|
||||
}
|
||||
@@ -85,6 +90,20 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
PushNotifications.cancelChannelNotifications(channelId);
|
||||
|
||||
if (!EphemeralStore.theme) {
|
||||
// When opening the app from a push notification the theme may not be set in the EphemeralStore
|
||||
// causing the goToScreen to use the Appearance theme instead and that causes the screen background color to potentially
|
||||
// not match the theme
|
||||
const themes = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_THEME, toTeamId).fetch();
|
||||
let theme = Preferences.THEMES.denim;
|
||||
if (themes.length) {
|
||||
theme = setThemeDefaults(JSON.parse(themes[0].value) as Theme);
|
||||
}
|
||||
updateThemeIfNeeded(theme, true);
|
||||
}
|
||||
|
||||
if (isTabletDevice) {
|
||||
dismissAllModalsAndPopToRoot();
|
||||
DeviceEventEmitter.emit(NavigationConstants.NAVIGATION_HOME, Screens.CHANNEL);
|
||||
@@ -92,137 +111,156 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
|
||||
dismissAllModalsAndPopToScreen(Screens.CHANNEL, '', undefined, {topBar: {visible: false}});
|
||||
}
|
||||
|
||||
logInfo('channel switch to', channel?.displayName, channelId, (Date.now() - dt), 'ms');
|
||||
console.log('channel switch to', channel?.displayName, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line
|
||||
}
|
||||
}
|
||||
|
||||
return {models};
|
||||
} catch (error) {
|
||||
logError('Failed to switch to channelId', channelId, 'teamId', teamId, 'error', error);
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {models};
|
||||
}
|
||||
|
||||
export async function removeCurrentUserFromChannel(serverUrl: string, channelId: string, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const serverDatabase = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!serverDatabase) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const models: Model[] = [];
|
||||
const myChannel = await getMyChannel(database, channelId);
|
||||
if (myChannel) {
|
||||
const channel = await myChannel.channel.fetch();
|
||||
if (!channel) {
|
||||
throw new Error('myChannel present but no channel on the database');
|
||||
}
|
||||
models.push(...await prepareDeleteChannel(channel));
|
||||
let teamId = channel.teamId;
|
||||
if (teamId) {
|
||||
teamId = await getCurrentTeamId(database);
|
||||
}
|
||||
const {operator, database} = serverDatabase;
|
||||
|
||||
// We update the history ASAP to avoid clashes with channel switch.
|
||||
await removeChannelFromTeamHistory(operator, teamId, channel.id, false);
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
const models: Model[] = [];
|
||||
const myChannel = await getMyChannel(database, channelId);
|
||||
if (myChannel) {
|
||||
const channel = await myChannel.channel.fetch() as ChannelModel;
|
||||
models.push(...await prepareDeleteChannel(channel));
|
||||
let teamId = channel.teamId;
|
||||
if (teamId) {
|
||||
teamId = await getCurrentTeamId(database);
|
||||
}
|
||||
const system = await removeChannelFromTeamHistory(operator, teamId, channel.id, true);
|
||||
if (system) {
|
||||
models.push(...system);
|
||||
}
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
try {
|
||||
await operator.batchRecords(models);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANGES FOR REMOVE USER FROM CHANNEL');
|
||||
}
|
||||
}
|
||||
return {models};
|
||||
} catch (error) {
|
||||
logError('failed to removeCurrentUserFromChannel', error);
|
||||
return {error};
|
||||
}
|
||||
return {models};
|
||||
}
|
||||
|
||||
export async function setChannelDeleteAt(serverUrl: string, channelId: string, deleteAt: number) {
|
||||
try {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const channel = await getChannelById(database, channelId);
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
const serverDatabase = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!serverDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = channel.prepareUpdate((c) => {
|
||||
c.deleteAt = deleteAt;
|
||||
});
|
||||
const {operator, database} = serverDatabase;
|
||||
|
||||
const channel = await getChannelById(database, channelId);
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = channel.prepareUpdate((c) => {
|
||||
c.deleteAt = deleteAt;
|
||||
});
|
||||
|
||||
try {
|
||||
await operator.batchRecords([model]);
|
||||
} catch (error) {
|
||||
logError('FAILED TO BATCH CHANGES FOR CHANNEL DELETE AT', error);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANGES FOR CHANNEL DELETE AT');
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectAllMyChannelIds(serverUrl: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
return queryAllMyChannel(database).fetchIds();
|
||||
} catch {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return queryAllMyChannel(database).fetchIds();
|
||||
}
|
||||
|
||||
export async function markChannelAsViewed(serverUrl: string, channelId: string, onlyCounts = false, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const member = await getMyChannel(database, channelId);
|
||||
if (!member) {
|
||||
return {error: 'not a member'};
|
||||
}
|
||||
export async function markChannelAsViewed(serverUrl: string, channelId: string, prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
member.prepareUpdate((m) => {
|
||||
m.isUnread = false;
|
||||
m.mentionsCount = 0;
|
||||
m.manuallyUnread = false;
|
||||
if (!onlyCounts) {
|
||||
m.viewedAt = member.lastViewedAt;
|
||||
m.lastViewedAt = Date.now();
|
||||
}
|
||||
});
|
||||
PushNotifications.removeChannelNotifications(serverUrl, channelId);
|
||||
const member = await getMyChannel(operator.database, channelId);
|
||||
if (!member) {
|
||||
return {error: 'not a member'};
|
||||
}
|
||||
|
||||
member.prepareUpdate((m) => {
|
||||
m.isUnread = false;
|
||||
m.mentionsCount = 0;
|
||||
m.manuallyUnread = false;
|
||||
m.viewedAt = member.lastViewedAt;
|
||||
m.lastViewedAt = Date.now();
|
||||
});
|
||||
|
||||
try {
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([member]);
|
||||
}
|
||||
|
||||
return {member};
|
||||
} catch (error) {
|
||||
logError('Failed markChannelAsViewed', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function markChannelAsUnread(serverUrl: string, channelId: string, messageCount: number, mentionsCount: number, lastViewed: number, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const member = await getMyChannel(database, channelId);
|
||||
if (!member) {
|
||||
return {error: 'not a member'};
|
||||
}
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
member.prepareUpdate((m) => {
|
||||
m.viewedAt = lastViewed - 1;
|
||||
m.lastViewedAt = lastViewed - 1;
|
||||
m.messageCount = messageCount;
|
||||
m.mentionsCount = mentionsCount;
|
||||
m.manuallyUnread = true;
|
||||
m.isUnread = true;
|
||||
});
|
||||
const member = await getMyChannel(operator.database, channelId);
|
||||
if (!member) {
|
||||
return {error: 'not a member'};
|
||||
}
|
||||
|
||||
member.prepareUpdate((m) => {
|
||||
m.viewedAt = lastViewed - 1;
|
||||
m.lastViewedAt = lastViewed;
|
||||
m.messageCount = messageCount;
|
||||
m.mentionsCount = mentionsCount;
|
||||
m.manuallyUnread = true;
|
||||
m.isUnread = true;
|
||||
});
|
||||
|
||||
try {
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([member]);
|
||||
}
|
||||
|
||||
return {member};
|
||||
} catch (error) {
|
||||
logError('Failed markChannelAsUnread', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetMessageCount(serverUrl: string, channelId: string) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const member = await getMyChannel(operator.database, channelId);
|
||||
if (!member) {
|
||||
return {error: 'not a member'};
|
||||
}
|
||||
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const member = await getMyChannel(database, channelId);
|
||||
if (!member) {
|
||||
return {error: 'not a member'};
|
||||
}
|
||||
member.prepareUpdate((m) => {
|
||||
m.messageCount = 0;
|
||||
});
|
||||
@@ -230,230 +268,202 @@ export async function resetMessageCount(serverUrl: string, channelId: string) {
|
||||
|
||||
return member;
|
||||
} catch (error) {
|
||||
logError('Failed resetMessageCount', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function storeMyChannelsForTeam(serverUrl: string, teamId: string, channels: Channel[], memberships: ChannelMembership[], prepareRecordsOnly = false, isCRTEnabled?: boolean) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const modelPromises: Array<Promise<Model[]>> = [
|
||||
...await prepareMyChannelsForTeam(operator, teamId, channels, memberships, isCRTEnabled),
|
||||
];
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
if (!models.length) {
|
||||
return {models: []};
|
||||
}
|
||||
|
||||
const flattenedModels = models.flat();
|
||||
|
||||
if (prepareRecordsOnly) {
|
||||
return {models: flattenedModels};
|
||||
}
|
||||
|
||||
if (flattenedModels.length) {
|
||||
await operator.batchRecords(flattenedModels);
|
||||
}
|
||||
|
||||
return {models: flattenedModels};
|
||||
} catch (error) {
|
||||
logError('Failed storeMyChannelsForTeam', error);
|
||||
return {error};
|
||||
export async function storeMyChannelsForTeam(serverUrl: string, teamId: string, channels: Channel[], memberships: ChannelMembership[], prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const modelPromises: Array<Promise<Model[]>> = [
|
||||
...await prepareMyChannelsForTeam(operator, teamId, channels, memberships),
|
||||
];
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
if (!models.length) {
|
||||
return {models: []};
|
||||
}
|
||||
|
||||
const flattenedModels = models.flat() as Model[];
|
||||
|
||||
if (prepareRecordsOnly) {
|
||||
return {models: flattenedModels};
|
||||
}
|
||||
|
||||
if (flattenedModels.length) {
|
||||
try {
|
||||
await operator.batchRecords(flattenedModels);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANNELS');
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
return {models: flattenedModels};
|
||||
}
|
||||
|
||||
export async function updateMyChannelFromWebsocket(serverUrl: string, channelMember: ChannelMembership, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const member = await getMyChannel(database, channelMember.channel_id);
|
||||
if (member) {
|
||||
member.prepareUpdate((m) => {
|
||||
m.roles = channelMember.roles;
|
||||
});
|
||||
if (!prepareRecordsOnly) {
|
||||
operator.batchRecords([member]);
|
||||
}
|
||||
}
|
||||
return {model: member};
|
||||
} catch (error) {
|
||||
logError('Failed updateMyChannelFromWebsocket', error);
|
||||
return {error};
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const member = await getMyChannel(operator.database, channelMember.channel_id);
|
||||
if (member) {
|
||||
member.prepareUpdate((m) => {
|
||||
m.roles = channelMember.roles;
|
||||
});
|
||||
if (!prepareRecordsOnly) {
|
||||
operator.batchRecords([member]);
|
||||
}
|
||||
}
|
||||
return {model: member};
|
||||
}
|
||||
|
||||
export async function updateChannelInfoFromChannel(serverUrl: string, channel: Channel, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const newInfo = await operator.handleChannelInfo({channelInfos: [{
|
||||
header: channel.header,
|
||||
purpose: channel.purpose,
|
||||
id: channel.id,
|
||||
}],
|
||||
prepareRecordsOnly: true});
|
||||
if (!prepareRecordsOnly) {
|
||||
operator.batchRecords(newInfo);
|
||||
}
|
||||
return {model: newInfo};
|
||||
} catch (error) {
|
||||
logError('Failed updateChannelInfoFromChannel', error);
|
||||
return {error};
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const newInfo = await operator.handleChannelInfo({channelInfos: [{
|
||||
header: channel.header,
|
||||
purpose: channel.purpose,
|
||||
id: channel.id,
|
||||
}],
|
||||
prepareRecordsOnly: true});
|
||||
if (!prepareRecordsOnly) {
|
||||
operator.batchRecords(newInfo);
|
||||
}
|
||||
return {model: newInfo};
|
||||
}
|
||||
|
||||
export async function updateLastPostAt(serverUrl: string, channelId: string, lastPostAt: number, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const myChannel = await getMyChannel(database, channelId);
|
||||
if (!myChannel) {
|
||||
return {error: 'not a member'};
|
||||
}
|
||||
|
||||
if (lastPostAt > myChannel.lastPostAt) {
|
||||
myChannel.resetPreparedState();
|
||||
myChannel.prepareUpdate((m) => {
|
||||
m.lastPostAt = lastPostAt;
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([myChannel]);
|
||||
}
|
||||
|
||||
return {member: myChannel};
|
||||
}
|
||||
|
||||
return {member: undefined};
|
||||
} catch (error) {
|
||||
logError('Failed updateLastPostAt', error);
|
||||
return {error};
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const member = await getMyChannel(operator.database, channelId);
|
||||
if (!member) {
|
||||
return {error: 'not a member'};
|
||||
}
|
||||
|
||||
if (lastPostAt > member.lastPostAt) {
|
||||
member.prepareUpdate((m) => {
|
||||
m.lastPostAt = lastPostAt;
|
||||
});
|
||||
|
||||
try {
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([member]);
|
||||
}
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {member};
|
||||
}
|
||||
|
||||
return {member: undefined};
|
||||
}
|
||||
|
||||
export async function updateMyChannelLastFetchedAt(serverUrl: string, channelId: string, lastFetchedAt: number, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const myChannel = await getMyChannel(database, channelId);
|
||||
if (!myChannel) {
|
||||
return {error: 'not a member'};
|
||||
}
|
||||
|
||||
if (lastFetchedAt > myChannel.lastFetchedAt) {
|
||||
myChannel.resetPreparedState();
|
||||
myChannel.prepareUpdate((m) => {
|
||||
m.lastFetchedAt = lastFetchedAt;
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([myChannel]);
|
||||
}
|
||||
|
||||
return {member: myChannel};
|
||||
}
|
||||
|
||||
return {member: undefined};
|
||||
} catch (error) {
|
||||
logError('Failed updateLastFetchedAt', error);
|
||||
return {error};
|
||||
export async function updateChannelsDisplayName(serverUrl: string, channels: ChannelModel[], users: UserProfile[], prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const currentUser = await getCurrentUser(database);
|
||||
if (!currentUser) {
|
||||
return {};
|
||||
}
|
||||
|
||||
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, license);
|
||||
const models: Model[] = [];
|
||||
for await (const channel of channels) {
|
||||
let newDisplayName = '';
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
const otherUserId = getUserIdFromChannelName(currentUser.id, channel.name);
|
||||
const user = users.find((u) => u.id === otherUserId);
|
||||
newDisplayName = displayUsername(user, currentUser.locale, displaySettings, false);
|
||||
} else {
|
||||
const dbProfiles = await queryUsersOnChannel(database, channel.id).fetch();
|
||||
const profileIds = new Set(dbProfiles.map((p) => p.id));
|
||||
const gmUsers = users.filter((u) => profileIds.has(u.id));
|
||||
if (gmUsers.length) {
|
||||
const uIds = new Set(gmUsers.map((u) => u.id));
|
||||
const newProfiles: Array<UserModel|UserProfile> = dbProfiles.filter((u) => !uIds.has(u.id));
|
||||
newProfiles.push(...gmUsers);
|
||||
newDisplayName = displayGroupMessageName(newProfiles, currentUser.locale, displaySettings, currentUser.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (channel.displayName !== newDisplayName) {
|
||||
channel.prepareUpdate((c) => {
|
||||
c.displayName = extractChannelDisplayName({type: c.type, display_name: newDisplayName}, c);
|
||||
});
|
||||
models.push(channel);
|
||||
}
|
||||
}
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
}
|
||||
|
||||
type User = UserProfile | UserModel;
|
||||
export async function updateChannelsDisplayName(serverUrl: string, channels: ChannelModel[], users: User[], prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
if (!currentUser) {
|
||||
return {};
|
||||
}
|
||||
export async function addChannelToDefaultCategory(serverUrl: string, channel: Channel | ChannelModel, prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(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 models: Model[] = [];
|
||||
for await (const channel of channels) {
|
||||
let newDisplayName = '';
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
const otherUserId = getUserIdFromChannelName(currentUser.id, channel.name);
|
||||
const user = users.find((u) => u.id === otherUserId);
|
||||
newDisplayName = displayUsername(user, currentUser.locale, displaySettings, false);
|
||||
} else {
|
||||
const dbProfiles = await queryUsersOnChannel(database, channel.id).fetch();
|
||||
const profileIds = new Set(dbProfiles.map((p) => p.id));
|
||||
const gmUsers = users.filter((u) => profileIds.has(u.id));
|
||||
if (gmUsers.length) {
|
||||
const uIds = new Set(gmUsers.map((u) => u.id));
|
||||
const newProfiles: Array<UserModel|UserProfile> = dbProfiles.filter((u) => !uIds.has(u.id));
|
||||
newProfiles.push(...gmUsers);
|
||||
newDisplayName = displayGroupMessageName(newProfiles, currentUser.locale, displaySettings, currentUser.id);
|
||||
}
|
||||
}
|
||||
const {database} = operator;
|
||||
|
||||
if (newDisplayName && channel.displayName !== newDisplayName) {
|
||||
channel.prepareUpdate((c) => {
|
||||
c.displayName = extractChannelDisplayName({
|
||||
type: c.type,
|
||||
display_name: newDisplayName,
|
||||
fake: true,
|
||||
}, c);
|
||||
});
|
||||
models.push(channel);
|
||||
}
|
||||
}
|
||||
const teamId = 'teamId' in channel ? channel.teamId : channel.team_id;
|
||||
const userId = await getCurrentUserId(database);
|
||||
if (!userId) {
|
||||
return {error: 'no current user id'};
|
||||
}
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
if (!isDMorGM(channel)) {
|
||||
const models = await operator.handleCategoryChannels({categoryChannels: [{
|
||||
category_id: makeCategoryId(CHANNELS_CATEGORY, userId, teamId),
|
||||
channel_id: channel.id,
|
||||
sort_order: 0,
|
||||
id: makeCategoryChannelId(teamId, channel.id),
|
||||
}],
|
||||
prepareRecordsOnly});
|
||||
|
||||
return {models};
|
||||
} catch (error) {
|
||||
logError('Failed updateChannelsDisplayName', error);
|
||||
return {error};
|
||||
}
|
||||
|
||||
const allTeams = await queryMyTeams(database).fetch();
|
||||
const models = (
|
||||
await Promise.all(
|
||||
allTeams.map(
|
||||
(t) => operator.handleCategoryChannels({categoryChannels: [{
|
||||
category_id: makeCategoryId(DMS_CATEGORY, userId, t.id),
|
||||
channel_id: channel.id,
|
||||
sort_order: 0,
|
||||
id: makeCategoryChannelId(t.id, channel.id),
|
||||
}],
|
||||
prepareRecordsOnly: true}),
|
||||
),
|
||||
)
|
||||
).flat();
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
}
|
||||
|
||||
export async function showUnreadChannelsOnly(serverUrl: string, onlyUnreads: boolean) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
return operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.ONLY_UNREADS,
|
||||
value: JSON.stringify(onlyUnreads),
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('Failed showUnreadChannelsOnly', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export const updateDmGmDisplayName = async (serverUrl: string) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentUserId = await getCurrentUserId(database);
|
||||
if (!currentUserId) {
|
||||
return {error: 'The current user id could not be retrieved from the database'};
|
||||
}
|
||||
|
||||
const channels = await queryUserChannelsByTypes(database, currentUserId, ['G', 'D']).fetch();
|
||||
const userIds = channels.reduce((acc: string[], ch) => {
|
||||
if (ch.type !== General.DM_CHANNEL) {
|
||||
return acc;
|
||||
}
|
||||
const uid = getUserIdFromChannelName(currentUserId, ch.name);
|
||||
acc.push(uid);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const users = await queryUsersById(database, userIds).fetch();
|
||||
|
||||
await updateChannelsDisplayName(serverUrl, channels, users, false);
|
||||
|
||||
return {channels};
|
||||
} catch (error) {
|
||||
logError('Failed updateDmGmDisplayName', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,155 +3,165 @@
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getDraft} from '@queries/servers/drafts';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export async function updateDraftFile(serverUrl: string, channelId: string, rootId: string, file: FileInfo, prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const draft = await getDraft(operator.database, channelId, rootId);
|
||||
if (!draft) {
|
||||
return {error: 'no draft'};
|
||||
}
|
||||
|
||||
const i = draft.files.findIndex((v) => v.clientId === file.clientId);
|
||||
if (i === -1) {
|
||||
return {error: 'file not found'};
|
||||
}
|
||||
|
||||
// We create a new list to make sure we re-render the draft input.
|
||||
const newFiles = [...draft.files];
|
||||
newFiles[i] = file;
|
||||
draft.prepareUpdate((d) => {
|
||||
d.files = newFiles;
|
||||
});
|
||||
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const draft = await getDraft(database, channelId, rootId);
|
||||
if (!draft) {
|
||||
return {error: 'no draft'};
|
||||
}
|
||||
|
||||
const i = draft.files.findIndex((v) => v.clientId === file.clientId);
|
||||
if (i === -1) {
|
||||
return {error: 'file not found'};
|
||||
}
|
||||
|
||||
// We create a new list to make sure we re-render the draft input.
|
||||
const newFiles = [...draft.files];
|
||||
newFiles[i] = file;
|
||||
draft.prepareUpdate((d) => {
|
||||
d.files = newFiles;
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([draft]);
|
||||
}
|
||||
|
||||
return {draft};
|
||||
} catch (error) {
|
||||
logError('Failed updateDraftFile', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeDraftFile(serverUrl: string, channelId: string, rootId: string, clientId: string, prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const draft = await getDraft(operator.database, channelId, rootId);
|
||||
if (!draft) {
|
||||
return {error: 'no draft'};
|
||||
}
|
||||
|
||||
const i = draft.files.findIndex((v) => v.clientId === clientId);
|
||||
if (i === -1) {
|
||||
return {error: 'file not found'};
|
||||
}
|
||||
|
||||
if (draft.files.length === 1 && !draft.message) {
|
||||
draft.prepareDestroyPermanently();
|
||||
} else {
|
||||
draft.prepareUpdate((d) => {
|
||||
d.files = draft.files.filter((v, index) => index !== i);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const draft = await getDraft(database, channelId, rootId);
|
||||
if (!draft) {
|
||||
return {error: 'no draft'};
|
||||
}
|
||||
|
||||
const i = draft.files.findIndex((v) => v.clientId === clientId);
|
||||
if (i === -1) {
|
||||
return {error: 'file not found'};
|
||||
}
|
||||
|
||||
if (draft.files.length === 1 && !draft.message) {
|
||||
draft.prepareDestroyPermanently();
|
||||
} else {
|
||||
draft.prepareUpdate((d) => {
|
||||
d.files = draft.files.filter((v, index) => index !== i);
|
||||
});
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([draft]);
|
||||
}
|
||||
|
||||
return {draft};
|
||||
} catch (error) {
|
||||
logError('Failed removeDraftFile', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateDraftMessage(serverUrl: string, channelId: string, rootId: string, message: string, prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const draft = await getDraft(operator.database, channelId, rootId);
|
||||
if (!draft) {
|
||||
if (!message) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const newDraft: Draft = {
|
||||
channel_id: channelId,
|
||||
root_id: rootId,
|
||||
message,
|
||||
};
|
||||
|
||||
return operator.handleDraft({drafts: [newDraft], prepareRecordsOnly});
|
||||
}
|
||||
|
||||
if (draft.message === message) {
|
||||
return {draft};
|
||||
}
|
||||
|
||||
if (draft.files.length === 0 && !message) {
|
||||
draft.prepareDestroyPermanently();
|
||||
} else {
|
||||
draft.prepareUpdate((d) => {
|
||||
d.message = message;
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const draft = await getDraft(database, channelId, rootId);
|
||||
if (!draft) {
|
||||
if (!message) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const newDraft: Draft = {
|
||||
channel_id: channelId,
|
||||
root_id: rootId,
|
||||
message,
|
||||
};
|
||||
|
||||
return operator.handleDraft({drafts: [newDraft], prepareRecordsOnly});
|
||||
}
|
||||
|
||||
if (draft.message === message) {
|
||||
return {draft};
|
||||
}
|
||||
|
||||
if (draft.files.length === 0 && !message) {
|
||||
draft.prepareDestroyPermanently();
|
||||
} else {
|
||||
draft.prepareUpdate((d) => {
|
||||
d.message = message;
|
||||
});
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([draft]);
|
||||
}
|
||||
|
||||
return {draft};
|
||||
} catch (error) {
|
||||
logError('Failed updateDraftMessage', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function addFilesToDraft(serverUrl: string, channelId: string, rootId: string, files: FileInfo[], prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const draft = await getDraft(operator.database, channelId, rootId);
|
||||
if (!draft) {
|
||||
const newDraft: Draft = {
|
||||
channel_id: channelId,
|
||||
root_id: rootId,
|
||||
files,
|
||||
message: '',
|
||||
};
|
||||
|
||||
return operator.handleDraft({drafts: [newDraft], prepareRecordsOnly});
|
||||
}
|
||||
|
||||
draft.prepareUpdate((d) => {
|
||||
d.files = [...draft.files, ...files];
|
||||
});
|
||||
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const draft = await getDraft(database, channelId, rootId);
|
||||
if (!draft) {
|
||||
const newDraft: Draft = {
|
||||
channel_id: channelId,
|
||||
root_id: rootId,
|
||||
files,
|
||||
message: '',
|
||||
};
|
||||
|
||||
return operator.handleDraft({drafts: [newDraft], prepareRecordsOnly});
|
||||
}
|
||||
|
||||
draft.prepareUpdate((d) => {
|
||||
d.files = [...draft.files, ...files];
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([draft]);
|
||||
}
|
||||
|
||||
return {draft};
|
||||
} catch (error) {
|
||||
logError('Failed addFilesToDraft', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export const removeDraft = async (serverUrl: string, channelId: string, rootId = '') => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const draft = await getDraft(database, channelId, rootId);
|
||||
if (draft) {
|
||||
await database.write(async () => {
|
||||
await draft.destroyPermanently();
|
||||
});
|
||||
}
|
||||
|
||||
return {draft};
|
||||
} catch (error) {
|
||||
logError('Failed removeDraft', error);
|
||||
return {error};
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const draft = await getDraft(database, channelId, rootId);
|
||||
if (draft) {
|
||||
await database.write(async () => {
|
||||
await draft.destroyPermanently();
|
||||
});
|
||||
}
|
||||
|
||||
return {draft};
|
||||
};
|
||||
|
||||
@@ -3,21 +3,23 @@
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getFileById} from '@queries/servers/file';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export const updateLocalFile = async (serverUrl: string, file: FileInfo) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
return operator.handleFiles({files: [file], prepareRecordsOnly: false});
|
||||
} catch (error) {
|
||||
logError('Failed updateLocalFile', error);
|
||||
return {error};
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
return operator.handleFiles({files: [file], prepareRecordsOnly: false});
|
||||
};
|
||||
|
||||
export const updateLocalFilePath = async (serverUrl: string, fileId: string, localPath: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const file = await getFileById(database, fileId);
|
||||
if (file) {
|
||||
await database.write(async () => {
|
||||
@@ -29,8 +31,6 @@ export const updateLocalFilePath = async (serverUrl: string, fileId: string, loc
|
||||
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
logError('Failed updateLocalFilePath', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchFilteredChannelGroups, fetchFilteredTeamGroups, fetchGroupsForAutocomplete} from '@actions/remote/groups';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryGroupsByName, queryGroupsByNameInChannel, queryGroupsByNameInTeam} from '@queries/servers/group';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type GroupModel from '@typings/database/models/servers/group';
|
||||
|
||||
export const searchGroupsByName = async (serverUrl: string, name: string): Promise<GroupModel[]> => {
|
||||
let database;
|
||||
|
||||
try {
|
||||
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
|
||||
} catch (e) {
|
||||
logError('searchGroupsByName - DB Error', e);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const groups = await fetchGroupsForAutocomplete(serverUrl, name);
|
||||
|
||||
if (groups && Array.isArray(groups)) {
|
||||
return groups;
|
||||
}
|
||||
throw groups.error;
|
||||
} catch (e) {
|
||||
logError('searchGroupsByName - ERROR', e);
|
||||
return queryGroupsByName(database, name).fetch();
|
||||
}
|
||||
};
|
||||
|
||||
export const searchGroupsByNameInTeam = async (serverUrl: string, name: string, teamId: string): Promise<GroupModel[]> => {
|
||||
let database;
|
||||
|
||||
try {
|
||||
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
|
||||
} catch (e) {
|
||||
logError('searchGroupsByNameInTeam - DB Error', e);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const groups = await fetchFilteredTeamGroups(serverUrl, name, teamId);
|
||||
|
||||
if (groups && Array.isArray(groups)) {
|
||||
return groups;
|
||||
}
|
||||
throw groups.error;
|
||||
} catch (e) {
|
||||
logError('searchGroupsByNameInTeam - ERROR', e);
|
||||
return queryGroupsByNameInTeam(database, name, teamId).fetch();
|
||||
}
|
||||
};
|
||||
|
||||
export const searchGroupsByNameInChannel = async (serverUrl: string, name: string, channelId: string): Promise<GroupModel[]> => {
|
||||
let database;
|
||||
|
||||
try {
|
||||
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
|
||||
} catch (e) {
|
||||
logError('searchGroupsByNameInChannel - DB Error', e);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const groups = await fetchFilteredChannelGroups(serverUrl, name, channelId);
|
||||
|
||||
if (groups && Array.isArray(groups)) {
|
||||
return groups;
|
||||
}
|
||||
throw groups.error;
|
||||
} catch (e) {
|
||||
logError('searchGroupsByNameInChannel - ERROR', e);
|
||||
return queryGroupsByNameInChannel(database, name, channelId).fetch();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPostById, queryPostsInChannel, queryPostsInThread} from '@queries/servers/post';
|
||||
import {logError} from '@utils/log';
|
||||
import {getPostById, queryPostsInChannel} from '@queries/servers/post';
|
||||
|
||||
export const updatePostSinceCache = async (serverUrl: string, notification: NotificationWithData) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
if (notification.payload?.channel_id) {
|
||||
const {database} = operator;
|
||||
const chunks = await queryPostsInChannel(database, notification.payload.channel_id).fetch();
|
||||
if (chunks.length) {
|
||||
const recent = chunks[0];
|
||||
@@ -24,35 +28,7 @@ export const updatePostSinceCache = async (serverUrl: string, notification: Noti
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
logError('Failed updatePostSinceCache', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePostsInThreadsSinceCache = async (serverUrl: string, notification: NotificationWithData) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
if (notification.payload?.root_id) {
|
||||
const {database} = operator;
|
||||
const chunks = await queryPostsInThread(database, notification.payload.root_id).fetch();
|
||||
if (chunks.length) {
|
||||
const recent = chunks[0];
|
||||
const lastPost = await getPostById(database, notification.payload.post_id);
|
||||
if (lastPost) {
|
||||
await operator.database.write(async () => {
|
||||
await recent.update(() => {
|
||||
recent.latest = lastPost.createAt;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,88 +1,31 @@
|
||||
// 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 {MM_TABLES} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPostById, prepareDeletePost, queryPostsById} from '@queries/servers/post';
|
||||
import {getPostById, prepareDeletePost} from '@queries/servers/post';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers/thread';
|
||||
import {generateId} from '@utils/general';
|
||||
import {logError} from '@utils/log';
|
||||
import {getLastFetchedAtFromPosts} from '@utils/post';
|
||||
import {getPostIdsForCombinedUserActivityPost} from '@utils/post_list';
|
||||
|
||||
import {updateLastPostAt, updateMyChannelLastFetchedAt} from './channel';
|
||||
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const {SERVER: {DRAFT, FILE, POST, POSTS_IN_THREAD, REACTION, THREAD, THREAD_PARTICIPANT, THREADS_IN_TEAM}} = MM_TABLES;
|
||||
|
||||
export const sendAddToChannelEphemeralPost = async (serverUrl: string, user: UserModel, addedUsernames: string[], messages: string[], channeId: string, postRootId = '') => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const timestamp = Date.now();
|
||||
const posts = addedUsernames.map((addedUsername, index) => {
|
||||
const message = messages[index];
|
||||
return {
|
||||
id: generateId(),
|
||||
user_id: user.id,
|
||||
channel_id: channeId,
|
||||
message,
|
||||
type: Post.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL as PostType,
|
||||
create_at: timestamp,
|
||||
edit_at: 0,
|
||||
update_at: timestamp,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
original_id: '',
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 0,
|
||||
metadata: {},
|
||||
root_id: postRootId,
|
||||
props: {
|
||||
username: user.username,
|
||||
addedUsername,
|
||||
},
|
||||
} as Post;
|
||||
});
|
||||
|
||||
await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||
order: posts.map((p) => p.id),
|
||||
posts,
|
||||
});
|
||||
|
||||
return {posts};
|
||||
} catch (error) {
|
||||
logError('Failed sendAddToChannelEphemeralPost', error);
|
||||
return {error};
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
};
|
||||
|
||||
export const sendEphemeralPost = async (serverUrl: string, message: string, channeId: string, rootId = '', userId?: string) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
if (!channeId) {
|
||||
throw new Error('channel Id not defined');
|
||||
}
|
||||
|
||||
let authorId = userId;
|
||||
if (!authorId) {
|
||||
authorId = await getCurrentUserId(database);
|
||||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
const post = {
|
||||
const timestamp = Date.now();
|
||||
const posts = addedUsernames.map((addedUsername, index) => {
|
||||
const message = messages[index];
|
||||
return {
|
||||
id: generateId(),
|
||||
user_id: authorId,
|
||||
user_id: user.id,
|
||||
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,
|
||||
@@ -93,185 +36,122 @@ export const sendEphemeralPost = async (serverUrl: string, message: string, chan
|
||||
pending_post_id: '',
|
||||
reply_count: 0,
|
||||
metadata: {},
|
||||
participants: null,
|
||||
root_id: rootId,
|
||||
props: {},
|
||||
root_id: postRootId,
|
||||
props: {
|
||||
username: user.username,
|
||||
addedUsername,
|
||||
},
|
||||
} as Post;
|
||||
});
|
||||
|
||||
await fetchPostAuthors(serverUrl, [post], false);
|
||||
await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||
order: [post.id],
|
||||
posts: [post],
|
||||
});
|
||||
await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||
order: posts.map((p) => p.id),
|
||||
posts,
|
||||
});
|
||||
|
||||
return {post};
|
||||
} catch (error) {
|
||||
logError('Failed sendEphemeralPost', error);
|
||||
return {error};
|
||||
return {posts};
|
||||
};
|
||||
|
||||
export const sendEphemeralPost = async (serverUrl: string, message: string, channeId: string, rootId = '', userId?: string) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
if (!channeId) {
|
||||
return {error: 'channel Id not defined'};
|
||||
}
|
||||
|
||||
let authorId = userId;
|
||||
if (!authorId) {
|
||||
authorId = await getCurrentUserId(operator.database);
|
||||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
const post = {
|
||||
id: generateId(),
|
||||
user_id: authorId,
|
||||
channel_id: channeId,
|
||||
message,
|
||||
type: Post.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL as PostType,
|
||||
create_at: timestamp,
|
||||
edit_at: 0,
|
||||
update_at: timestamp,
|
||||
delete_at: 0,
|
||||
is_pinned: false,
|
||||
original_id: '',
|
||||
hashtags: '',
|
||||
pending_post_id: '',
|
||||
reply_count: 0,
|
||||
metadata: {},
|
||||
participants: null,
|
||||
root_id: rootId,
|
||||
props: {},
|
||||
} as Post;
|
||||
|
||||
await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||
order: [post.id],
|
||||
posts: [post],
|
||||
});
|
||||
|
||||
return {post};
|
||||
};
|
||||
|
||||
export async function removePost(serverUrl: string, post: PostModel | Post) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
if (post.type === Post.POST_TYPES.COMBINED_USER_ACTIVITY && post.props?.system_post_ids) {
|
||||
const systemPostIds = getPostIdsForCombinedUserActivityPost(post.id);
|
||||
const removeModels = [];
|
||||
for await (const id of systemPostIds) {
|
||||
const postModel = await getPostById(database, id);
|
||||
if (postModel) {
|
||||
const preparedPost = await prepareDeletePost(postModel);
|
||||
removeModels.push(...preparedPost);
|
||||
}
|
||||
}
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
if (removeModels.length) {
|
||||
await operator.batchRecords(removeModels);
|
||||
}
|
||||
} else {
|
||||
const postModel = await getPostById(database, post.id);
|
||||
if (post.type === Post.POST_TYPES.COMBINED_USER_ACTIVITY && post.props?.system_post_ids) {
|
||||
const systemPostIds = getPostIdsForCombinedUserActivityPost(post.id);
|
||||
const removeModels = [];
|
||||
for await (const id of systemPostIds) {
|
||||
const postModel = await getPostById(operator.database, id);
|
||||
if (postModel) {
|
||||
const preparedPost = await prepareDeletePost(postModel);
|
||||
if (preparedPost.length) {
|
||||
await operator.batchRecords(preparedPost);
|
||||
}
|
||||
removeModels.push(...preparedPost);
|
||||
}
|
||||
}
|
||||
|
||||
return {post};
|
||||
} catch (error) {
|
||||
logError('Failed removePost', error);
|
||||
return {error};
|
||||
if (removeModels.length) {
|
||||
await operator.batchRecords(removeModels);
|
||||
}
|
||||
} else {
|
||||
const postModel = await getPostById(operator.database, post.id);
|
||||
if (postModel) {
|
||||
const preparedPost = await prepareDeletePost(postModel);
|
||||
if (preparedPost.length) {
|
||||
await operator.batchRecords(preparedPost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {post};
|
||||
}
|
||||
|
||||
export async function markPostAsDeleted(serverUrl: string, post: Post, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const dbPost = await getPostById(database, post.id);
|
||||
if (!dbPost) {
|
||||
throw new Error('Post not found');
|
||||
}
|
||||
|
||||
const model = dbPost.prepareUpdate((p) => {
|
||||
p.deleteAt = Date.now();
|
||||
p.message = '';
|
||||
p.metadata = null;
|
||||
p.props = undefined;
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([dbPost]);
|
||||
}
|
||||
return {model};
|
||||
} catch (error) {
|
||||
logError('Failed markPostAsDeleted', error);
|
||||
return {error};
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
}
|
||||
|
||||
export async function storePostsForChannel(
|
||||
serverUrl: string, channelId: string, posts: Post[], order: string[], previousPostId: string,
|
||||
actionType: string, authors: UserProfile[], prepareRecordsOnly = false,
|
||||
) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
|
||||
const models = [];
|
||||
const postModels = await operator.handlePosts({
|
||||
actionType,
|
||||
order,
|
||||
posts,
|
||||
previousPostId,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
models.push(...postModels);
|
||||
|
||||
if (authors.length) {
|
||||
const userModels = await operator.handleUsers({users: authors, prepareRecordsOnly: true});
|
||||
models.push(...userModels);
|
||||
}
|
||||
|
||||
const lastFetchedAt = getLastFetchedAtFromPosts(posts);
|
||||
let myChannelModel: MyChannelModel | undefined;
|
||||
if (lastFetchedAt) {
|
||||
const {member} = await updateMyChannelLastFetchedAt(serverUrl, channelId, lastFetchedAt, true);
|
||||
myChannelModel = member;
|
||||
}
|
||||
|
||||
let lastPostAt = 0;
|
||||
for (const post of posts) {
|
||||
const isCrtReply = isCRTEnabled && post.root_id !== '';
|
||||
if (!isCrtReply) {
|
||||
lastPostAt = post.create_at > lastPostAt ? post.create_at : lastPostAt;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastPostAt) {
|
||||
const {member} = await updateLastPostAt(serverUrl, channelId, lastPostAt, true);
|
||||
if (member) {
|
||||
myChannelModel = member;
|
||||
}
|
||||
}
|
||||
|
||||
if (myChannelModel) {
|
||||
models.push(myChannelModel);
|
||||
}
|
||||
|
||||
if (isCRTEnabled) {
|
||||
const threadModels = await prepareThreadsFromReceivedPosts(operator, posts, false);
|
||||
if (threadModels?.length) {
|
||||
models.push(...threadModels);
|
||||
}
|
||||
}
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
} catch (error) {
|
||||
logError('storePostsForChannel', error);
|
||||
return {error};
|
||||
const dbPost = await getPostById(operator.database, post.id);
|
||||
if (!dbPost) {
|
||||
return {error: 'Post not found'};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPosts(serverUrl: string, ids: string[]) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
return queryPostsById(database, ids).fetch();
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePosts(serverUrl: string, postIds: string[]) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const postsFormatted = `'${postIds.join("','")}'`;
|
||||
|
||||
await database.write(() => {
|
||||
return database.adapter.unsafeExecute({
|
||||
sqls: [
|
||||
[`DELETE FROM ${POST} where id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${REACTION} where post_id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${FILE} where post_id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${DRAFT} where root_id IN (${postsFormatted})`, []],
|
||||
|
||||
[`DELETE FROM ${POSTS_IN_THREAD} where root_id IN (${postsFormatted})`, []],
|
||||
|
||||
[`DELETE FROM ${THREAD} where id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${THREAD_PARTICIPANT} where thread_id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${THREADS_IN_TEAM} where thread_id IN (${postsFormatted})`, []],
|
||||
],
|
||||
});
|
||||
});
|
||||
return {error: false};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
const model = dbPost.prepareUpdate((p) => {
|
||||
p.deleteAt = Date.now();
|
||||
p.message = '';
|
||||
p.metadata = null;
|
||||
p.props = undefined;
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([dbPost]);
|
||||
}
|
||||
return {model};
|
||||
}
|
||||
|
||||
@@ -5,18 +5,23 @@ import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getRecentReactions} from '@queries/servers/system';
|
||||
import {getEmojiFirstAlias} from '@utils/emoji/helpers';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
const MAXIMUM_RECENT_EMOJI = 27;
|
||||
|
||||
export const addRecentReaction = async (serverUrl: string, emojiNames: string[], prepareRecordsOnly = false) => {
|
||||
try {
|
||||
if (!emojiNames.length) {
|
||||
return [];
|
||||
}
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const {database} = operator;
|
||||
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
let recent = await getRecentReactions(database);
|
||||
if (!emojiNames.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let recent = await getRecentReactions(database);
|
||||
|
||||
try {
|
||||
const recentEmojis = new Set(recent);
|
||||
const aliases = emojiNames.map((e) => getEmojiFirstAlias(e));
|
||||
for (const alias of aliases) {
|
||||
@@ -38,7 +43,6 @@ export const addRecentReaction = async (serverUrl: string, emojiNames: string[],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('Failed addRecentReaction', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import {queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {getConfig, getLicense, getGlobalDataRetentionPolicy, getGranularDataRetentionPolicies, getLastGlobalDataRetentionRun, getIsDataRetentionEnabled} from '@queries/servers/system';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import {deletePosts} from './post';
|
||||
|
||||
import type {DataRetentionPoliciesRequest} from '@actions/remote/systems';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const {SERVER: {POST}} = MM_TABLES;
|
||||
|
||||
export async function storeConfigAndLicense(serverUrl: string, config: ClientConfig, license: ClientLicense) {
|
||||
try {
|
||||
// If we have credentials for this server then update the values in the database
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials) {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentLicense = await getLicense(database);
|
||||
const systems: IdValue[] = [];
|
||||
|
||||
if (!deepEqual(license, currentLicense)) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.LICENSE,
|
||||
value: JSON.stringify(license),
|
||||
});
|
||||
}
|
||||
|
||||
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 storeDataRetentionPolicies(serverUrl: string, data: DataRetentionPoliciesRequest, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {globalPolicy, teamPolicies, channelPolicies} = data;
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const systems: IdValue[] = [{
|
||||
id: SYSTEM_IDENTIFIERS.DATA_RETENTION_POLICIES,
|
||||
value: globalPolicy || {},
|
||||
}, {
|
||||
id: SYSTEM_IDENTIFIERS.GRANULAR_DATA_RETENTION_POLICIES,
|
||||
value: {
|
||||
team: teamPolicies || [],
|
||||
channel: channelPolicies || [],
|
||||
},
|
||||
}];
|
||||
|
||||
return operator.handleSystem({
|
||||
systems,
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateLastDataRetentionRun(serverUrl: string, value?: number, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const systems: IdValue[] = [{
|
||||
id: SYSTEM_IDENTIFIERS.LAST_DATA_RETENTION_RUN,
|
||||
value: value || Date.now(),
|
||||
}];
|
||||
|
||||
return operator.handleSystem({systems, prepareRecordsOnly});
|
||||
} catch (error) {
|
||||
logError('Failed updateLastDataRetentionRun', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function dataRetentionCleanup(serverUrl: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
|
||||
if (!isDataRetentionEnabled) {
|
||||
return {error: undefined};
|
||||
}
|
||||
|
||||
const lastRunAt = await getLastGlobalDataRetentionRun(database);
|
||||
const lastCleanedToday = new Date(lastRunAt).toDateString() === new Date().toDateString();
|
||||
|
||||
// Do not run if clean up is already done today
|
||||
if (lastRunAt && lastCleanedToday) {
|
||||
return {error: undefined};
|
||||
}
|
||||
|
||||
const globalPolicy = await getGlobalDataRetentionPolicy(database);
|
||||
const granularPoliciesData = await getGranularDataRetentionPolicies(database);
|
||||
|
||||
// Get global data retention cutoff
|
||||
let globalRetentionCutoff = 0;
|
||||
if (globalPolicy?.message_deletion_enabled) {
|
||||
globalRetentionCutoff = globalPolicy.message_retention_cutoff;
|
||||
}
|
||||
|
||||
// Get Granular data retention policies
|
||||
let teamPolicies: TeamDataRetentionPolicy[] = [];
|
||||
let channelPolicies: ChannelDataRetentionPolicy[] = [];
|
||||
if (granularPoliciesData) {
|
||||
teamPolicies = granularPoliciesData.team;
|
||||
channelPolicies = granularPoliciesData.channel;
|
||||
}
|
||||
|
||||
const channelsCutoffs: {[key: string]: number} = {};
|
||||
|
||||
// Get channel level cutoff from team policies
|
||||
for await (const teamPolicy of teamPolicies) {
|
||||
const {team_id, post_duration} = teamPolicy;
|
||||
const channelIds = await queryAllChannelsForTeam(database, team_id).fetchIds();
|
||||
if (channelIds.length) {
|
||||
const cutoff = getDataRetentionPolicyCutoff(post_duration);
|
||||
channelIds.forEach((channelId) => {
|
||||
channelsCutoffs[channelId] = cutoff;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get channel level cutoff from channel policies
|
||||
channelPolicies.forEach(({channel_id, post_duration}) => {
|
||||
channelsCutoffs[channel_id] = getDataRetentionPolicyCutoff(post_duration);
|
||||
});
|
||||
|
||||
const conditions = [];
|
||||
|
||||
const channelIds = Object.keys(channelsCutoffs);
|
||||
if (channelIds.length) {
|
||||
// Fetch posts by channel level cutoff
|
||||
for (const channelId of channelIds) {
|
||||
const cutoff = channelsCutoffs[channelId];
|
||||
conditions.push(`(channel_id='${channelId}' AND create_at < ${cutoff})`);
|
||||
}
|
||||
|
||||
// Fetch posts by global cutoff which are not already fetched by channel level cutoff
|
||||
conditions.push(`(channel_id NOT IN ('${channelIds.join("','")}') AND create_at < ${globalRetentionCutoff})`);
|
||||
} else {
|
||||
conditions.push(`create_at < ${globalRetentionCutoff}`);
|
||||
}
|
||||
|
||||
const postIds = await database.get<PostModel>(POST).query(
|
||||
Q.unsafeSqlQuery(`SELECT * FROM ${POST} where ${conditions.join(' OR ')}`),
|
||||
).fetchIds();
|
||||
|
||||
if (postIds.length) {
|
||||
const batchSize = 1000;
|
||||
const deletePromises = [];
|
||||
for (let i = 0; i < postIds.length; i += batchSize) {
|
||||
const batch = postIds.slice(i, batchSize);
|
||||
deletePromises.push(
|
||||
deletePosts(serverUrl, batch),
|
||||
);
|
||||
}
|
||||
const deleteResult = await Promise.all(deletePromises);
|
||||
for (const {error} of deleteResult) {
|
||||
if (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await updateLastDataRetentionRun(serverUrl);
|
||||
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
logError('An error occurred while performing data retention cleanup', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
// Returns cutoff time based on the policy's post_duration
|
||||
function getDataRetentionPolicyCutoff(postDuration: number) {
|
||||
const periodDate = new Date();
|
||||
periodDate.setDate(periodDate.getDate() - postDuration);
|
||||
periodDate.setHours(0);
|
||||
periodDate.setMinutes(0);
|
||||
periodDate.setSeconds(0);
|
||||
return periodDate.getTime();
|
||||
}
|
||||
|
||||
export async function setLastServerVersionCheck(serverUrl: string, reset = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
await operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.LAST_SERVER_VERSION_CHECK,
|
||||
value: reset ? 0 : Date.now(),
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('setLastServerVersionCheck', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setGlobalThreadsTab(serverUrl: string, globalThreadsTab: GlobalThreadsTab) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
await operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.GLOBAL_THREADS_TAB,
|
||||
value: globalThreadsTab,
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('setGlobalThreadsTab', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function dismissAnnouncement(serverUrl: string, announcementText: string) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.LAST_DISMISSED_BANNER, value: announcementText}], prepareRecordsOnly: false});
|
||||
} catch (error) {
|
||||
logError('An error occurred while dismissing an announcement', error);
|
||||
}
|
||||
}
|
||||
@@ -2,86 +2,33 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareDeleteTeam, getMyTeamById, queryTeamSearchHistoryByTeamId, removeTeamFromTeamHistory, getTeamSearchHistoryById, getTeamById} from '@queries/servers/team';
|
||||
import {logError} from '@utils/log';
|
||||
import {prepareDeleteTeam, getMyTeamById, removeTeamFromTeamHistory} from '@queries/servers/team';
|
||||
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
export async function removeUserFromTeam(serverUrl: string, teamId: string) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const myTeam = await getMyTeamById(database, teamId);
|
||||
if (myTeam) {
|
||||
const team = await getTeamById(database, myTeam.id);
|
||||
if (!team) {
|
||||
throw new Error('Team not found');
|
||||
}
|
||||
const models = await prepareDeleteTeam(team);
|
||||
const system = await removeTeamFromTeamHistory(operator, team.id, true);
|
||||
if (system) {
|
||||
models.push(...system);
|
||||
}
|
||||
if (models.length) {
|
||||
const serverDatabase = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!serverDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {operator, database} = serverDatabase;
|
||||
|
||||
const myTeam = await getMyTeamById(database, teamId);
|
||||
if (myTeam) {
|
||||
const team = await myTeam.team.fetch() as TeamModel;
|
||||
const models = await prepareDeleteTeam(team);
|
||||
const system = await removeTeamFromTeamHistory(operator, team.id, true);
|
||||
if (system) {
|
||||
models.push(...system);
|
||||
}
|
||||
if (models.length) {
|
||||
try {
|
||||
await operator.batchRecords(models);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANGES FOR REMOVE USER FROM TEAM');
|
||||
}
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
logError('Failed removeUserFromTeam', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function addSearchToTeamSearchHistory(serverUrl: string, teamId: string, terms: string) {
|
||||
const MAX_TEAM_SEARCHES = 15;
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const newSearch: TeamSearchHistory = {
|
||||
created_at: Date.now(),
|
||||
display_term: terms,
|
||||
term: terms,
|
||||
team_id: teamId,
|
||||
};
|
||||
|
||||
const models: Model[] = [];
|
||||
const searchModels = await operator.handleTeamSearchHistory({teamSearchHistories: [newSearch], prepareRecordsOnly: true});
|
||||
const searchModel = searchModels[0];
|
||||
|
||||
models.push(searchModel);
|
||||
|
||||
// determine if need to delete the oldest entry
|
||||
if (searchModel._raw._changed !== 'created_at') {
|
||||
const teamSearchHistory = await queryTeamSearchHistoryByTeamId(database, teamId).fetch();
|
||||
if (teamSearchHistory.length > MAX_TEAM_SEARCHES) {
|
||||
const lastSearches = teamSearchHistory.slice(MAX_TEAM_SEARCHES);
|
||||
for (const lastSearch of lastSearches) {
|
||||
models.push(lastSearch.prepareDestroyPermanently());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
return {searchModel};
|
||||
} catch (error) {
|
||||
logError('Failed addSearchToTeamSearchHistory', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeSearchFromTeamSearchHistory(serverUrl: string, id: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const teamSearch = await getTeamSearchHistoryById(database, id);
|
||||
if (teamSearch) {
|
||||
await database.write(async () => {
|
||||
await teamSearch.destroyPermanently();
|
||||
});
|
||||
}
|
||||
return {teamSearch};
|
||||
} catch (error) {
|
||||
logError('Failed removeSearchFromTeamSearchHistory', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,97 +8,70 @@ 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 {addChannelToTeamHistory, addTeamToTeamHistory} from '@queries/servers/team';
|
||||
import {getCurrentTeamId, getCurrentUserId, setCurrentChannelId} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled, getThreadById, prepareThreadsFromReceivedPosts, queryThreadsInTeam} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {dismissAllModalsAndPopToRoot, goToScreen} from '@screens/navigation';
|
||||
import {goToScreen} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logError} from '@utils/log';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
export const switchToGlobalThreads = async (serverUrl: string, teamId?: string, prepareRecordsOnly = false) => {
|
||||
export const switchToGlobalThreads = async (serverUrl: string, prepareRecordsOnly = false) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const models: Model[] = [];
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const models: Model[] = [];
|
||||
|
||||
let teamIdToUse = teamId;
|
||||
if (!teamId) {
|
||||
teamIdToUse = await getCurrentTeamId(database);
|
||||
}
|
||||
|
||||
if (!teamIdToUse) {
|
||||
throw new Error('no team to switch to');
|
||||
}
|
||||
|
||||
await setCurrentTeamAndChannelId(operator, teamIdToUse, '');
|
||||
const history = await addChannelToTeamHistory(operator, teamIdToUse, Screens.GLOBAL_THREADS, true);
|
||||
await setCurrentChannelId(operator, '');
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const history = await addChannelToTeamHistory(operator, currentTeamId, Screens.GLOBAL_THREADS, true);
|
||||
models.push(...history);
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
if (isTabletDevice) {
|
||||
DeviceEventEmitter.emit(Navigation.NAVIGATION_HOME, Screens.GLOBAL_THREADS);
|
||||
} else {
|
||||
goToScreen(Screens.GLOBAL_THREADS, '', {}, {topBar: {visible: false}});
|
||||
}
|
||||
|
||||
return {models};
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Failed switchToGlobalThreads', error);
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {models};
|
||||
};
|
||||
|
||||
export const switchToThread = async (serverUrl: string, rootId: string, isFromNotification = false) => {
|
||||
export const switchToThread = async (serverUrl: string, rootId: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const user = await getCurrentUser(database);
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
return {error: 'User not found'};
|
||||
}
|
||||
|
||||
const post = await getPostById(database, rootId);
|
||||
if (!post) {
|
||||
throw new Error('Post not found');
|
||||
return {error: 'Post not found'};
|
||||
}
|
||||
const channel = await getChannelById(database, post.channelId);
|
||||
if (!channel) {
|
||||
throw new Error('Channel not found');
|
||||
return {error: 'Channel not found'};
|
||||
}
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const isTabletDevice = await isTablet();
|
||||
const teamId = channel.teamId || currentTeamId;
|
||||
|
||||
let switchingTeams = false;
|
||||
if (currentTeamId === teamId) {
|
||||
const models = await prepareCommonSystemValues(operator, {
|
||||
currentChannelId: channel.id,
|
||||
});
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
} else {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
switchingTeams = true;
|
||||
modelPromises.push(addTeamToTeamHistory(operator, teamId, true));
|
||||
const commonValues: PrepareCommonSystemValuesArgs = {
|
||||
currentChannelId: channel.id,
|
||||
currentTeamId: teamId,
|
||||
};
|
||||
modelPromises.push(prepareCommonSystemValues(operator, commonValues));
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
const theme = EphemeralStore.theme;
|
||||
if (!theme) {
|
||||
return {error: 'Theme not found'};
|
||||
}
|
||||
|
||||
// Modal right buttons
|
||||
@@ -124,46 +97,32 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
|
||||
const translations = getTranslations(user.locale);
|
||||
|
||||
// Get title translation or default title message
|
||||
const title = translations[t('thread.header.thread')] || 'Thread';
|
||||
let title = translations[t('thread.header.thread')] || 'Thread';
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
title = translations[t('thread.header.thread_dm')] || 'Direct Message Thread';
|
||||
}
|
||||
|
||||
let subtitle = '';
|
||||
if (channel?.type === General.DM_CHANNEL) {
|
||||
subtitle = channel.displayName;
|
||||
} else {
|
||||
if (channel?.type !== General.DM_CHANNEL) {
|
||||
// Get translation or default message
|
||||
subtitle = translations[t('thread.header.thread_in')] || 'in {channelName}';
|
||||
subtitle = subtitle.replace('{channelName}', channel.displayName);
|
||||
}
|
||||
|
||||
EphemeralStore.setCurrentThreadId(rootId);
|
||||
|
||||
if (isFromNotification) {
|
||||
await dismissAllModalsAndPopToRoot();
|
||||
await NavigationStore.waitUntilScreenIsTop(Screens.HOME);
|
||||
if (switchingTeams && isTabletDevice) {
|
||||
DeviceEventEmitter.emit(Navigation.NAVIGATION_HOME, Screens.GLOBAL_THREADS);
|
||||
}
|
||||
}
|
||||
goToScreen(Screens.THREAD, '', {rootId}, {
|
||||
topBar: {
|
||||
title: {
|
||||
text: title,
|
||||
},
|
||||
subtitle: {
|
||||
color: changeOpacity(EphemeralStore.theme!.sidebarHeaderTextColor, 0.72),
|
||||
color: changeOpacity(theme.sidebarHeaderTextColor, 0.72),
|
||||
text: subtitle,
|
||||
},
|
||||
noBorder: true,
|
||||
scrollEdgeAppearance: {
|
||||
noBorder: true,
|
||||
},
|
||||
rightButtons,
|
||||
},
|
||||
});
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
logError('Failed switchToThread', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
@@ -172,180 +131,145 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
|
||||
// 1. If a reply, then update the reply_count, add user as the participant
|
||||
// 2. Else add the post as a thread
|
||||
export async function createThreadFromNewPost(serverUrl: string, post: Post, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const models: Model[] = [];
|
||||
if (post.root_id) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Add user as a participant to the thread
|
||||
const threadParticipantModels = await operator.handleThreadParticipants({
|
||||
threadsParticipants: [{
|
||||
thread_id: post.root_id,
|
||||
participants: [{
|
||||
thread_id: post.root_id,
|
||||
id: post.user_id,
|
||||
}],
|
||||
}],
|
||||
prepareRecordsOnly: true,
|
||||
skipSync: true,
|
||||
});
|
||||
models.push(...threadParticipantModels);
|
||||
} else { // If the post is a root post, then we need to add it to the thread table
|
||||
const threadModels = await prepareThreadsFromReceivedPosts(operator, [post], false);
|
||||
models.push(...threadModels);
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
} catch (error) {
|
||||
logError('Failed createThreadFromNewPost', error);
|
||||
return {error};
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const models: Model[] = [];
|
||||
if (post.root_id) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Add user as a participant to the thread
|
||||
const threadParticipantModels = await operator.handleThreadParticipants({
|
||||
threadsParticipants: [{
|
||||
thread_id: post.root_id,
|
||||
participants: [{
|
||||
thread_id: post.root_id,
|
||||
id: post.user_id,
|
||||
}],
|
||||
}],
|
||||
prepareRecordsOnly: true,
|
||||
skipSync: true,
|
||||
});
|
||||
models.push(...threadParticipantModels);
|
||||
} else { // If the post is a root post, then we need to add it to the thread table
|
||||
const threadModels = await prepareThreadsFromReceivedPosts(operator, [post]);
|
||||
models.push(...threadModels);
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {models};
|
||||
}
|
||||
|
||||
// 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) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentUserId = await getCurrentUserId(database);
|
||||
|
||||
const posts: Post[] = [];
|
||||
const users: UserProfile[] = [];
|
||||
const threadsToHandle: ThreadWithLastFetchedAt[] = [];
|
||||
|
||||
// Extract posts & users from the received threads
|
||||
for (let i = 0; i < threads.length; i++) {
|
||||
const {participants, post} = threads[i];
|
||||
posts.push(post);
|
||||
participants.forEach((participant) => {
|
||||
if (currentUserId !== participant.id) {
|
||||
users.push(participant);
|
||||
}
|
||||
});
|
||||
threadsToHandle.push({...threads[i], lastFetchedAt: post.create_at});
|
||||
}
|
||||
|
||||
const postModels = await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
|
||||
order: [],
|
||||
posts,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
const threadModels = await operator.handleThreads({
|
||||
threads: threadsToHandle,
|
||||
teamId,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
const models = [...postModels, ...threadModels];
|
||||
|
||||
if (users.length) {
|
||||
const userModels = await operator.handleUsers({
|
||||
users,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
models.push(...userModels);
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
return {models};
|
||||
} catch (error) {
|
||||
logError('Failed processReceivedThreads', error);
|
||||
return {error};
|
||||
export async function processReceivedThreads(serverUrl: string, threads: Thread[], teamId: string, loadedInGlobalThreads = false, prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const currentUserId = await getCurrentUserId(database);
|
||||
|
||||
const posts: Post[] = [];
|
||||
const users: UserProfile[] = [];
|
||||
|
||||
// Extract posts & users from the received threads
|
||||
for (let i = 0; i < threads.length; i++) {
|
||||
const {participants, post} = threads[i];
|
||||
posts.push(post);
|
||||
participants.forEach((participant) => {
|
||||
if (currentUserId !== participant.id) {
|
||||
users.push(participant);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const postModels = await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
|
||||
order: [],
|
||||
posts,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
const threadModels = await operator.handleThreads({
|
||||
threads,
|
||||
teamId,
|
||||
prepareRecordsOnly: true,
|
||||
loadedInGlobalThreads,
|
||||
});
|
||||
|
||||
const models = [...postModels, ...threadModels];
|
||||
|
||||
if (users.length) {
|
||||
const userModels = await operator.handleUsers({
|
||||
users,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
models.push(...userModels);
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
return {models};
|
||||
}
|
||||
|
||||
export async function markTeamThreadsAsRead(serverUrl: string, teamId: string, prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {database} = operator;
|
||||
const threads = await queryThreadsInTeam(database, teamId, true, true, true).fetch();
|
||||
const models = threads.map((thread) => thread.prepareUpdate((record) => {
|
||||
record.unreadMentions = 0;
|
||||
record.unreadReplies = 0;
|
||||
record.lastViewedAt = Date.now();
|
||||
record.viewedAt = Date.now();
|
||||
}));
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
return {models};
|
||||
} catch (error) {
|
||||
logError('Failed markTeamThreadsAsRead', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function markThreadAsViewed(serverUrl: string, threadId: string, prepareRecordsOnly = false) {
|
||||
export async function updateThread(serverUrl: string, threadId: string, updatedThread: Partial<Thread>, prepareRecordsOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {database} = operator;
|
||||
const thread = await getThreadById(database, threadId);
|
||||
if (!thread) {
|
||||
return {error: 'Thread not found'};
|
||||
if (thread) {
|
||||
const model = thread.prepareUpdate((record) => {
|
||||
record.isFollowing = updatedThread.is_following ?? record.isFollowing;
|
||||
record.replyCount = updatedThread.reply_count ?? record.replyCount;
|
||||
|
||||
record.lastViewedAt = updatedThread.last_viewed_at ?? record.lastViewedAt;
|
||||
record.unreadMentions = updatedThread.unread_mentions ?? record.unreadMentions;
|
||||
record.unreadReplies = updatedThread.unread_replies ?? record.unreadReplies;
|
||||
});
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([model]);
|
||||
}
|
||||
return {model};
|
||||
}
|
||||
|
||||
thread.prepareUpdate((th) => {
|
||||
th.viewedAt = thread.lastViewedAt;
|
||||
th.lastViewedAt = Date.now();
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([thread]);
|
||||
}
|
||||
|
||||
return {model: thread};
|
||||
return {error: 'Thread not found'};
|
||||
} catch (error) {
|
||||
logError('Failed markThreadAsViewed', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateThread(serverUrl: string, threadId: string, updatedThread: Partial<ThreadWithViewedAt>, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const thread = await getThreadById(database, threadId);
|
||||
if (!thread) {
|
||||
throw new Error('Thread not found');
|
||||
}
|
||||
|
||||
const model = thread.prepareUpdate((record) => {
|
||||
record.isFollowing = updatedThread.is_following ?? record.isFollowing;
|
||||
record.replyCount = updatedThread.reply_count ?? record.replyCount;
|
||||
record.lastViewedAt = updatedThread.last_viewed_at ?? record.lastViewedAt;
|
||||
record.viewedAt = updatedThread.viewed_at ?? record.viewedAt;
|
||||
record.unreadMentions = updatedThread.unread_mentions ?? record.unreadMentions;
|
||||
record.unreadReplies = updatedThread.unread_replies ?? record.unreadReplies;
|
||||
});
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([model]);
|
||||
}
|
||||
return {model};
|
||||
} catch (error) {
|
||||
logError('Failed updateThread', error);
|
||||
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};
|
||||
}
|
||||
}
|
||||
|
||||
29
app/actions/local/timezone.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getTimeZone} from 'react-native-localize';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export const isTimezoneEnabled = (config: Partial<ClientConfig>) => {
|
||||
return config?.ExperimentalTimezone === 'true';
|
||||
};
|
||||
|
||||
export function getDeviceTimezone() {
|
||||
return getTimeZone();
|
||||
}
|
||||
|
||||
export const getUserTimezone = (currentUser: UserModel) => {
|
||||
if (currentUser?.timezone) {
|
||||
return {
|
||||
...currentUser?.timezone,
|
||||
useAutomaticTimezone: currentUser?.timezone?.useAutomaticTimezone === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
useAutomaticTimezone: true,
|
||||
automaticTimezone: '',
|
||||
manualTimezone: '',
|
||||
};
|
||||
};
|
||||
@@ -5,8 +5,7 @@ import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import General from '@constants/general';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getRecentCustomStatuses} from '@queries/servers/system';
|
||||
import {getCurrentUser, getUserById} from '@queries/servers/user';
|
||||
import {logError} from '@utils/log';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
|
||||
import {addRecentReaction} from './reactions';
|
||||
|
||||
@@ -14,92 +13,107 @@ import type Model from '@nozbe/watermelondb/Model';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export async function setCurrentUserStatusOffline(serverUrl: string) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const user = await getCurrentUser(database);
|
||||
if (!user) {
|
||||
throw new Error(`No current user for ${serverUrl}`);
|
||||
}
|
||||
|
||||
user.prepareStatus(General.OFFLINE);
|
||||
await operator.batchRecords([user]);
|
||||
return null;
|
||||
} catch (error) {
|
||||
logError('Failed setCurrentUserStatusOffline', error);
|
||||
return {error};
|
||||
const serverDatabase = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!serverDatabase) {
|
||||
return {error: `No database present for ${serverUrl}`};
|
||||
}
|
||||
|
||||
const {database, operator} = serverDatabase;
|
||||
|
||||
const user = await getCurrentUser(database);
|
||||
if (!user) {
|
||||
return {error: `No current user for ${serverUrl}`};
|
||||
}
|
||||
|
||||
user.prepareStatus(General.OFFLINE);
|
||||
|
||||
try {
|
||||
await operator.batchRecords([user]);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANGES FOR SET CURRENT USER STATUS OFFLINE');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function updateLocalCustomStatus(serverUrl: string, user: UserModel, customStatus?: UserCustomStatus) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const models: Model[] = [];
|
||||
const currentProps = {...user.props, customStatus: customStatus || {}};
|
||||
const userModel = user.prepareUpdate((u: UserModel) => {
|
||||
u.props = currentProps;
|
||||
});
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
models.push(userModel);
|
||||
if (customStatus) {
|
||||
const recent = await updateRecentCustomStatuses(serverUrl, customStatus, true);
|
||||
if (Array.isArray(recent)) {
|
||||
models.push(...recent);
|
||||
}
|
||||
const models: Model[] = [];
|
||||
const currentProps = {...user.props, customStatus: customStatus || {}};
|
||||
const userModel = user.prepareUpdate((u: UserModel) => {
|
||||
u.props = currentProps;
|
||||
});
|
||||
|
||||
if (customStatus.emoji) {
|
||||
const recentEmojis = await addRecentReaction(serverUrl, [customStatus.emoji], true);
|
||||
if (Array.isArray(recentEmojis)) {
|
||||
models.push(...recentEmojis);
|
||||
}
|
||||
}
|
||||
models.push(userModel);
|
||||
if (customStatus) {
|
||||
const recent = await updateRecentCustomStatuses(serverUrl, customStatus, true);
|
||||
if (Array.isArray(recent)) {
|
||||
models.push(...recent);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
logError('Failed updateLocalCustomStatus', error);
|
||||
return {error};
|
||||
if (customStatus.emoji) {
|
||||
const recentEmojis = await addRecentReaction(serverUrl, [customStatus.emoji], true);
|
||||
if (Array.isArray(recentEmojis)) {
|
||||
models.push(...recentEmojis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await operator.batchRecords(models);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANGES FOR UPDATING CUSTOM STATUS');
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export const updateRecentCustomStatuses = async (serverUrl: string, customStatus: UserCustomStatus, prepareRecordsOnly = false, remove = false) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const recentStatuses = await getRecentCustomStatuses(database);
|
||||
const index = recentStatuses.findIndex((cs) => (
|
||||
cs.emoji === customStatus.emoji &&
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const recentStatuses = await getRecentCustomStatuses(operator.database);
|
||||
const index = recentStatuses.findIndex((cs) => (
|
||||
cs.emoji === customStatus.emoji &&
|
||||
cs.text === customStatus.text &&
|
||||
cs.duration === customStatus.duration
|
||||
));
|
||||
));
|
||||
|
||||
if (index !== -1) {
|
||||
recentStatuses.splice(index, 1);
|
||||
}
|
||||
|
||||
if (!remove) {
|
||||
recentStatuses.unshift(customStatus);
|
||||
}
|
||||
|
||||
return operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.RECENT_CUSTOM_STATUS,
|
||||
value: JSON.stringify(recentStatuses),
|
||||
}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('Failed updateRecentCustomStatuses', error);
|
||||
return {error};
|
||||
if (index !== -1) {
|
||||
recentStatuses.splice(index, 1);
|
||||
}
|
||||
|
||||
if (!remove) {
|
||||
recentStatuses.unshift(customStatus);
|
||||
}
|
||||
|
||||
return operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.RECENT_CUSTOM_STATUS,
|
||||
value: JSON.stringify(recentStatuses),
|
||||
}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateLocalUser = async (
|
||||
serverUrl: string,
|
||||
userDetails: Partial<UserProfile> & { status?: string},
|
||||
) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const user = await getCurrentUser(database);
|
||||
if (user) {
|
||||
await database.write(async () => {
|
||||
@@ -121,29 +135,9 @@ export const updateLocalUser = async (
|
||||
});
|
||||
});
|
||||
}
|
||||
return {user};
|
||||
} catch (error) {
|
||||
logError('Failed updateLocalUser', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const storeProfile = async (serverUrl: string, profile: UserProfile) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const user = await getUserById(database, profile.id);
|
||||
if (user) {
|
||||
return {user};
|
||||
}
|
||||
|
||||
const records = await operator.handleUsers({
|
||||
users: [profile],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
return {user: records[0]};
|
||||
} catch (error) {
|
||||
logError('Failed storeProfile', error);
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntlShape} from 'react-intl';
|
||||
|
||||
import {sendEphemeralPost} from '@actions/local/post';
|
||||
import ClientError from '@client/rest/error';
|
||||
import {AppCallResponseTypes} from '@constants/apps';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {cleanForm, createCallRequest, makeCallErrorResponse} from '@utils/apps';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
export async function handleBindingClick<Res=unknown>(serverUrl: string, binding: AppBinding, context: AppContext, intl: IntlShape): Promise<{data?: AppCallResponse<Res>; error?: AppCallResponse<Res>}> {
|
||||
// Fetch form
|
||||
@@ -74,7 +75,7 @@ export async function doAppSubmit<Res=unknown>(serverUrl: string, inCall: AppCal
|
||||
track_as_submit: true,
|
||||
},
|
||||
};
|
||||
const res = await client.executeAppCall<Res>(call, true);
|
||||
const res = await client.executeAppCall(call, true) as AppCallResponse<Res>;
|
||||
const responseType = res.type || AppCallResponseTypes.OK;
|
||||
|
||||
switch (responseType) {
|
||||
@@ -133,7 +134,7 @@ export async function doAppFetchForm<Res=unknown>(serverUrl: string, call: AppCa
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await client.executeAppCall<Res>(call, false);
|
||||
const res = await client.executeAppCall(call, false) as AppCallResponse<Res>;
|
||||
const responseType = res.type || AppCallResponseTypes.OK;
|
||||
|
||||
switch (responseType) {
|
||||
@@ -175,7 +176,7 @@ export async function doAppLookup<Res=unknown>(serverUrl: string, call: AppCallR
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await client.executeAppCall<Res>(call, false);
|
||||
const res = await client.executeAppCall(call, false) as AppCallResponse<Res>;
|
||||
const responseType = res.type || AppCallResponseTypes.OK;
|
||||
|
||||
switch (responseType) {
|
||||
@@ -206,7 +207,7 @@ export function postEphemeralCallResponseForPost(serverUrl: string, response: Ap
|
||||
serverUrl,
|
||||
message,
|
||||
post.channelId,
|
||||
post.rootId || post.id,
|
||||
post.rootId,
|
||||
response.app_metadata?.bot_user_id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {storeCategories} from '@actions/local/category';
|
||||
import {General} from '@constants';
|
||||
import {CHANNELS_CATEGORY, DMS_CATEGORY, FAVORITES_CATEGORY} from '@constants/categories';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelCategory, queryCategoriesByTeamIds} from '@queries/servers/categories';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import {showFavoriteChannelSnackbar} from '@utils/snack_bar';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
@@ -41,75 +34,3 @@ export const fetchCategories = async (serverUrl: string, teamId: string, prune =
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleFavoriteChannel = async (serverUrl: string, channelId: string, showSnackBar = false) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const {database} = operator;
|
||||
const channel = await getChannelById(database, channelId);
|
||||
if (!channel) {
|
||||
return {error: 'channel not found'};
|
||||
}
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const teamId = channel?.teamId || currentTeamId;
|
||||
const currentCategory = await getChannelCategory(database, teamId, channelId);
|
||||
|
||||
if (!currentCategory) {
|
||||
return {error: 'channel does not belong to a category'};
|
||||
}
|
||||
|
||||
const categories = await queryCategoriesByTeamIds(database, [teamId]).fetch();
|
||||
const isFavorited = currentCategory.type === FAVORITES_CATEGORY;
|
||||
let targetWithChannels: CategoryWithChannels;
|
||||
let favoriteWithChannels: CategoryWithChannels;
|
||||
|
||||
if (isFavorited) {
|
||||
const categoryType = (channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL) ? DMS_CATEGORY : CHANNELS_CATEGORY;
|
||||
const targetCategory = categories.find((c) => c.type === categoryType);
|
||||
if (!targetCategory) {
|
||||
return {error: 'target category not found'};
|
||||
}
|
||||
targetWithChannels = await targetCategory.toCategoryWithChannels();
|
||||
targetWithChannels.channel_ids.unshift(channelId);
|
||||
|
||||
favoriteWithChannels = await currentCategory.toCategoryWithChannels();
|
||||
const channelIndex = favoriteWithChannels.channel_ids.indexOf(channelId);
|
||||
favoriteWithChannels.channel_ids.splice(channelIndex, 1);
|
||||
} else {
|
||||
const favoritesCategory = categories.find((c) => c.type === FAVORITES_CATEGORY);
|
||||
if (!favoritesCategory) {
|
||||
return {error: 'No favorites category'};
|
||||
}
|
||||
favoriteWithChannels = await favoritesCategory.toCategoryWithChannels();
|
||||
favoriteWithChannels.channel_ids.unshift(channelId);
|
||||
|
||||
targetWithChannels = await currentCategory.toCategoryWithChannels();
|
||||
const channelIndex = targetWithChannels.channel_ids.indexOf(channelId);
|
||||
targetWithChannels.channel_ids.splice(channelIndex, 1);
|
||||
}
|
||||
|
||||
await client.updateChannelCategories('me', teamId, [targetWithChannels, favoriteWithChannels]);
|
||||
|
||||
if (showSnackBar) {
|
||||
const onUndo = () => toggleFavoriteChannel(serverUrl, channelId, false);
|
||||
showFavoriteChannelSnackbar(!isFavorited, onUndo);
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,45 +2,38 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
import {IntlShape} from 'react-intl';
|
||||
|
||||
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
|
||||
import {removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {storeCategories} from '@actions/local/category';
|
||||
import {addChannelToDefaultCategory, 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 {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 {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo} 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, getCurrentTeamId, getCurrentUserId} 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 {isTablet} from '@utils/helpers';
|
||||
import {logDebug, 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';
|
||||
import {fetchPostsForChannel} from './post';
|
||||
import {setDirectChannelVisible} from './preference';
|
||||
import {fetchRolesIfNeeded} from './role';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from './team';
|
||||
import {fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
|
||||
import {addUserToTeam, fetchTeamByName, removeUserFromTeam} from './team';
|
||||
import {fetchProfilesPerChannels, fetchUsersByIds} from './user';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
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[];
|
||||
@@ -163,7 +156,6 @@ export async function createChannel(serverUrl: string, displayName: string, purp
|
||||
return {channel: channelData};
|
||||
} catch (error) {
|
||||
EphemeralStore.creatingChannel = false;
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
@@ -205,65 +197,10 @@ export async function patchChannel(serverUrl: string, channelPatch: Partial<Chan
|
||||
}
|
||||
return {channel: channelData};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function leaveChannel(serverUrl: string, channelId: 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 {database} = operator;
|
||||
const isTabletDevice = await isTablet();
|
||||
const user = await getCurrentUser(database);
|
||||
const models: Model[] = [];
|
||||
|
||||
if (!user) {
|
||||
return {error: 'current user not found'};
|
||||
}
|
||||
|
||||
EphemeralStore.addLeavingChannel(channelId);
|
||||
await client.removeFromChannel(user.id, channelId);
|
||||
|
||||
if (user.isGuest) {
|
||||
const {models: updateVisibleModels} = await updateUsersNoLongerVisible(serverUrl, true);
|
||||
if (updateVisibleModels) {
|
||||
models.push(...updateVisibleModels);
|
||||
}
|
||||
}
|
||||
|
||||
const {models: removeUserModels} = await removeCurrentUserFromChannel(serverUrl, channelId, true);
|
||||
if (removeUserModels) {
|
||||
models.push(...removeUserModels);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
|
||||
if (isTabletDevice) {
|
||||
switchToLastChannel(serverUrl);
|
||||
} else {
|
||||
setCurrentChannelId(operator, '');
|
||||
}
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
} finally {
|
||||
EphemeralStore.removeLeavingChannel(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchChannelCreator(serverUrl: string, channelId: string, fetchOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
@@ -346,7 +283,7 @@ export async function fetchChannelStats(serverUrl: string, channelId: string, fe
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string, includeDeleted = true, since = 0, fetchOnly = false, excludeDirect = false, isCRTEnabled?: boolean): Promise<MyChannelsRequest> {
|
||||
export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string, includeDeleted = true, since = 0, fetchOnly = false, excludeDirect = false): Promise<MyChannelsRequest> {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -360,9 +297,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),
|
||||
@@ -385,20 +319,16 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string,
|
||||
}, []);
|
||||
|
||||
if (!fetchOnly) {
|
||||
const {models: chModels} = await storeMyChannelsForTeam(serverUrl, teamId, channels, memberships, true, isCRTEnabled);
|
||||
const {models: chModels} = await storeMyChannelsForTeam(serverUrl, teamId, channels, memberships, true);
|
||||
const {models: catModels} = await storeCategories(serverUrl, categories, true, true); // Re-sync
|
||||
const models = (chModels || []).concat(catModels || []);
|
||||
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};
|
||||
}
|
||||
@@ -437,114 +367,68 @@ export async function fetchMyChannel(serverUrl: string, teamId: string, channelI
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMissingDirectChannelsInfo(serverUrl: string, directChannels: Channel[], locale?: string, teammateDisplayNameSetting?: string, currentUserId?: string, fetchOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
export async function fetchMissingSidebarInfo(serverUrl: string, directChannels: Channel[], locale?: string, teammateDisplayNameSetting?: string, currentUserId?: string, fetchOnly = false) {
|
||||
const channelIds = directChannels.sort((a, b) => b.last_post_at - a.last_post_at).map((dc) => dc.id);
|
||||
const result = await fetchProfilesPerChannels(serverUrl, channelIds, currentUserId, false);
|
||||
if (result.error) {
|
||||
return {error: result.error};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const displayNameByChannel: Record<string, string> = {};
|
||||
const users: UserProfile[] = [];
|
||||
const updatedChannels = new Set<Channel>();
|
||||
|
||||
const dms: Channel[] = [];
|
||||
const dmIds: string[] = [];
|
||||
const dmWithoutDisplayName = new Set<string>();
|
||||
const gms: Channel[] = [];
|
||||
for (const c of directChannels) {
|
||||
if (c.type === General.DM_CHANNEL) {
|
||||
dms.push(c);
|
||||
dmIds.push(c.id);
|
||||
if (!c.display_name) {
|
||||
dmWithoutDisplayName.add(c.id);
|
||||
if (result.data) {
|
||||
result.data.forEach((data) => {
|
||||
if (data.users?.length) {
|
||||
users.push(...data.users);
|
||||
if (data.users.length > 1) {
|
||||
displayNameByChannel[data.channelId] = displayGroupMessageName(data.users, locale, teammateDisplayNameSetting, currentUserId);
|
||||
} else {
|
||||
displayNameByChannel[data.channelId] = displayUsername(data.users[0], locale, teammateDisplayNameSetting, false);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
gms.push(c);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const currentUser = await getCurrentUser(database);
|
||||
|
||||
// let's filter those channels that we already have the users
|
||||
const membersCount = await getMembersCountByChannelsId(database, dmIds);
|
||||
const profileChannelsToFetch = dmIds.filter((id) => membersCount[id] <= 1 || dmWithoutDisplayName.has(id));
|
||||
const results = await Promise.all([
|
||||
profileChannelsToFetch.length ? fetchProfilesPerChannels(serverUrl, profileChannelsToFetch, currentUserId, false) : Promise.resolve({data: undefined}),
|
||||
fetchProfilesInGroupChannels(serverUrl, gms.map((c) => c.id), false),
|
||||
]);
|
||||
|
||||
const profileRequests = results.flat();
|
||||
for (const result of profileRequests) {
|
||||
result.data?.forEach((data) => {
|
||||
if (data.users?.length) {
|
||||
users.push(...data.users);
|
||||
if (data.users.length > 1) {
|
||||
displayNameByChannel[data.channelId] = displayGroupMessageName(data.users, locale, teammateDisplayNameSetting, currentUserId);
|
||||
} else {
|
||||
displayNameByChannel[data.channelId] = displayUsername(data.users[0], locale, teammateDisplayNameSetting, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
directChannels.forEach((c) => {
|
||||
const displayName = displayNameByChannel[c.id];
|
||||
if (displayName) {
|
||||
c.display_name = displayName;
|
||||
c.fake = true;
|
||||
updatedChannels.add(c);
|
||||
}
|
||||
});
|
||||
|
||||
if (currentUserId) {
|
||||
const ownDirectChannel = dms.find((dm) => dm.name === getDirectChannelName(currentUserId, currentUserId));
|
||||
if (ownDirectChannel) {
|
||||
ownDirectChannel.display_name = displayUsername(currentUser, locale, teammateDisplayNameSetting, false);
|
||||
ownDirectChannel.fake = true;
|
||||
updatedChannels.add(ownDirectChannel);
|
||||
}
|
||||
}
|
||||
directChannels.forEach((c) => {
|
||||
const displayName = displayNameByChannel[c.id];
|
||||
if (displayName) {
|
||||
c.display_name = displayName;
|
||||
c.fake = true;
|
||||
}
|
||||
});
|
||||
|
||||
const updatedChannelsArray = Array.from(updatedChannels);
|
||||
if (!fetchOnly) {
|
||||
if (currentUserId) {
|
||||
const ownDirectChannel = directChannels.find((dm) => dm.name === getDirectChannelName(currentUserId, currentUserId));
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (ownDirectChannel && database) {
|
||||
const currentUser = await getCurrentUser(database);
|
||||
ownDirectChannel.display_name = displayUsername(currentUser, locale, teammateDisplayNameSetting, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
if (updatedChannelsArray.length) {
|
||||
modelPromises.push(operator.handleChannel({channels: updatedChannelsArray, prepareRecordsOnly: true}));
|
||||
if (users.length) {
|
||||
modelPromises.push(operator.handleUsers({users, prepareRecordsOnly: true}));
|
||||
modelPromises.push(operator.handleChannel({channels: directChannels, prepareRecordsOnly: true}));
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
|
||||
return {directChannels: updatedChannelsArray, users};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchDirectChannelsInfo(serverUrl: string, directChannels: ChannelModel[]) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
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 currentUser = await getCurrentUser(database);
|
||||
const channels = directChannels.map((d) => d.toApi());
|
||||
return fetchMissingDirectChannelsInfo(serverUrl, channels, currentUser?.locale, teammateDisplayNameSetting, currentUser?.id);
|
||||
return {directChannels, users};
|
||||
}
|
||||
|
||||
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 +437,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 {
|
||||
@@ -573,7 +455,7 @@ export async function joinChannel(serverUrl: string, teamId: string, channelId?:
|
||||
}
|
||||
} catch (error) {
|
||||
if (channelId || channel?.id) {
|
||||
EphemeralStore.removeJoiningChannel(channelId || channel!.id);
|
||||
EphemeralStore.removeJoiningChanel(channelId || channel!.id);
|
||||
}
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
@@ -595,21 +477,21 @@ export async function joinChannel(serverUrl: string, teamId: string, channelId?:
|
||||
try {
|
||||
await operator.batchRecords(flattenedModels);
|
||||
} catch {
|
||||
logError('FAILED TO BATCH CHANNELS');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANNELS');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (channelId || channel?.id) {
|
||||
EphemeralStore.removeJoiningChannel(channelId || channel!.id);
|
||||
EphemeralStore.removeJoiningChanel(channelId || channel!.id);
|
||||
}
|
||||
return {error};
|
||||
}
|
||||
|
||||
if (channelId || channel?.id) {
|
||||
loadCallForChannel(serverUrl, channelId || channel!.id);
|
||||
EphemeralStore.removeJoiningChannel(channelId || channel!.id);
|
||||
EphemeralStore.removeJoiningChanel(channelId || channel!.id);
|
||||
}
|
||||
return {channel, member};
|
||||
}
|
||||
@@ -626,7 +508,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};
|
||||
}
|
||||
@@ -644,105 +527,159 @@ 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;
|
||||
try {
|
||||
const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
database = result.database;
|
||||
} catch (e) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
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 isArchived = false;
|
||||
const chReq = await fetchChannelByName(serverUrl, team.id, channelName);
|
||||
if (chReq.error) {
|
||||
errorHandler(intl);
|
||||
return {error: chReq.error};
|
||||
}
|
||||
const channel = chReq.channel;
|
||||
if (!channel) {
|
||||
errorHandler(intl);
|
||||
return {error: 'Could not fetch channel'};
|
||||
}
|
||||
|
||||
isArchived = channel.delete_at > 0;
|
||||
if (isArchived && system.config.ExperimentalViewArchivedChannels !== 'true') {
|
||||
errorHandler(intl);
|
||||
return {error: 'Channel is archived'};
|
||||
}
|
||||
|
||||
myChannel = await getMyChannel(database, channel.id);
|
||||
|
||||
if (!myChannel) {
|
||||
if (channel.type === General.PRIVATE_CHANNEL) {
|
||||
const displayName = channel.display_name;
|
||||
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'};
|
||||
}
|
||||
}
|
||||
console.log('joining channel', displayName, channel.id); //eslint-disable-line
|
||||
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[]>> = [];
|
||||
const {operator} = DatabaseManager.serverDatabases[serverUrl];
|
||||
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}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!(myChannel 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, channelId, teamId);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -762,13 +699,6 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
|
||||
if (!currentUser) {
|
||||
return {error: 'Cannot get the current user'};
|
||||
}
|
||||
|
||||
const channelName = getDirectChannelName(currentUser.id, userId);
|
||||
const channel = await getChannelByName(database, '', channelName);
|
||||
if (channel) {
|
||||
return {data: channel.toApi()};
|
||||
}
|
||||
|
||||
EphemeralStore.creatingDMorGMTeammates = [userId];
|
||||
const created = await client.createDirectChannel([userId, currentUser.id]);
|
||||
const profiles: UserProfile[] = [];
|
||||
@@ -777,10 +707,9 @@ 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 {directChannels, users} = await fetchMissingDirectChannelsInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
|
||||
const system = await getCommonSystemValues(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], system.config, system.license);
|
||||
const {directChannels, users} = await fetchMissingSidebarInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
|
||||
created.display_name = directChannels?.[0].display_name || created.display_name;
|
||||
if (users?.length) {
|
||||
profiles.push(...users);
|
||||
@@ -857,7 +786,7 @@ export async function makeDirectChannel(serverUrl: string, userId: string, displ
|
||||
try {
|
||||
const currentUserId = await getCurrentUserId(operator.database);
|
||||
const channelName = getDirectChannelName(userId, currentUserId);
|
||||
let channel: Channel|ChannelModel|undefined = await getChannelByName(operator.database, '', channelName);
|
||||
let channel: Channel|ChannelModel|undefined = await getChannelByName(operator.database, channelName);
|
||||
let result: {data?: Channel|ChannelModel; error?: any};
|
||||
if (channel) {
|
||||
result = {data: channel};
|
||||
@@ -875,7 +804,6 @@ export async function makeDirectChannel(serverUrl: string, userId: string, displ
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchArchivedChannels(serverUrl: string, teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE) {
|
||||
let client: Client;
|
||||
try {
|
||||
@@ -895,16 +823,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,11 +849,10 @@ 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 {directChannels, users} = await fetchMissingDirectChannelsInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
|
||||
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 fetchMissingSidebarInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
|
||||
|
||||
const member = {
|
||||
channel_id: created.id,
|
||||
@@ -965,7 +895,6 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSharedChannels(serverUrl: string, teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE) {
|
||||
let client: Client;
|
||||
try {
|
||||
@@ -1003,7 +932,6 @@ export async function makeGroupChannel(serverUrl: string, userIds: string[], sho
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getChannelMemberCountsByGroup(serverUrl: string, channelId: string, includeTimezones: boolean) {
|
||||
let client: Client;
|
||||
try {
|
||||
@@ -1037,64 +965,38 @@ export async function getChannelTimezones(serverUrl: string, channelId: string)
|
||||
}
|
||||
|
||||
export async function switchToChannelById(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false) {
|
||||
if (channelId === Screens.GLOBAL_THREADS) {
|
||||
return switchToGlobalThreads(serverUrl, teamId);
|
||||
}
|
||||
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
DeviceEventEmitter.emit(Events.CHANNEL_SWITCH, true);
|
||||
|
||||
fetchPostsForChannel(serverUrl, channelId);
|
||||
await switchToChannel(serverUrl, channelId, teamId, skipLastUnread);
|
||||
setDirectChannelVisible(serverUrl, channelId);
|
||||
markChannelAsRead(serverUrl, channelId);
|
||||
fetchChannelStats(serverUrl, channelId);
|
||||
fetchGroupsForChannelIfConstrained(serverUrl, channelId);
|
||||
|
||||
DeviceEventEmitter.emit(Events.CHANNEL_SWITCH, false);
|
||||
|
||||
if (await AppsManager.isAppsEnabled(serverUrl)) {
|
||||
AppsManager.fetchBindings(serverUrl, channelId);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export async function switchToPenultimateChannel(serverUrl: string, teamId?: string) {
|
||||
export async function switchToPenultimateChannel(serverUrl: string) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const teamIdToUse = teamId || await getCurrentTeamId(database);
|
||||
const channelId = await getNthLastChannelFromTeam(database, teamIdToUse, 1);
|
||||
const currentTeam = await getCurrentTeamId(database);
|
||||
const channelId = await getNthLastChannelFromTeam(database, currentTeam, 1);
|
||||
if (channelId === Screens.GLOBAL_THREADS) {
|
||||
return switchToGlobalThreads(serverUrl);
|
||||
}
|
||||
return switchToChannelById(serverUrl, channelId);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function switchToLastChannel(serverUrl: string, teamId?: string) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const teamIdToUse = teamId || await getCurrentTeamId(database);
|
||||
const channelId = await getNthLastChannelFromTeam(database, teamIdToUse);
|
||||
return switchToChannelById(serverUrl, channelId);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchChannels(serverUrl: string, term: string, teamId: string, isSearch = false) {
|
||||
export async function searchChannels(serverUrl: string, term: string) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -1108,8 +1010,8 @@ export async function searchChannels(serverUrl: string, term: string, teamId: st
|
||||
}
|
||||
|
||||
try {
|
||||
const autoCompleteFunc = isSearch ? client.autocompleteChannelsForSearch : client.autocompleteChannels;
|
||||
const channels = await autoCompleteFunc(teamId, term);
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const channels = await client.autocompleteChannels(currentTeamId, term);
|
||||
return {channels};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
@@ -1153,200 +1055,3 @@ export async function searchAllChannels(serverUrl: string, term: string, archive
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export const updateChannelNotifyProps = async (serverUrl: string, channelId: string, props: Partial<ChannelNotifyProps>) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = await getCurrentUserId(database);
|
||||
const notifyProps = {...props, channel_id: channelId, user_id: userId} as ChannelNotifyProps & {channel_id: string; user_id: string};
|
||||
|
||||
await client.updateChannelNotifyProps(notifyProps);
|
||||
|
||||
return {
|
||||
notifyProps,
|
||||
};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleMuteChannel = async (serverUrl: string, channelId: string, showSnackBar = false) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const channelSettings = await queryMyChannelSettingsByIds(database, [channelId]).fetch();
|
||||
const myChannelSetting = channelSettings?.[0];
|
||||
const mark_unread = myChannelSetting.notifyProps?.mark_unread === 'mention' ? 'all' : 'mention';
|
||||
|
||||
const notifyProps: Partial<ChannelNotifyProps> = {...myChannelSetting.notifyProps, mark_unread};
|
||||
await updateChannelNotifyProps(serverUrl, channelId, notifyProps);
|
||||
|
||||
await database.write(async () => {
|
||||
await myChannelSetting.update((c) => {
|
||||
c.notifyProps = notifyProps;
|
||||
});
|
||||
});
|
||||
|
||||
if (showSnackBar) {
|
||||
const onUndo = () => toggleMuteChannel(serverUrl, channelId, false);
|
||||
showMuteChannelSnackbar(mark_unread === 'mention', onUndo);
|
||||
}
|
||||
|
||||
return {
|
||||
notifyProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const archiveChannel = async (serverUrl: string, channelId: 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 {database} = operator;
|
||||
const config = await getConfig(database);
|
||||
EphemeralStore.addArchivingChannel(channelId);
|
||||
await client.deleteChannel(channelId);
|
||||
if (config?.ExperimentalViewArchivedChannels === 'true') {
|
||||
await setChannelDeleteAt(serverUrl, channelId, Date.now());
|
||||
} else {
|
||||
removeCurrentUserFromChannel(serverUrl, channelId);
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
} finally {
|
||||
EphemeralStore.removeArchivingChannel(channelId);
|
||||
}
|
||||
};
|
||||
|
||||
export const unarchiveChannel = async (serverUrl: string, channelId: 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 {
|
||||
EphemeralStore.addArchivingChannel(channelId);
|
||||
await client.unarchiveChannel(channelId);
|
||||
await setChannelDeleteAt(serverUrl, channelId, 0);
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
} finally {
|
||||
EphemeralStore.removeArchivingChannel(channelId);
|
||||
}
|
||||
};
|
||||
|
||||
export const convertChannelToPrivate = async (serverUrl: string, channelId: 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 {database} = operator;
|
||||
const channel = await getChannelById(database, channelId);
|
||||
if (channel) {
|
||||
EphemeralStore.addConvertingChannel(channelId);
|
||||
}
|
||||
await client.convertChannelToPrivate(channelId);
|
||||
if (channel) {
|
||||
channel.prepareUpdate((c) => {
|
||||
c.type = General.PRIVATE_CHANNEL;
|
||||
});
|
||||
await operator.batchRecords([channel]);
|
||||
}
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
} finally {
|
||||
EphemeralStore.removeConvertingChannel(channelId);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleKickFromChannel = async (serverUrl: string, channelId: string, event: string = Events.LEAVE_CHANNEL) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
if (currentChannelId !== channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentServer = await getActiveServer();
|
||||
if (currentServer?.url === serverUrl) {
|
||||
const channel = await getChannelById(database, channelId);
|
||||
DeviceEventEmitter.emit(event, channel?.displayName);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
}
|
||||
|
||||
const tabletDevice = await isTablet();
|
||||
|
||||
if (tabletDevice) {
|
||||
const teamId = await getCurrentTeamId(database);
|
||||
await removeChannelFromTeamHistory(operator, teamId, channelId);
|
||||
const newChannelId = await getNthLastChannelFromTeam(database, teamId, 0, channelId);
|
||||
if (newChannelId) {
|
||||
if (currentServer?.url === serverUrl) {
|
||||
if (newChannelId === Screens.GLOBAL_THREADS) {
|
||||
await switchToGlobalThreads(serverUrl, teamId, false);
|
||||
} else {
|
||||
await switchToChannelById(serverUrl, newChannelId, teamId, true);
|
||||
}
|
||||
} else {
|
||||
await setCurrentTeamAndChannelId(operator, teamId, channelId);
|
||||
}
|
||||
} // TODO else jump to "join a channel" screen https://mattermost.atlassian.net/browse/MM-41051
|
||||
} else {
|
||||
await setCurrentChannelId(operator, '');
|
||||
}
|
||||
} catch (error) {
|
||||
logDebug('cannot kick user from channel', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {doAppSubmit, postEphemeralCallResponseForCommandArgs} from '@actions/remote/apps';
|
||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||
import {AppCallResponseTypes} from '@constants/apps';
|
||||
import {showPermalink} from '@actions/remote/permalink';
|
||||
import {Client} from '@client/rest';
|
||||
import DeepLinkTypes 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';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
import {makeDirectChannel, switchToChannelById, switchToChannelByName} from './channel';
|
||||
|
||||
export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string): Promise<{data?: CommandResponse; error?: string | {message: string}}> => {
|
||||
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`};
|
||||
@@ -32,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));
|
||||
|
||||
@@ -42,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(' ');
|
||||
@@ -58,11 +61,7 @@ export const executeCommand = async (serverUrl: string, intl: IntlShape, message
|
||||
}
|
||||
|
||||
const cmd = msg.substring(0, cmdLength).toLowerCase();
|
||||
if (cmd === '/code') {
|
||||
msg = cmd + ' ' + msg.substring(cmdLength, msg.length).trimEnd();
|
||||
} else {
|
||||
msg = cmd + ' ' + msg.substring(cmdLength, msg.length).trim();
|
||||
}
|
||||
msg = cmd + msg.substring(cmdLength);
|
||||
|
||||
let data;
|
||||
try {
|
||||
@@ -78,51 +77,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, '--');
|
||||
@@ -139,7 +131,54 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc
|
||||
const match = matchDeepLink(location, serverUrl, config?.SiteURL);
|
||||
|
||||
if (match) {
|
||||
handleDeepLink(match, intl, location);
|
||||
switch (match.type) {
|
||||
case DeepLinkTypes.CHANNEL: {
|
||||
const data = match.data as DeepLinkChannel;
|
||||
switchToChannelByName(data.serverUrl, data.channelName, data.teamName, DraftUtils.errorBadChannel, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLinkTypes.PERMALINK: {
|
||||
const data = match.data as DeepLinkPermalink;
|
||||
showPermalink(serverUrl, data.teamName, data.postId, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLinkTypes.DMCHANNEL: {
|
||||
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(data.serverUrl, user.id, displayUsername(user, intl.locale, await getTeammateNameDisplay(database)), true);
|
||||
break;
|
||||
}
|
||||
case DeepLinkTypes.GROUPCHANNEL: {
|
||||
const data = match.data as DeepLinkGM;
|
||||
if (!data.channelId) {
|
||||
DraftUtils.errorBadChannel(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
switchToChannelById(data.serverUrl, data.channelId);
|
||||
break;
|
||||
}
|
||||
case DeepLinkTypes.PLUGIN: {
|
||||
const data = match.data as DeepLinkPlugin;
|
||||
showModal('PluginInternal', data.id, {link: location});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const {formatMessage} = intl;
|
||||
const onError = () => Alert.alert(
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {Client} from '@client/rest';
|
||||
import {Emoji, General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {debounce} from '@helpers/api/general';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {queryCustomEmojisByName} from '@queries/servers/custom_emoji';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
|
||||
export const fetchCustomEmojis = async (serverUrl: string, page = 0, perPage = General.PAGE_SIZE_DEFAULT, sort = Emoji.SORT_BY_NAME) => {
|
||||
let client: Client;
|
||||
try {
|
||||
@@ -88,17 +87,10 @@ const debouncedFetchEmojiByNames = debounce(async (serverUrl: string) => {
|
||||
promises.push(client.getCustomEmojiByName(name));
|
||||
}
|
||||
|
||||
const emojis = await Promise.all(promises);
|
||||
|
||||
try {
|
||||
const emojisResult = await Promise.allSettled(promises);
|
||||
const emojis = emojisResult.reduce<CustomEmoji[]>((result, e) => {
|
||||
if (e.status === 'fulfilled') {
|
||||
result.push(e.value);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
if (emojis.length) {
|
||||
await operator.handleCustomEmojis({emojis, prepareRecordsOnly: false});
|
||||
}
|
||||
await operator.handleCustomEmojis({emojis, prepareRecordsOnly: false});
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
|
||||
@@ -1,77 +1,94 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {dataRetentionCleanup, setLastServerVersionCheck} from '@actions/local/systems';
|
||||
import {switchToGlobalThreads} from '@actions/local/thread';
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, getCurrentChannelId, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {queryChannelsById, getDefaultChannelForTeam} from '@queries/servers/channel';
|
||||
import {prepareModels} from '@queries/servers/entry';
|
||||
import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getNthLastChannelFromTeam} from '@queries/servers/team';
|
||||
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 {deferredAppEntryActions, entry} from './gql_common';
|
||||
import {AppEntryData, AppEntryError, deferredAppEntryActions, fetchAppEntryData, registerDeviceToken, syncOtherServers, teamsToRemove} from './common';
|
||||
|
||||
export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) {
|
||||
export async function appEntry(serverUrl: string, since = 0) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
if (!since) {
|
||||
registerDeviceToken(serverUrl);
|
||||
if (Object.keys(DatabaseManager.serverDatabases).length === 1) {
|
||||
await setLastServerVersionCheck(serverUrl, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Run data retention cleanup
|
||||
await dataRetentionCleanup(serverUrl);
|
||||
|
||||
// clear lastUnreadChannelId
|
||||
const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''});
|
||||
if (removeLastUnreadChannelId) {
|
||||
await operator.batchRecords(removeLastUnreadChannelId);
|
||||
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;
|
||||
const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, currentTeamId);
|
||||
const fetchedError = (fetchedData as AppEntryError).error;
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, currentTeamId, currentChannelId, since);
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
return {error: entryData.error};
|
||||
if (fetchedError) {
|
||||
return {error: fetchedError};
|
||||
}
|
||||
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData} = entryData;
|
||||
if (isUpgrade && meData?.user) {
|
||||
const isTabletDevice = await isTablet();
|
||||
const me = await prepareCommonSystemValues(operator, {
|
||||
currentUserId: meData.user.id,
|
||||
currentTeamId: initialTeamId,
|
||||
currentChannelId: isTabletDevice ? initialChannelId : undefined,
|
||||
});
|
||||
if (me?.length) {
|
||||
await operator.batchRecords(me);
|
||||
const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds} = fetchedData as AppEntryData;
|
||||
const rolesData = await fetchRoles(serverUrl, teamData?.memberships, chData?.memberships, meData?.user, true, true);
|
||||
|
||||
if (initialTeamId === currentTeamId) {
|
||||
if (tabletDevice) {
|
||||
const cId = await getNthLastChannelFromTeam(database, currentTeamId);
|
||||
if (cId === Screens.GLOBAL_THREADS) {
|
||||
switchToGlobalThreads(serverUrl);
|
||||
} else {
|
||||
switchToChannelById(serverUrl, cId, initialTeamId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Immediately set the new team as the current team in the database so that the UI
|
||||
// renders the correct team.
|
||||
let channelId = '';
|
||||
if (tabletDevice) {
|
||||
const channel = await getDefaultChannelForTeam(database, initialTeamId);
|
||||
channelId = channel?.id || '';
|
||||
}
|
||||
if (channelId) {
|
||||
switchToChannelById(serverUrl, channelId, initialTeamId);
|
||||
} else {
|
||||
setCurrentTeamAndChannelId(operator, initialTeamId, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId, currentChannelId, initialTeamId, initialChannelId);
|
||||
const removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
|
||||
|
||||
const dt = Date.now();
|
||||
await operator.batchRecords(models);
|
||||
logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
setTeamLoading(serverUrl, false);
|
||||
let removeChannels;
|
||||
if (removeChannelIds?.length) {
|
||||
removeChannels = await queryChannelsById(database, removeChannelIds).fetch();
|
||||
}
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = meData?.user || (await getCurrentUser(database))!;
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData});
|
||||
if (rolesData.roles?.length) {
|
||||
modelPromises.push(operator.handleRole({roles: rolesData.roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = meData.user || (await getCurrentUser(database))!;
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
|
||||
|
||||
if (!since) {
|
||||
@@ -79,22 +96,30 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
||||
syncOtherServers(serverUrl);
|
||||
}
|
||||
|
||||
verifyPushProxy(serverUrl);
|
||||
|
||||
return {userId: currentUserId};
|
||||
const error = teamData.error || chData?.error || prefData.error || meData.error;
|
||||
return {error, userId: meData?.user?.id};
|
||||
}
|
||||
|
||||
export async function upgradeEntry(serverUrl: string) {
|
||||
const dt = Date.now();
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const configAndLicense = await fetchConfigAndLicense(serverUrl, false);
|
||||
const entryData = await appEntry(serverUrl, 0, true);
|
||||
const error = configAndLicense.error || entryData.error;
|
||||
const entry = await appEntry(serverUrl);
|
||||
|
||||
const error = configAndLicense.error || entry.error;
|
||||
|
||||
if (!error) {
|
||||
await DatabaseManager.updateServerIdentifier(serverUrl, configAndLicense.config!.DiagnosticId);
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
const models = await prepareCommonSystemValues(operator, {currentUserId: entry.userId});
|
||||
if (models?.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
DatabaseManager.updateServerIdentifier(serverUrl, configAndLicense.config!.DiagnosticId);
|
||||
DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
deleteV1Data();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {dataRetentionCleanup} from '@actions/local/systems';
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {fetchChannelStats, fetchMissingSidebarInfo, fetchMyChannelsForTeam, markChannelAsRead, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchPostsForChannel, 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 {gqlAllChannels} from '@client/graphQL/entry';
|
||||
import {General, Preferences, Screens} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||
import {fetchAllTeams, fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, MyTeamsRequest} from '@actions/remote/team';
|
||||
import {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPreferenceValue, getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
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 {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 {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';
|
||||
import {queryAllServers} from '@queries/app/servers';
|
||||
import {queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {getConfig} from '@queries/servers/system';
|
||||
import {deleteMyTeams, getAvailableTeamIds, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {isCRTEnabled} from '@utils/thread';
|
||||
|
||||
import {fetchNewThreads} from '../thread';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type {Database, Model} from '@nozbe/watermelondb';
|
||||
|
||||
export type AppEntryData = {
|
||||
initialTeamId: string;
|
||||
@@ -46,38 +33,16 @@ export type AppEntryData = {
|
||||
meData: MyUserRequest;
|
||||
removeTeamIds?: string[];
|
||||
removeChannelIds?: string[];
|
||||
isCRTEnabled: boolean;
|
||||
}
|
||||
|
||||
export type AppEntryError = {
|
||||
error: Error | ClientError | string;
|
||||
error?: Error | ClientError | string;
|
||||
}
|
||||
|
||||
export type EntryResponse = {
|
||||
models: Model[];
|
||||
initialTeamId: string;
|
||||
initialChannelId: string;
|
||||
prefData: MyPreferencesRequest;
|
||||
teamData: MyTeamsRequest;
|
||||
chData?: MyChannelsRequest;
|
||||
meData?: MyUserRequest;
|
||||
} | {
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
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) {
|
||||
return [];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
@@ -92,87 +57,31 @@ export const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[])
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const lastDisconnectedAt = since || await getWebSocketLastDisconnected(database);
|
||||
|
||||
const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, teamId);
|
||||
if ('error' in fetchedData) {
|
||||
return {error: fetchedData.error};
|
||||
}
|
||||
|
||||
const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds, isCRTEnabled} = fetchedData;
|
||||
const error = teamData.error || chData?.error || prefData.error || meData.error;
|
||||
if (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const rolesData = await fetchRoles(serverUrl, teamData.memberships, chData?.memberships, meData.user, true);
|
||||
|
||||
const initialChannelId = await entryInitialChannelId(database, channelId, teamId, initialTeamId, meData?.user?.locale || '', chData?.channels, chData?.memberships);
|
||||
|
||||
const removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
|
||||
|
||||
let removeChannels;
|
||||
if (removeChannelIds?.length) {
|
||||
removeChannels = await queryChannelsById(database, removeChannelIds).fetch();
|
||||
}
|
||||
|
||||
const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData, isCRTEnabled});
|
||||
if (rolesData.roles?.length) {
|
||||
modelPromises.push(operator.handleRole({roles: rolesData.roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
|
||||
return {models: models.flat(), initialChannelId, initialTeamId, prefData, teamData, chData, meData};
|
||||
};
|
||||
|
||||
export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, initialTeamId = ''): Promise<AppEntryData | AppEntryError> => {
|
||||
export const fetchAppEntryData = async (serverUrl: string, since: number, initialTeamId: string): Promise<AppEntryData | AppEntryError> => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
let since = sinceArg;
|
||||
|
||||
const includeDeletedChannels = true;
|
||||
const fetchOnly = true;
|
||||
|
||||
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));
|
||||
if (prefData.preferences) {
|
||||
const crtToggled = await getHasCRTChanged(database, prefData.preferences);
|
||||
if (crtToggled) {
|
||||
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
const isSameServer = currentServerUrl === serverUrl;
|
||||
if (isSameServer) {
|
||||
since = 0;
|
||||
}
|
||||
const {error} = await truncateCrtRelatedTables(serverUrl);
|
||||
if (error) {
|
||||
return {error: `Resetting CRT on ${serverUrl} failed`};
|
||||
}
|
||||
}
|
||||
}
|
||||
await fetchConfigAndLicense(serverUrl);
|
||||
|
||||
// Fetch in parallel teams / team membership / channels for current team / user preferences / user
|
||||
const promises: [Promise<MyTeamsRequest>, Promise<MyChannelsRequest | undefined>, Promise<MyUserRequest>] = [
|
||||
const promises: [Promise<MyTeamsRequest>, Promise<MyChannelsRequest | undefined>, Promise<MyPreferencesRequest>, Promise<MyUserRequest>] = [
|
||||
fetchMyTeams(serverUrl, fetchOnly),
|
||||
initialTeamId ? fetchMyChannelsForTeam(serverUrl, initialTeamId, includeDeletedChannels, since, fetchOnly, false, isCRTEnabled) : Promise.resolve(undefined),
|
||||
initialTeamId ? fetchMyChannelsForTeam(serverUrl, initialTeamId, includeDeletedChannels, since, fetchOnly) : Promise.resolve(undefined),
|
||||
fetchMyPreferences(serverUrl, fetchOnly),
|
||||
fetchMe(serverUrl, fetchOnly),
|
||||
];
|
||||
|
||||
const removeTeamIds: string[] = [];
|
||||
const resolution = await Promise.all(promises);
|
||||
const [teamData, , meData] = resolution;
|
||||
const [teamData, , prefData, meData] = resolution;
|
||||
let [, chData] = resolution;
|
||||
|
||||
if (!initialTeamId && teamData.teams?.length && teamData.memberships?.length) {
|
||||
@@ -183,11 +92,14 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
const myTeams = teamData.teams!.filter((t) => teamMembers.has(t.id));
|
||||
const defaultTeam = selectDefaultTeam(myTeams, meData.user?.locale || DEFAULT_LOCALE, teamOrderPreference, config?.ExperimentalPrimaryTeam);
|
||||
if (defaultTeam?.id) {
|
||||
chData = await fetchMyChannelsForTeam(serverUrl, defaultTeam.id, includeDeletedChannels, since, fetchOnly, false, isCRTEnabled);
|
||||
chData = await fetchMyChannelsForTeam(serverUrl, defaultTeam.id, includeDeletedChannels, since, fetchOnly);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -196,10 +108,13 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
prefData,
|
||||
meData,
|
||||
removeTeamIds,
|
||||
isCRTEnabled,
|
||||
};
|
||||
|
||||
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: '',
|
||||
@@ -216,7 +131,7 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
}
|
||||
|
||||
const availableTeamIds = await getAvailableTeamIds(database, initialTeamId, teamData.teams, prefData.preferences, meData.user?.locale);
|
||||
const alternateTeamData = await fetchAlternateTeamData(serverUrl, availableTeamIds, removeTeamIds, includeDeletedChannels, since, fetchOnly, isCRTEnabled);
|
||||
const alternateTeamData = await fetchAlternateTeamData(serverUrl, availableTeamIds, removeTeamIds, includeDeletedChannels, since, fetchOnly);
|
||||
|
||||
data = {
|
||||
...data,
|
||||
@@ -246,13 +161,13 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
|
||||
export const fetchAlternateTeamData = async (
|
||||
serverUrl: string, availableTeamIds: string[], removeTeamIds: string[],
|
||||
includeDeleted = true, since = 0, fetchOnly = false, isCRTEnabled?: boolean) => {
|
||||
includeDeleted = true, since = 0, fetchOnly = false) => {
|
||||
let initialTeamId = '';
|
||||
let chData;
|
||||
|
||||
for (const teamId of availableTeamIds) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
chData = await fetchMyChannelsForTeam(serverUrl, teamId, includeDeleted, since, fetchOnly, false, isCRTEnabled);
|
||||
chData = await fetchMyChannelsForTeam(serverUrl, teamId, includeDeleted, since, fetchOnly);
|
||||
const chError = chData.error as ClientError | undefined;
|
||||
if (chError?.status_code === 403) {
|
||||
removeTeamIds.push(teamId);
|
||||
@@ -269,92 +184,52 @@ export const fetchAlternateTeamData = async (
|
||||
return {initialTeamId, removeTeamIds};
|
||||
};
|
||||
|
||||
export async function entryInitialChannelId(database: Database, requestedChannelId = '', requestedTeamId = '', initialTeamId: string, locale: string, channels?: Channel[], memberships?: ChannelMember[]) {
|
||||
const membershipIds = new Set(memberships?.map((m) => m.channel_id));
|
||||
const requestedChannel = channels?.find((c) => (c.id === requestedChannelId) && membershipIds.has(c.id));
|
||||
|
||||
// If team and channel are the requested, return the channel
|
||||
if (initialTeamId === requestedTeamId && requestedChannel) {
|
||||
return requestedChannelId;
|
||||
}
|
||||
|
||||
// DM or GMs don't care about changes in teams, so return directly
|
||||
if (requestedChannel && isDMorGM(requestedChannel)) {
|
||||
return requestedChannelId;
|
||||
}
|
||||
|
||||
// Check if we are still members of any channel on the history
|
||||
const teamChannelHistory = await getTeamChannelHistory(database, initialTeamId);
|
||||
for (const c of teamChannelHistory) {
|
||||
if (membershipIds.has(c) || c === Screens.GLOBAL_THREADS) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we are member of the default channel.
|
||||
const defaultChannel = channels?.find((c) => c.name === General.DEFAULT_CHANNEL && c.team_id === initialTeamId);
|
||||
const iAmMemberOfTheTeamDefaultChannel = Boolean(defaultChannel && membershipIds.has(defaultChannel.id));
|
||||
if (iAmMemberOfTheTeamDefaultChannel) {
|
||||
return defaultChannel!.id;
|
||||
}
|
||||
|
||||
// Get the first channel of the list, based on the locale.
|
||||
const myFirstTeamChannel = channels?.filter((c) =>
|
||||
c.team_id === requestedTeamId &&
|
||||
c.type === General.OPEN_CHANNEL &&
|
||||
membershipIds.has(c.id),
|
||||
).sort(sortChannelsByDisplayName.bind(null, locale))[0];
|
||||
return myFirstTeamChannel?.id || '';
|
||||
}
|
||||
|
||||
export async function restDeferredAppEntryActions(
|
||||
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) {
|
||||
// defer sidebar DM & GM profiles
|
||||
let channelsToFetchProfiles: Set<Channel>|undefined;
|
||||
setTimeout(async () => {
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
const directChannels = chData.channels.filter(isDMorGM);
|
||||
channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
// defer fetching posts for initial channel
|
||||
if (initialChannelId) {
|
||||
fetchPostsForChannel(serverUrl, initialChannelId);
|
||||
markChannelAsRead(serverUrl, initialChannelId);
|
||||
fetchChannelStats(serverUrl, initialChannelId);
|
||||
}
|
||||
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||
// defer sidebar DM & GM profiles
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
const directChannels = chData.channels.filter(isDMorGM);
|
||||
const channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
if (channelsToFetchProfiles.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config, license);
|
||||
await fetchMissingSidebarInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId);
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||
}
|
||||
|
||||
// defer fetch channels and unread posts for other teams
|
||||
if (teamData.teams?.length && teamData.memberships?.length) {
|
||||
fetchTeamsChannelsAndUnreadPosts(serverUrl, since, teamData.teams, teamData.memberships, initialTeamId);
|
||||
await fetchTeamsChannelsAndUnreadPosts(serverUrl, since, teamData.teams, teamData.memberships, initialTeamId);
|
||||
}
|
||||
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
|
||||
if (preferences && isCRTEnabled(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 updateAllUsersSince(serverUrl, since);
|
||||
|
||||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, currentUserId);
|
||||
|
||||
setTimeout(async () => {
|
||||
if (channelsToFetchProfiles?.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId);
|
||||
}
|
||||
}, FETCH_MISSING_DM_TIMEOUT);
|
||||
fetchAllTeams(serverUrl);
|
||||
updateAllUsersSince(serverUrl, since);
|
||||
}
|
||||
|
||||
export const registerDeviceToken = async (serverUrl: string) => {
|
||||
@@ -365,89 +240,36 @@ 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).then(() => {
|
||||
dataRetentionCleanup(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);
|
||||
syncAllChannelMembers(server.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const syncAllChannelMembersAndThreads = async (serverUrl: string) => {
|
||||
const syncAllChannelMembers = async (serverUrl: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await getConfig(database);
|
||||
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
const error = await graphQLSyncAllChannelMembers(serverUrl);
|
||||
if (error) {
|
||||
logDebug('failed graphQL, falling back to rest', error);
|
||||
restSyncAllChannelMembers(serverUrl);
|
||||
}
|
||||
} else {
|
||||
restSyncAllChannelMembers(serverUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return 'Server database not found';
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const response = await gqlAllChannels(serverUrl);
|
||||
if ('error' in response) {
|
||||
return response.error;
|
||||
}
|
||||
|
||||
if (response.errors) {
|
||||
return response.errors[0].message;
|
||||
}
|
||||
|
||||
const userId = await getCurrentUserId(database);
|
||||
|
||||
const channels = getMemberChannelsFromGQLQuery(response.data);
|
||||
const memberships = response.data.channelMembers?.map((m) => gqlToClientChannelMembership(m, userId));
|
||||
|
||||
if (channels && memberships) {
|
||||
const modelPromises = await prepareMyChannelsForTeam(operator, '', channels, memberships, undefined, true);
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
}
|
||||
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
if (isCRTEnabled) {
|
||||
const myTeams = await queryMyTeams(operator.database).fetch();
|
||||
for await (const myTeam of myTeams) {
|
||||
// need to await here since GM/DM threads in different teams overlap
|
||||
await syncTeamThreads(serverUrl, myTeam.id);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const restSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
@@ -457,111 +279,12 @@ const restSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
|
||||
try {
|
||||
const myTeams = await client.getMyTeams();
|
||||
const preferences = await client.getMyPreferences();
|
||||
const config = await client.getClientConfigOld();
|
||||
|
||||
let excludeDirect = false;
|
||||
for await (const myTeam of myTeams) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
};
|
||||
|
||||
export async function verifyPushProxy(serverUrl: string) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const ppVerification = await getPushVerificationStatus(database);
|
||||
if (
|
||||
ppVerification !== PUSH_PROXY_STATUS_UNKNOWN &&
|
||||
ppVerification !== ''
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceId = await getDeviceToken();
|
||||
if (!deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await client.ping(deviceId);
|
||||
const canReceiveNotifications = response?.data?.CanReceiveNotifications;
|
||||
switch (canReceiveNotifications) {
|
||||
case PUSH_PROXY_RESPONSE_NOT_AVAILABLE:
|
||||
operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_NOT_AVAILABLE}], prepareRecordsOnly: false});
|
||||
return;
|
||||
case PUSH_PROXY_RESPONSE_UNKNOWN:
|
||||
return;
|
||||
default:
|
||||
operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_VERIFIED}], prepareRecordsOnly: false});
|
||||
}
|
||||
} catch (err) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {storeConfigAndLicense} from '@actions/local/systems';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {fetchDataRetentionPolicy} from '@actions/remote/systems';
|
||||
import {MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry';
|
||||
import {Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPreferenceValue} from '@helpers/api/preference';
|
||||
import {selectDefaultTeam} from '@helpers/api/team';
|
||||
import {queryAllChannels, queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig, getIsDataRetentionEnabled} from '@queries/servers/system';
|
||||
import {filterAndTransformRoles, getMemberChannelsFromGQLQuery, getMemberTeamsFromGQLQuery, gqlToClientChannelMembership, gqlToClientPreference, gqlToClientSidebarCategory, gqlToClientTeamMembership, gqlToClientUser} from '@utils/graphql';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
import {teamsToRemove, FETCH_UNREADS_TIMEOUT, entryRest, EntryResponse, entryInitialChannelId, restDeferredAppEntryActions, getRemoveTeamIds} from './common';
|
||||
|
||||
import type {MyChannelsRequest} from '@actions/remote/channel';
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
export async function deferredAppEntryGraphQLActions(
|
||||
serverUrl: string,
|
||||
since: number,
|
||||
currentUserId: string,
|
||||
teamData: MyTeamsRequest,
|
||||
chData: MyChannelsRequest | undefined,
|
||||
preferences: PreferenceType[] | undefined,
|
||||
config: ClientConfig,
|
||||
initialTeamId?: string,
|
||||
initialChannelId?: string,
|
||||
) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const {database} = operator;
|
||||
|
||||
setTimeout(() => {
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
|
||||
if (initialTeamId) {
|
||||
await syncTeamThreads(serverUrl, initialTeamId);
|
||||
}
|
||||
|
||||
if (teamData.teams?.length) {
|
||||
for await (const team of teamData.teams) {
|
||||
if (team.id !== initialTeamId) {
|
||||
// need to await here since GM/DM threads in different teams overlap
|
||||
await syncTeamThreads(serverUrl, team.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (initialTeamId) {
|
||||
const result = await getChannelData(serverUrl, initialTeamId, currentUserId, true);
|
||||
if ('error' in result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const removeChannels = await getRemoveChannels(database, result.chData, initialTeamId, false);
|
||||
|
||||
const modelPromises = await prepareModels({operator, removeChannels, chData: result.chData}, true);
|
||||
|
||||
const roles = filterAndTransformRoles(result.roles);
|
||||
if (roles.length) {
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
operator.batchRecords(models);
|
||||
|
||||
setTimeout(() => {
|
||||
if (result.chData?.channels?.length && result.chData.memberships?.length) {
|
||||
// defer fetching posts for unread channels on other teams
|
||||
fetchPostsForUnreadChannels(serverUrl, result.chData.channels, result.chData.memberships, initialChannelId);
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
}
|
||||
|
||||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, currentUserId);
|
||||
|
||||
updateCanJoinTeams(serverUrl);
|
||||
updateAllUsersSince(serverUrl, since);
|
||||
|
||||
return {error: undefined};
|
||||
}
|
||||
|
||||
const getRemoveChannels = async (database: Database, chData: MyChannelsRequest | undefined, initialTeamId: string, singleTeam: boolean) => {
|
||||
const removeChannels: ChannelModel[] = [];
|
||||
if (chData?.channels) {
|
||||
const fetchedChannelIds = chData.channels?.map((channel) => channel.id);
|
||||
|
||||
const query = singleTeam ? queryAllChannelsForTeam(database, initialTeamId) : queryAllChannels(database);
|
||||
const channels = await query.fetch();
|
||||
|
||||
for (const channel of channels) {
|
||||
const excludeCondition = singleTeam ? true : channel.teamId !== initialTeamId && channel.teamId !== '';
|
||||
if (excludeCondition && !fetchedChannelIds?.includes(channel.id)) {
|
||||
removeChannels.push(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return removeChannels;
|
||||
};
|
||||
|
||||
const getChannelData = async (serverUrl: string, initialTeamId: string, userId: string, exclude: boolean): Promise<{chData: MyChannelsRequest; roles: Array<Partial<GQLRole>|undefined>} | {error: unknown}> => {
|
||||
let response;
|
||||
try {
|
||||
const request = exclude ? gqlOtherChannels : gqlEntryChannels;
|
||||
response = await request(serverUrl, initialTeamId);
|
||||
} catch (error) {
|
||||
return {error: (error as ClientError).message};
|
||||
}
|
||||
|
||||
if ('error' in response) {
|
||||
return {error: response.error};
|
||||
}
|
||||
|
||||
if ('errors' in response && response.errors?.length) {
|
||||
return {error: response.errors[0].message};
|
||||
}
|
||||
|
||||
const channelsFetchedData = response.data;
|
||||
|
||||
const chData = {
|
||||
channels: getMemberChannelsFromGQLQuery(channelsFetchedData),
|
||||
memberships: channelsFetchedData.channelMembers?.map((m) => gqlToClientChannelMembership(m, userId)),
|
||||
categories: channelsFetchedData.sidebarCategories?.map((c) => gqlToClientSidebarCategory(c, '')),
|
||||
};
|
||||
const roles = channelsFetchedData.channelMembers?.map((m) => m.roles).flat() || [];
|
||||
|
||||
return {chData, roles};
|
||||
};
|
||||
|
||||
export const entryGQL = async (serverUrl: string, currentTeamId?: string, currentChannelId?: string): Promise<EntryResponse> => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const {database} = operator;
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await gqlEntry(serverUrl);
|
||||
} catch (error) {
|
||||
return {error: (error as ClientError).message};
|
||||
}
|
||||
|
||||
if ('error' in response) {
|
||||
return {error: response.error};
|
||||
}
|
||||
|
||||
if ('errors' in response && response.errors?.length) {
|
||||
return {error: response.errors[0].message};
|
||||
}
|
||||
|
||||
const fetchedData = response.data;
|
||||
|
||||
const config = fetchedData.config || {} as ClientConfig;
|
||||
const license = fetchedData.license || {} as ClientLicense;
|
||||
await storeConfigAndLicense(serverUrl, config, license);
|
||||
|
||||
const meData = {
|
||||
user: gqlToClientUser(fetchedData.user!),
|
||||
};
|
||||
|
||||
const allTeams = getMemberTeamsFromGQLQuery(fetchedData);
|
||||
const allTeamMemberships = fetchedData.teamMembers.map((m) => gqlToClientTeamMembership(m, meData.user.id));
|
||||
|
||||
const [nonArchivedTeams, archivedTeamIds] = allTeams.reduce((acc, t) => {
|
||||
if (t.delete_at) {
|
||||
acc[1].add(t.id);
|
||||
return acc;
|
||||
}
|
||||
return [[...acc[0], t], acc[1]];
|
||||
}, [[], new Set<string>()]);
|
||||
|
||||
const nonArchivedTeamMemberships = allTeamMemberships.filter((m) => !archivedTeamIds.has(m.team_id));
|
||||
|
||||
const teamData = {
|
||||
teams: nonArchivedTeams,
|
||||
memberships: nonArchivedTeamMemberships,
|
||||
};
|
||||
|
||||
const prefData = {
|
||||
preferences: fetchedData.user?.preferences?.map(gqlToClientPreference),
|
||||
};
|
||||
|
||||
if (prefData.preferences) {
|
||||
const crtToggled = await getHasCRTChanged(database, prefData.preferences);
|
||||
if (crtToggled) {
|
||||
const {error} = await truncateCrtRelatedTables(serverUrl);
|
||||
if (error) {
|
||||
return {error: `Resetting CRT on ${serverUrl} failed`};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let initialTeamId = currentTeamId;
|
||||
if (!teamData.teams.length) {
|
||||
initialTeamId = '';
|
||||
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId && t.delete_at === 0)) {
|
||||
const teamOrderPreference = getPreferenceValue(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
|
||||
initialTeamId = selectDefaultTeam(teamData.teams, meData.user.locale, teamOrderPreference, config.ExperimentalPrimaryTeam)?.id || '';
|
||||
}
|
||||
const gqlRoles = [
|
||||
...fetchedData.user?.roles || [],
|
||||
...fetchedData.teamMembers?.map((m) => m.roles).flat() || [],
|
||||
];
|
||||
|
||||
let chData;
|
||||
if (initialTeamId) {
|
||||
const result = await getChannelData(serverUrl, initialTeamId, meData.user.id, false);
|
||||
if ('error' in result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
chData = result.chData;
|
||||
gqlRoles.push(...result.roles);
|
||||
}
|
||||
|
||||
const roles = filterAndTransformRoles(gqlRoles);
|
||||
|
||||
const initialChannelId = await entryInitialChannelId(database, currentChannelId, currentTeamId, initialTeamId, meData.user.id, chData?.channels, chData?.memberships);
|
||||
const removeChannels = await getRemoveChannels(database, chData, initialTeamId, true);
|
||||
const removeTeamIds = await getRemoveTeamIds(database, teamData);
|
||||
const removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
|
||||
|
||||
const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData}, true);
|
||||
if (roles.length) {
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
return {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData};
|
||||
};
|
||||
|
||||
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const config = await getConfig(database);
|
||||
let result;
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
result = await entryGQL(serverUrl, teamId, channelId);
|
||||
if ('error' in result) {
|
||||
logDebug('Error using GraphQL, trying REST', result.error);
|
||||
result = entryRest(serverUrl, teamId, channelId, since);
|
||||
}
|
||||
} else {
|
||||
result = entryRest(serverUrl, teamId, channelId, since);
|
||||
}
|
||||
|
||||
// Fetch data retention policies
|
||||
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
|
||||
if (isDataRetentionEnabled) {
|
||||
fetchDataRetentionPolicy(serverUrl);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export async function deferredAppEntryActions(
|
||||
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
|
||||
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
initialTeamId?: string, initialChannelId?: string) {
|
||||
let result;
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
result = await deferredAppEntryGraphQLActions(serverUrl, since, currentUserId, teamData, chData, preferences, config, initialTeamId, initialChannelId);
|
||||
if (result.error) {
|
||||
logDebug('Error using GraphQL, trying REST', result.error);
|
||||
result = restDeferredAppEntryActions(serverUrl, since, currentUserId, currentUserLocale, preferences, config, license, teamData, chData, initialTeamId, initialChannelId);
|
||||
}
|
||||
} else {
|
||||
result = restDeferredAppEntryActions(serverUrl, since, currentUserId, currentUserLocale, preferences, config, license, teamData, chData, initialTeamId, initialChannelId);
|
||||
}
|
||||
|
||||
autoUpdateTimezone(serverUrl);
|
||||
|
||||
return result;
|
||||
}
|
||||