forked from Ivasoft/mattermost-mobile
Compare commits
55 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41848b2634 | ||
|
|
8d81d946c5 | ||
|
|
10d27ee5ba | ||
|
|
647def15be | ||
|
|
da440e50fb | ||
|
|
793ac98d74 | ||
|
|
fab353b494 | ||
|
|
8853b5dd45 | ||
|
|
464d93df8d | ||
|
|
47f126b71f | ||
|
|
4b2a0c7aea | ||
|
|
f6bedbb7d6 | ||
|
|
b5ee7c8908 | ||
|
|
aab0814b7f | ||
|
|
bf840785fb | ||
|
|
514e9cfd08 | ||
|
|
b5c3e95a4b | ||
|
|
e6547d7dc1 | ||
|
|
2b8bba7c24 | ||
|
|
9b9373e27b | ||
|
|
bc25a29c42 | ||
|
|
4c4dd8297d | ||
|
|
c3fc53a071 | ||
|
|
34af598a6d | ||
|
|
ff89f3530e | ||
|
|
8a95000bd0 | ||
|
|
21e1466068 | ||
|
|
4ebcba6069 | ||
|
|
b34ce42016 | ||
|
|
97d393a2ba | ||
|
|
fb03a88304 | ||
|
|
6e239d5566 | ||
|
|
72b95fa265 | ||
|
|
d19fc71ad4 | ||
|
|
526290bbdf | ||
|
|
962b38d024 | ||
|
|
51109c74d3 | ||
|
|
098230e79e | ||
|
|
8fa67bd5b4 | ||
|
|
6d7749a098 | ||
|
|
37479587cc | ||
|
|
3708b86b30 | ||
|
|
cabce2a808 | ||
|
|
4abb483f2c | ||
|
|
7a0bf1dc77 | ||
|
|
6cf1140a0f | ||
|
|
9506875683 | ||
|
|
679a897848 | ||
|
|
b5fd0284e8 | ||
|
|
67eea1750d | ||
|
|
d4e405485b | ||
|
|
1389e4f7f7 | ||
|
|
7cf4084fe5 | ||
|
|
6b7cffd6af | ||
|
|
dba3278c9f |
@@ -1,6 +1,4 @@
|
||||
version: 2.1
|
||||
orbs:
|
||||
owasp: entur/owasp@0.0.10
|
||||
|
||||
executors:
|
||||
android:
|
||||
@@ -13,7 +11,7 @@ executors:
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
docker:
|
||||
- image: circleci/android:api-29-node
|
||||
- image: circleci/android:api-27-node
|
||||
working_directory: ~/mattermost-mobile
|
||||
resource_class: <<parameters.resource_class>>
|
||||
|
||||
@@ -23,7 +21,7 @@ executors:
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
macos:
|
||||
xcode: "12.0.0"
|
||||
xcode: "11.0.0"
|
||||
working_directory: ~/mattermost-mobile
|
||||
shell: /bin/bash --login -o pipefail
|
||||
|
||||
@@ -44,6 +42,7 @@ commands:
|
||||
for:
|
||||
type: string
|
||||
steps:
|
||||
- ruby-setup
|
||||
- restore_cache:
|
||||
name: Restore Fastlane cache
|
||||
key: v1-gems-<< parameters.for >>-{{ checksum "fastlane/Gemfile.lock" }}-{{ arch }}
|
||||
@@ -81,7 +80,7 @@ commands:
|
||||
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
|
||||
- run:
|
||||
name: Generate assets
|
||||
command: node ./scripts/generate-assets.js
|
||||
command: make dist/assets
|
||||
- save_cache:
|
||||
name: Save assets cache
|
||||
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
|
||||
@@ -103,8 +102,8 @@ commands:
|
||||
paths:
|
||||
- node_modules
|
||||
- run:
|
||||
name: "Patch dependencies"
|
||||
command: npx patch-package
|
||||
name: "Run post install scripts"
|
||||
command: make post-install
|
||||
|
||||
pods-dependencies:
|
||||
description: "Get cocoapods dependencies"
|
||||
@@ -112,12 +111,10 @@ commands:
|
||||
- restore_cache:
|
||||
name: Restore cocoapods specs and pods
|
||||
key: v1-cocoapods-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
|
||||
- run:
|
||||
name: iOS gems
|
||||
command: npm run ios-gems
|
||||
- run:
|
||||
name: Getting cocoapods dependencies
|
||||
command: npm run pod-install
|
||||
working_directory: ios
|
||||
command: pod install
|
||||
- save_cache:
|
||||
name: Save cocoapods specs and pods cache
|
||||
key: v1-cocoapods-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
|
||||
@@ -144,14 +141,11 @@ commands:
|
||||
echo MATTERMOST_RELEASE_STORE_FILE=${STORE_FILE} | tee -a android/gradle.properties > /dev/null
|
||||
echo ${STORE_ALIAS} | tee -a android/gradle.properties > /dev/null
|
||||
echo ${STORE_PASSWORD} | tee -a android/gradle.properties > /dev/null
|
||||
- run:
|
||||
name: Jetify android libraries
|
||||
command: ./node_modules/.bin/jetify
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build android
|
||||
no_output_timeout: 30m
|
||||
command: export TERM=xterm && bundle exec fastlane android build
|
||||
command: bundle exec fastlane android build
|
||||
|
||||
build-ios:
|
||||
description: "Build the iOS app"
|
||||
@@ -169,7 +163,7 @@ commands:
|
||||
no_output_timeout: 30m
|
||||
command: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
export TERM=xterm && bundle exec fastlane ios build
|
||||
bundle exec fastlane ios build
|
||||
|
||||
deploy-to-store:
|
||||
description: "Deploy build to store"
|
||||
@@ -202,13 +196,14 @@ commands:
|
||||
filename:
|
||||
type: string
|
||||
steps:
|
||||
- run:
|
||||
name: Copying artifacts
|
||||
command: |
|
||||
mkdir /tmp/artifacts;
|
||||
cp ~/mattermost-mobile/<<parameters.filename>> /tmp/artifacts;
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts
|
||||
path: ~/mattermost-mobile/<<parameters.filename>>
|
||||
|
||||
ruby-setup:
|
||||
steps:
|
||||
- run:
|
||||
name: Set Ruby Version
|
||||
command: echo "ruby-2.6.3" > ~/.ruby-version
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -228,59 +223,7 @@ jobs:
|
||||
command: npm test
|
||||
- run:
|
||||
name: Check i18n
|
||||
command: ./scripts/precommit/i18n.sh
|
||||
|
||||
check-deps:
|
||||
parameters:
|
||||
cve_data_directory:
|
||||
type: string
|
||||
default: "~/.owasp/dependency-check-data"
|
||||
working_directory: ~/mattermost-mobile
|
||||
executor: owasp/default
|
||||
environment:
|
||||
version_url: "https://jeremylong.github.io/DependencyCheck/current.txt"
|
||||
executable_url: "https://dl.bintray.com/jeremy-long/owasp/dependency-check-VERSION-release.zip"
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
name: Restore npm cache
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
- run:
|
||||
name: Checkout config
|
||||
command: cd .. && git clone https://github.com/mattermost/security-automation-config
|
||||
- run:
|
||||
name: Install Go
|
||||
command: sudo apt-get update && sudo apt-get install golang
|
||||
- owasp/with_commandline:
|
||||
steps:
|
||||
# Taken from https://github.com/entur/owasp-orb/blob/master/src/%40orb.yml#L349-L361
|
||||
- owasp/generate_cache_keys:
|
||||
cache_key: commmandline-default-cache-key-v6
|
||||
- owasp/restore_owasp_cache
|
||||
- run:
|
||||
name: Update OWASP Dependency-Check Database
|
||||
command: |
|
||||
if ! ~/.owasp/dependency-check/bin/dependency-check.sh --data << parameters.cve_data_directory >> --updateonly; then
|
||||
# Update failed, probably due to a bad DB version; delete cached DB and try again
|
||||
rm -rv ~/.owasp/dependency-check-data/*.db
|
||||
~/.owasp/dependency-check/bin/dependency-check.sh --data << parameters.cve_data_directory >> --updateonly
|
||||
fi
|
||||
- owasp/store_owasp_cache:
|
||||
cve_data_directory: <<parameters.cve_data_directory>>
|
||||
- run:
|
||||
name: Run OWASP Dependency-Check Analyzer
|
||||
command: |
|
||||
~/.owasp/dependency-check/bin/dependency-check.sh \
|
||||
--data << parameters.cve_data_directory >> --format ALL --noupdate --enableExperimental \
|
||||
--propertyfile ../security-automation-config/dependency-check/dependencycheck.properties \
|
||||
--suppression ../security-automation-config/dependency-check/suppression.xml \
|
||||
--suppression ../security-automation-config/dependency-check/suppression.$CIRCLE_PROJECT_REPONAME.xml \
|
||||
--scan './**/*' || true
|
||||
- owasp/collect_reports:
|
||||
persist_to_workspace: false
|
||||
- run:
|
||||
name: Post results to Mattermost
|
||||
command: go run ../security-automation-config/dependency-check/post_results.go
|
||||
command: make i18n-extract-ci
|
||||
|
||||
build-android-beta:
|
||||
executor: android
|
||||
@@ -288,7 +231,7 @@ jobs:
|
||||
- build-android
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
filename: "Mattermost_Beta.apk"
|
||||
|
||||
build-android-release:
|
||||
executor: android
|
||||
@@ -296,7 +239,7 @@ jobs:
|
||||
- build-android
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
filename: "Mattermost.apk"
|
||||
|
||||
build-android-pr:
|
||||
executor: android
|
||||
@@ -305,7 +248,7 @@ jobs:
|
||||
steps:
|
||||
- build-android
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
filename: "Mattermost_Beta.apk"
|
||||
|
||||
build-android-unsigned:
|
||||
executor: android
|
||||
@@ -317,9 +260,6 @@ jobs:
|
||||
- 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
|
||||
@@ -327,7 +267,7 @@ jobs:
|
||||
command: bundle exec fastlane android unsigned
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
filename: "Mattermost-unsigned.apk"
|
||||
|
||||
build-ios-beta:
|
||||
executor: ios
|
||||
@@ -335,7 +275,7 @@ jobs:
|
||||
- build-ios
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
filename: "Mattermost_Beta.ipa"
|
||||
|
||||
build-ios-release:
|
||||
executor: ios
|
||||
@@ -343,7 +283,7 @@ jobs:
|
||||
- build-ios
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
filename: "Mattermost.ipa"
|
||||
|
||||
build-ios-pr:
|
||||
executor: ios
|
||||
@@ -352,13 +292,14 @@ jobs:
|
||||
steps:
|
||||
- build-ios
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
filename: "Mattermost_Beta.ipa"
|
||||
|
||||
build-ios-unsigned:
|
||||
executor: ios
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- ruby-setup
|
||||
- npm-dependencies
|
||||
- pods-dependencies
|
||||
- assets
|
||||
@@ -374,75 +315,56 @@ jobs:
|
||||
- persist_to_workspace:
|
||||
root: ~/
|
||||
paths:
|
||||
- mattermost-mobile/*.ipa
|
||||
- mattermost-mobile/Mattermost-unsigned.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"
|
||||
filename: "Mattermost-unsigned.ipa"
|
||||
|
||||
deploy-android-release:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- ruby-setup
|
||||
- deploy-to-store:
|
||||
task: "Deploy to Google Play"
|
||||
target: android
|
||||
file: "*.apk"
|
||||
file: Mattermost.apk
|
||||
|
||||
deploy-android-beta:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- ruby-setup
|
||||
- deploy-to-store:
|
||||
task: "Deploy to Google Play"
|
||||
target: android
|
||||
file: "*.apk"
|
||||
file: Mattermost_Beta.apk
|
||||
|
||||
deploy-ios-release:
|
||||
executor: ios
|
||||
steps:
|
||||
- ruby-setup
|
||||
- deploy-to-store:
|
||||
task: "Deploy to TestFlight"
|
||||
target: ios
|
||||
file: "*.ipa"
|
||||
file: Mattermost.ipa
|
||||
|
||||
deploy-ios-beta:
|
||||
executor: ios
|
||||
steps:
|
||||
- ruby-setup
|
||||
- deploy-to-store:
|
||||
task: "Deploy to TestFlight"
|
||||
target: ios
|
||||
file: "*.ipa"
|
||||
file: Mattermost_Beta.ipa
|
||||
|
||||
github-release:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- ruby-setup
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run:
|
||||
@@ -455,10 +377,6 @@ workflows:
|
||||
build:
|
||||
jobs:
|
||||
- test
|
||||
- check-deps:
|
||||
context: sast-webhook
|
||||
requires:
|
||||
- test
|
||||
|
||||
- build-android-release:
|
||||
context: mattermost-mobile-android-release
|
||||
@@ -577,17 +495,6 @@ workflows:
|
||||
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+$/
|
||||
|
||||
- github-release:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
@@ -597,4 +504,4 @@ workflows:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
only: unsigned
|
||||
only: unsigned
|
||||
@@ -23,29 +23,18 @@
|
||||
},
|
||||
"rules": {
|
||||
"global-require": 0,
|
||||
"no-undefined": 0,
|
||||
"react/display-name": [2, { "ignoreTranspilerName": false }],
|
||||
"react/jsx-filename-extension": 0,
|
||||
"camelcase": [
|
||||
0,
|
||||
{
|
||||
"properties": "never"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/ban-types": 0,
|
||||
"react/jsx-filename-extension": [2, {"extensions": [".js"]}],
|
||||
"no-undefined": 0,
|
||||
"no-nested-ternary": 0,
|
||||
"@typescript-eslint/camelcase": 0,
|
||||
"@typescript-eslint/no-undefined": 0,
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
2,
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "after-used"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": 2,
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-use-before-define": 0,
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off"
|
||||
"@typescript-eslint/explicit-function-return-type": 0
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
@@ -53,23 +42,6 @@
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["detox/e2e/**"],
|
||||
"globals": {
|
||||
"by": true,
|
||||
"detox": true,
|
||||
"device": true,
|
||||
"element": true,
|
||||
"waitFor": true
|
||||
},
|
||||
"rules": {
|
||||
"func-names": 0,
|
||||
"import/no-unresolved": 0,
|
||||
"max-nested-callbacks": 0,
|
||||
"no-process-env": 0,
|
||||
"no-unused-expressions": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -71,4 +71,4 @@ untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.122.0
|
||||
^0.113.0
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -5,8 +5,6 @@ build-ios
|
||||
server.PID
|
||||
mattermost.keystore
|
||||
tmp/
|
||||
.env
|
||||
env.d.ts
|
||||
|
||||
# OSX
|
||||
#
|
||||
@@ -91,11 +89,6 @@ ios/sentry.properties
|
||||
coverage
|
||||
.tmp
|
||||
|
||||
# E2E testing
|
||||
mattermost-license.txt
|
||||
*.mattermost-license
|
||||
detox/artifacts
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
"stories": [
|
||||
"../app/components/**/*.stories.mdx",
|
||||
"../app/components/**/*.stories.@(js|jsx|ts|tsx)"
|
||||
],
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
}
|
||||
|
||||
1481
CHANGELOG.md
1481
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "cocoapods", "1.10.0.rc.1"
|
||||
gem "cocoapods", "1.7.5"
|
||||
@@ -2,27 +2,23 @@ GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.2)
|
||||
activesupport (5.2.4.4)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
activesupport (4.2.11.1)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
algoliasearch (1.27.4)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
atomos (0.1.3)
|
||||
claide (1.0.3)
|
||||
cocoapods (1.10.0.rc.1)
|
||||
addressable (~> 2.6)
|
||||
cocoapods (1.7.5)
|
||||
activesupport (>= 4.0.2, < 5)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.10.0.rc.1)
|
||||
cocoapods-core (= 1.7.5)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 1.4.0, < 2.0)
|
||||
cocoapods-downloader (>= 1.2.2, < 2.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.4.0, < 2.0)
|
||||
cocoapods-stats (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.3.1, < 2.0)
|
||||
cocoapods-try (>= 1.1.0, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
@@ -31,63 +27,50 @@ GEM
|
||||
molinillo (~> 0.6.6)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (~> 1.4)
|
||||
xcodeproj (>= 1.17.0, < 2.0)
|
||||
cocoapods-core (1.10.0.rc.1)
|
||||
activesupport (> 5.0, < 6)
|
||||
addressable (~> 2.6)
|
||||
algoliasearch (~> 1.0)
|
||||
concurrent-ruby (~> 1.1)
|
||||
xcodeproj (>= 1.10.0, < 2.0)
|
||||
cocoapods-core (1.7.5)
|
||||
activesupport (>= 4.0.2, < 6)
|
||||
fuzzy_match (~> 2.0.4)
|
||||
nap (~> 1.0)
|
||||
netrc (~> 0.11)
|
||||
public_suffix
|
||||
typhoeus (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.4)
|
||||
cocoapods-downloader (1.4.0)
|
||||
cocoapods-downloader (1.3.0)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.0)
|
||||
cocoapods-trunk (1.5.0)
|
||||
cocoapods-stats (1.1.0)
|
||||
cocoapods-trunk (1.4.1)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.2.0)
|
||||
cocoapods-try (1.1.0)
|
||||
colored2 (3.1.2)
|
||||
concurrent-ruby (1.1.7)
|
||||
concurrent-ruby (1.1.5)
|
||||
escape (0.0.4)
|
||||
ethon (0.12.0)
|
||||
ffi (>= 1.3.0)
|
||||
ffi (1.13.1)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.8.5)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.3.1)
|
||||
minitest (5.14.2)
|
||||
minitest (5.14.0)
|
||||
molinillo (0.6.6)
|
||||
nanaimo (0.3.0)
|
||||
nanaimo (0.2.6)
|
||||
nap (1.1.0)
|
||||
netrc (0.11.0)
|
||||
public_suffix (4.0.6)
|
||||
ruby-macho (1.4.0)
|
||||
thread_safe (0.3.6)
|
||||
typhoeus (1.4.0)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (1.2.7)
|
||||
tzinfo (1.2.6)
|
||||
thread_safe (~> 0.1)
|
||||
xcodeproj (1.18.0)
|
||||
xcodeproj (1.15.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
nanaimo (~> 0.2.6)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
cocoapods (= 1.10.0.rc.1)
|
||||
cocoapods (= 1.7.5)
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
2.0.2
|
||||
251
Makefile
Normal file
251
Makefile
Normal file
@@ -0,0 +1,251 @@
|
||||
.PHONY: pre-run pre-build clean
|
||||
.PHONY: check-style
|
||||
.PHONY: i18n-extract-ci
|
||||
.PHONY: start stop
|
||||
.PHONY: run run-ios run-android
|
||||
.PHONY: build build-ios build-android unsigned-ios unsigned-android ios-sim-x86_64
|
||||
.PHONY: build-pr can-build-pr prepare-pr
|
||||
.PHONY: test help
|
||||
|
||||
OS := $(shell sh -c 'uname -s 2>/dev/null')
|
||||
BASE_ASSETS = $(shell find assets/base -type d) $(shell find assets/base -type f -name '*')
|
||||
OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell find assets/override -type f -name '*' 2> /dev/null)
|
||||
MM_UTILITIES_DIR = ../mattermost-utilities
|
||||
|
||||
node_modules: package.json
|
||||
@if ! [ $(shell which npm 2> /dev/null) ]; then \
|
||||
echo "npm is not installed https://npmjs.com"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@echo Getting Javascript dependencies
|
||||
@npm install
|
||||
|
||||
npm-ci: package.json
|
||||
@if ! [ $(shell which npm 2> /dev/null) ]; then \
|
||||
echo "npm is not installed https://npmjs.com"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@echo Getting Javascript dependencies
|
||||
@npm ci
|
||||
|
||||
.podinstall:
|
||||
ifeq ($(OS), Darwin)
|
||||
@echo "Required version of Cocoapods is not installed"
|
||||
@echo Installing gems;
|
||||
@bundle install
|
||||
@echo Getting Cocoapods dependencies;
|
||||
@cd ios && bundle exec pod install;
|
||||
endif
|
||||
@touch $@
|
||||
|
||||
dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
|
||||
@mkdir -p dist
|
||||
|
||||
@if [ -e dist/assets ] ; then \
|
||||
rm -rf dist/assets; \
|
||||
fi
|
||||
|
||||
@echo "Generating app assets"
|
||||
@node scripts/make-dist-assets.js
|
||||
|
||||
pre-run: | node_modules .podinstall dist/assets ## Installs dependencies and assets
|
||||
|
||||
pre-build: | npm-ci .podinstall dist/assets ## Install dependencies and assets before building
|
||||
|
||||
check-style: node_modules ## Runs eslint
|
||||
@echo Checking for style guide compliance
|
||||
@npm run check
|
||||
|
||||
clean: ## Cleans dependencies, previous builds and temp files
|
||||
@echo Cleaning started
|
||||
|
||||
@rm -f .podinstall
|
||||
@rm -rf ios/Pods
|
||||
@rm -rf node_modules
|
||||
@rm -rf dist
|
||||
@rm -rf ios/build
|
||||
@rm -rf android/app/build
|
||||
|
||||
@echo Cleanup finished
|
||||
|
||||
post-install:
|
||||
@./node_modules/.bin/patch-package
|
||||
@./node_modules/.bin/jetify
|
||||
|
||||
@rm -f node_modules/intl/.babelrc
|
||||
@# Hack to get react-intl and its dependencies to work with react-native
|
||||
@# Based off of https://github.com/este/este/blob/master/gulp/native-fix.js
|
||||
@sed -i'' -e 's|"./locale-data/index.js": false|"./locale-data/index.js": "./locale-data/index.js"|g' node_modules/react-intl/package.json
|
||||
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
|
||||
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
|
||||
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
|
||||
|
||||
start: | pre-run ## Starts the React Native packager server
|
||||
$(call start_packager)
|
||||
|
||||
stop: ## Stops the React Native packager server
|
||||
$(call stop_packager)
|
||||
|
||||
check-device-ios:
|
||||
@if ! [ $(shell which xcodebuild) ]; then \
|
||||
echo "xcode is not installed"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if ! [ $(shell which watchman) ]; then \
|
||||
echo "watchman is not installed"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
check-device-android:
|
||||
@if ! [ $(ANDROID_HOME) ]; then \
|
||||
echo "ANDROID_HOME is not set"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if ! [ $(shell which adb 2> /dev/null) ]; then \
|
||||
echo "adb is not installed"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@echo "Connect your Android device or open the emulator"
|
||||
@adb wait-for-device
|
||||
|
||||
@if ! [ $(shell which watchman 2> /dev/null) ]; then \
|
||||
echo "watchman is not installed"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
prepare-android-build:
|
||||
@rm -rf ./node_modules/react-native/local-cli/templates/HelloWorld
|
||||
@rm -rf ./node_modules/react-native-linear-gradient/Examples/
|
||||
@rm -rf ./node_modules/react-native-orientation/demo/
|
||||
|
||||
run: run-ios ## alias for run-ios
|
||||
|
||||
run-ios: | check-device-ios pre-run ## Runs the app on an iOS simulator
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo Running iOS app in development; \
|
||||
if [ ! -z "${SIMULATOR}" ]; then \
|
||||
react-native run-ios --simulator="${SIMULATOR}"; \
|
||||
else \
|
||||
react-native run-ios; \
|
||||
fi; \
|
||||
wait; \
|
||||
else \
|
||||
echo Running iOS app in development; \
|
||||
if [ ! -z "${SIMULATOR}" ]; then \
|
||||
react-native run-ios --simulator="${SIMULATOR}"; \
|
||||
else \
|
||||
react-native run-ios; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
run-android: | check-device-android pre-run prepare-android-build ## Runs the app on an Android emulator or dev device
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo Running Android app in development; \
|
||||
if [ ! -z ${VARIANT} ]; then \
|
||||
react-native run-android --no-packager --variant=${VARIANT}; \
|
||||
else \
|
||||
react-native run-android --no-packager; \
|
||||
fi; \
|
||||
wait; \
|
||||
else \
|
||||
echo Running Android app in development; \
|
||||
if [ ! -z ${VARIANT} ]; then \
|
||||
react-native run-android --no-packager --variant=${VARIANT}; \
|
||||
else \
|
||||
react-native run-android --no-packager; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
build: | stop pre-build check-style i18n-extract-ci ## Builds the app for Android & iOS
|
||||
$(call start_packager)
|
||||
@echo "Building App"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build
|
||||
$(call stop_packager)
|
||||
|
||||
|
||||
build-ios: | stop pre-build check-style i18n-extract-ci ## Builds the iOS app
|
||||
$(call start_packager)
|
||||
@echo "Building iOS app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
|
||||
$(call stop_packager)
|
||||
|
||||
build-android: | stop pre-build check-style i18n-extract-ci prepare-android-build ## Build the Android app
|
||||
$(call start_packager)
|
||||
@echo "Building Android app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
|
||||
$(call stop_packager)
|
||||
|
||||
unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS app
|
||||
$(call start_packager)
|
||||
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
|
||||
$(call stop_packager)
|
||||
|
||||
ios-sim-x86_64: stop pre-build check-style ## Build an unsigned x86_64 version of the iOS app for iPhone simulator
|
||||
$(call start_packager)
|
||||
@echo "Building unsigned x86_64 iOS app for iPhone simulator"
|
||||
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
|
||||
@mkdir -p build-ios
|
||||
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -arch x86_64 -sdk iphonesimulator -configuration Release -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ ENABLE_BITCODE=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ENABLE_BITCODE=NO
|
||||
@cd build-ios/Build/Products/Release-iphonesimulator/ && zip -r Mattermost-simulator-x86_64.app.zip Mattermost.app/
|
||||
@mv build-ios/Build/Products/Release-iphonesimulator/Mattermost-simulator-x86_64.app.zip .
|
||||
@rm -rf build-ios/
|
||||
@cd fastlane && bundle exec fastlane upload_file_to_s3 file:Mattermost-simulator-x86_64.app.zip os_type:iOS
|
||||
$(call stop_packager)
|
||||
|
||||
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
|
||||
@cd fastlane && NODE_ENV=production bundle exec fastlane android unsigned
|
||||
|
||||
test: | pre-run check-style ## Runs tests
|
||||
@npm test
|
||||
|
||||
build-pr: | can-build-pr stop pre-build check-style i18n-extract-ci ## Build a PR from the mattermost-mobile repo
|
||||
$(call start_packager)
|
||||
@echo "Building App from PR ${PR_ID}"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build_pr pr:PR-${PR_ID}
|
||||
$(call stop_packager)
|
||||
|
||||
can-build-pr:
|
||||
@if [ -z ${PR_ID} ]; then \
|
||||
echo a PR number needs to be specified; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
i18n-extract: ## Extract strings for translation from the source code
|
||||
npm run mmjstool -- i18n extract-mobile
|
||||
|
||||
i18n-extract-ci:
|
||||
mkdir -p tmp
|
||||
cp assets/base/i18n/en.json tmp/en.json
|
||||
mkdir -p tmp/fake-webapp-dir/i18n/
|
||||
echo '{}' > tmp/fake-webapp-dir/i18n/en.json
|
||||
npm run mmjstool -- i18n extract-mobile --webapp-dir tmp/fake-webapp-dir --mobile-dir .
|
||||
diff tmp/en.json assets/base/i18n/en.json
|
||||
rm -rf tmp
|
||||
|
||||
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
define start_packager
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
else \
|
||||
echo React Native packager server already running; \
|
||||
fi
|
||||
endef
|
||||
|
||||
define stop_packager
|
||||
@echo Stopping React Native packager server
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
|
||||
ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9; \
|
||||
echo React Native packager server stopped; \
|
||||
else \
|
||||
echo No React Native packager server running; \
|
||||
fi
|
||||
endef
|
||||
171
NOTICE.txt
171
NOTICE.txt
@@ -113,20 +113,18 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @react-native-community/clipboard
|
||||
## @react-native-community/cookies
|
||||
|
||||
This product contains '@react-native-community/clipboard' by React Native Community.
|
||||
This product contains '@react-native-community/cookies' by React Native Community.
|
||||
|
||||
React Native Clipboard API for both iOS and Android
|
||||
Cookie Manager for React Native
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-community/clipboard
|
||||
* https://github.com/react-native-community/cookies
|
||||
|
||||
* LICENSE: MIT
|
||||
* LICENSE: MIT License
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
Copyright (c) 2020 React Native Community
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -1521,20 +1519,22 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-cookies
|
||||
## react-native-circular-progress
|
||||
|
||||
This product contains a modified version of 'react-native-cookies' by Joseph P. Ferraro.
|
||||
This product contains 'react-native-circular-progress' by Bart Gryszko.
|
||||
|
||||
Cookie manager for react native.
|
||||
React Native component for creating animated, circular progress with react-native-svg
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/joeferraro/react-native-cookies
|
||||
* https://github.com/bgryszko/react-native-circular-progress
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 shimo
|
||||
Copyright (c) 2015 Bart Gryszko
|
||||
|
||||
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -1543,16 +1543,20 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
@@ -1855,6 +1859,30 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
|
||||
---
|
||||
|
||||
## react-native-image-gallery
|
||||
|
||||
This product contains a modified version of 'react-native-image-gallery' by Archriss.
|
||||
|
||||
Pure JavaScript image gallery component for iOS and Android
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/archriss/react-native-image-gallery#readme
|
||||
|
||||
* LICENSE: ISC
|
||||
|
||||
Note: An original license file for this dependency is not available. We determined the type of license based on the package registry entry for this project. The following text has been prepared using a template from the SPDX Workgroup (https://spdx.org) for this type of license.
|
||||
|
||||
ISC License:
|
||||
|
||||
Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC")
|
||||
Copyright (c) 1995-2003 by Internet Software Consortium
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-image-picker
|
||||
|
||||
This product contains 'react-native-image-picker' by Marc Shilling.
|
||||
@@ -1892,12 +1920,12 @@ SOFTWARE.
|
||||
|
||||
## react-native-keyboard-aware-scroll-view
|
||||
|
||||
This product contains a modified version of 'react-native-keyboard-aware-scroll-view' by APSL.
|
||||
This product contains 'react-native-keyboard-aware-scroll-view' by Alvaro Medina Ballester.
|
||||
|
||||
A ScrollView component that handles keyboard appearance and automatically scrolls to focused TextInput.
|
||||
A React Native ScrollView component that resizes when the keyboard appears.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/APSL/react-native-keyboard-aware-scroll-view
|
||||
* https://github.com/APSL/react-native-keyboard-aware-scroll-view#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
@@ -2289,39 +2317,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-redash
|
||||
|
||||
This product contains 'react-native-redash' by William Candillon.
|
||||
|
||||
The React Native Reanimated and Gesture Handler Toolbelt.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/wcandillon/react-native-redash
|
||||
|
||||
* LICENSE: MIT License
|
||||
|
||||
Copyright (c) 2020 William Candillon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-safe-area
|
||||
|
||||
This product contains 'react-native-safe-area' by Masayuki Iwai.
|
||||
@@ -2450,39 +2445,6 @@ limitations under the License.
|
||||
|
||||
---
|
||||
|
||||
## react-native-share
|
||||
|
||||
This product contains 'react-native-share' by react-native-share.
|
||||
|
||||
React Native Share, a simple tool for share message and file to other apps.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-share/react-native-share
|
||||
|
||||
* LICENSE: The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Esteban Fuentealba
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-slider
|
||||
|
||||
This product contains 'react-native-slider' by Jean Regisser.
|
||||
@@ -2578,6 +2540,41 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-v8
|
||||
|
||||
This product contains 'react-native-v8' by Kudo Chien.
|
||||
|
||||
Opt-in V8 runtime for React Native Android
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/Kudo/react-native-v8
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Kudo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-vector-icons
|
||||
|
||||
This product contains 'react-native-vector-icons' by Joel Arvidsson.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Mattermost Mobile
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (5.25)
|
||||
- **Minimum Server versions:** Current ESR version (5.19)
|
||||
- **Supported iOS versions:** 11+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
|
||||
@@ -101,14 +101,15 @@ if (System.getenv("SENTRY_ENABLED") == "true") {
|
||||
* 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
|
||||
def enableSeparateBuildPerCPUArchitecture = false
|
||||
|
||||
/**
|
||||
* Run Proguard to shrink the Java bytecode in release builds.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
def jscFlavor = 'org.webkit:android-jsc-intl:+'
|
||||
// Add v8-android - prebuilt libv8android.so into APK
|
||||
def jscFlavor = 'org.chromium:v8-android:+'
|
||||
|
||||
/**
|
||||
* Whether to enable the Hermes VM.
|
||||
@@ -132,11 +133,13 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||
versionCode 338
|
||||
versionName "1.38.1"
|
||||
versionCode 307
|
||||
versionName "1.32.2"
|
||||
multiDexEnabled = true
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
|
||||
}
|
||||
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
@@ -152,7 +155,7 @@ android {
|
||||
abi {
|
||||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk enableSeparateBuildPerCPUArchitecture // If true, also generate a universal APK
|
||||
universalApk false // If true, also generate a universal APK
|
||||
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
}
|
||||
@@ -181,7 +184,7 @@ android {
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
versionCodes.get(abi) * 1000000 + defaultConfig.versionCode
|
||||
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,6 +194,14 @@ android {
|
||||
targetCompatibility 1.8
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
// Make sure libjsc.so does not packed in APK
|
||||
exclude "**/libjsc.so"
|
||||
pickFirst "lib/armeabi-v7a/libc++_shared.so"
|
||||
pickFirst "lib/arm64-v8a/libc++_shared.so"
|
||||
pickFirst "lib/x86/libc++_shared.so"
|
||||
pickFirst "lib/x86_64/libc++_shared.so"
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -250,7 +261,7 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
|
||||
implementation project(':reactnativenotifications')
|
||||
implementation "com.google.firebase:firebase-messaging:$firebaseVersion"
|
||||
implementation 'com.google.firebase:firebase-messaging:17.3.4'
|
||||
|
||||
// For animated GIF support
|
||||
implementation 'com.facebook.fresco:fresco:2.0.0'
|
||||
@@ -258,8 +269,6 @@ dependencies {
|
||||
// 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:+')
|
||||
}
|
||||
|
||||
// Run this once to be able to run the application with BUCK
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.wix.detox.Detox;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class DetoxTest {
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
|
||||
|
||||
@Test
|
||||
public void runDetoxTests() {
|
||||
Detox.DetoxIdlePolicyConfig idlePolicyConfig = new Detox.DetoxIdlePolicyConfig();
|
||||
idlePolicyConfig.masterTimeoutSec = 60;
|
||||
idlePolicyConfig.idleResourceTimeoutSec = 30;
|
||||
|
||||
Detox.runTests(mActivityRule, idlePolicyConfig);
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,12 @@
|
||||
android:theme="@style/AppTheme"
|
||||
android:installLocation="auto"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:resizeableActivity="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
>
|
||||
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
|
||||
<meta-data android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
|
||||
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\"/>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
|
||||
BIN
android/app/src/main/assets/fonts/AntDesign.ttf
Normal file
BIN
android/app/src/main/assets/fonts/AntDesign.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Entypo.ttf
Normal file
BIN
android/app/src/main/assets/fonts/Entypo.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/EvilIcons.ttf
Normal file
BIN
android/app/src/main/assets/fonts/EvilIcons.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Feather.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Feather.ttf
Executable file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/FontAwesome.ttf
Normal file
BIN
android/app/src/main/assets/fonts/FontAwesome.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf
Normal file
BIN
android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf
Normal file
BIN
android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf
Normal file
BIN
android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Foundation.ttf
Normal file
BIN
android/app/src/main/assets/fonts/Foundation.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Ionicons.ttf
Normal file
BIN
android/app/src/main/assets/fonts/Ionicons.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf
Normal file
BIN
android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/MaterialIcons.ttf
Normal file
BIN
android/app/src/main/assets/fonts/MaterialIcons.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Octicons.ttf
Normal file
BIN
android/app/src/main/assets/fonts/Octicons.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/SimpleLineIcons.ttf
Normal file
BIN
android/app/src/main/assets/fonts/SimpleLineIcons.ttf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Zocial.ttf
Normal file
BIN
android/app/src/main/assets/fonts/Zocial.ttf
Normal file
Binary file not shown.
Binary file not shown.
@@ -50,7 +50,6 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
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 PUSH_TYPE_UPDATE_BADGE = "update_badge";
|
||||
|
||||
private NotificationChannel mHighImportanceChannel;
|
||||
@@ -164,7 +163,6 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
switch(type) {
|
||||
case PUSH_TYPE_MESSAGE:
|
||||
case PUSH_TYPE_SESSION:
|
||||
super.postNotification(notificationId);
|
||||
break;
|
||||
case PUSH_TYPE_CLEAR:
|
||||
@@ -172,19 +170,15 @@ public class CustomPushNotification extends PushNotification {
|
||||
break;
|
||||
}
|
||||
|
||||
if (mAppLifecycleFacade.isReactInitialized()) {
|
||||
notifyReceivedToJS();
|
||||
}
|
||||
notifyReceivedToJS();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpened() {
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
final String channelId = data.getString("channel_id");
|
||||
if (channelId != null) {
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
channelIdToNotification.remove(channelId);
|
||||
}
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
channelIdToNotification.remove(channelId);
|
||||
digestNotification();
|
||||
}
|
||||
|
||||
@@ -228,9 +222,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
|
||||
String channelId = bundle.getString("channel_id");
|
||||
if (channelId != null) {
|
||||
userInfoBundle.putString("channel_id", channelId);
|
||||
}
|
||||
userInfoBundle.putString("channel_id", channelId);
|
||||
|
||||
notification.addExtras(userInfoBundle);
|
||||
}
|
||||
@@ -300,11 +292,11 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
private Notification.MessagingStyle getMessagingStyle(Bundle bundle) {
|
||||
Notification.MessagingStyle messagingStyle;
|
||||
String senderId = bundle.getString("sender_id");
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || senderId == null) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messagingStyle = new Notification.MessagingStyle("");
|
||||
} else {
|
||||
String senderId = bundle.getString("sender_id");
|
||||
Person sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName("")
|
||||
@@ -364,7 +356,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
int bundleCount = bundleList.size() - 1;
|
||||
for (int i = bundleCount; i >= 0; i--) {
|
||||
Bundle data = bundleList.get(i);
|
||||
String message = data.getString("message", data.getString("body"));
|
||||
String message = data.getString("message");
|
||||
String senderId = data.getString("sender_id");
|
||||
if (senderId == null) {
|
||||
senderId = "sender_id";
|
||||
@@ -372,7 +364,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
Bundle userInfoBundle = data.getBundle("userInfo");
|
||||
String senderName = getSenderName(data);
|
||||
if (userInfoBundle != null) {
|
||||
boolean localPushNotificationTest = userInfoBundle.getBoolean("test");
|
||||
boolean localPushNotificationTest = userInfoBundle.getBoolean("localTest");
|
||||
if (localPushNotificationTest) {
|
||||
senderName = "Test";
|
||||
}
|
||||
@@ -403,15 +395,13 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
NotificationChannel notificationChannel = mHighImportanceChannel;
|
||||
|
||||
boolean testNotification = false;
|
||||
boolean localNotification = false;
|
||||
boolean localPushNotificationTest = false;
|
||||
Bundle userInfoBundle = bundle.getBundle("userInfo");
|
||||
if (userInfoBundle != null) {
|
||||
testNotification = userInfoBundle.getBoolean("test");
|
||||
localNotification = userInfoBundle.getBoolean("local");
|
||||
localPushNotificationTest = userInfoBundle.getBoolean("localTest");
|
||||
}
|
||||
|
||||
if (mAppLifecycleFacade.isAppVisible() && !testNotification && !localNotification) {
|
||||
if (mAppLifecycleFacade.isAppVisible() && !localPushNotificationTest) {
|
||||
notificationChannel = mMinImportanceChannel;
|
||||
}
|
||||
|
||||
@@ -469,8 +459,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
private void addNotificationReplyAction(Notification.Builder notification, int notificationId, Bundle bundle) {
|
||||
String postId = bundle.getString("post_id");
|
||||
|
||||
if (android.text.TextUtils.isEmpty(postId) || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
if (postId == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -536,12 +525,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
|
||||
private String removeSenderNameFromMessage(String message, String senderName) {
|
||||
Integer index = message.indexOf(senderName);
|
||||
if (index == 0) {
|
||||
message = message.substring(senderName.length());
|
||||
}
|
||||
|
||||
return message.replaceFirst(": ", "").trim();
|
||||
return message.replaceFirst(senderName, "").replaceFirst(": ", "").trim();
|
||||
}
|
||||
|
||||
private void notificationReceiptDelivery(String ackId, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
|
||||
|
||||
@@ -82,12 +82,7 @@ public class RNPasteableActionCallback implements ActionMode.Callback {
|
||||
return null;
|
||||
}
|
||||
|
||||
CharSequence chars = item.getText();
|
||||
if (chars == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String text = chars.toString();
|
||||
String text = item.getText().toString();
|
||||
if (text.length() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -106,27 +106,8 @@ public class ReceiptDelivery {
|
||||
Response response = client.newCall(request).execute();
|
||||
String responseBody = response.body().string();
|
||||
if (response.code() != 200) {
|
||||
switch (response.code()) {
|
||||
case 302:
|
||||
promise.reject("Receipt delivery failure", "StatusFound");
|
||||
return;
|
||||
case 400:
|
||||
promise.reject("Receipt delivery failure", "StatusBadRequest");
|
||||
return;
|
||||
case 401:
|
||||
promise.reject("Receipt delivery failure", "Unauthorized");
|
||||
return;
|
||||
case 500:
|
||||
promise.reject("Receipt delivery failure", "StatusInternalServerError");
|
||||
return;
|
||||
case 501:
|
||||
promise.reject("Receipt delivery failure", "StatusNotImplemented");
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Exception(responseBody);
|
||||
}
|
||||
|
||||
JSONObject jsonResponse = new JSONObject(responseBody);
|
||||
Bundle bundle = new Bundle();
|
||||
String keys[] = new String[]{"post_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
</resources>
|
||||
@@ -14,8 +14,6 @@
|
||||
<string name="allowOtherServers_description">Allow the user to change the above server URL.</string>
|
||||
<string name="username_title">Default Username</string>
|
||||
<string name="username_description">Set the username or email address to use to authenticate against the Mattermost Server.</string>
|
||||
<string name="timeout_title">Default Request Timeout</string>
|
||||
<string name="timeout_description">How long in milliseconds the mobile app should wait for the server to respond.</string>
|
||||
<string name="vendor_title">EMM Vendor or Company Name</string>
|
||||
<string name="vendor_description">Name of the EMM vendor or company deploying the app. Used in help text when prompting for passcodes so users are aware why the app is being protected.</string>
|
||||
</resources>
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -43,12 +43,6 @@
|
||||
android:description="@string/username_description"
|
||||
android:restrictionType="string"
|
||||
android:defaultValue="" />
|
||||
<restriction
|
||||
android:key="timeout"
|
||||
android:title="@string/timeout_title"
|
||||
android:description="@string/timeout_description"
|
||||
android:restrictionType="string"
|
||||
android:defaultValue="10000" />
|
||||
<restriction
|
||||
android:key="vendor"
|
||||
android:title="@string/vendor_title"
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "29.0.2"
|
||||
buildToolsVersion = "28.0.3"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
compileSdkVersion = 28
|
||||
targetSdkVersion = 28
|
||||
supportLibVersion = "28.0.0"
|
||||
kotlinVersion = "1.3.61"
|
||||
firebaseVersion = "21.0.0"
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
|
||||
}
|
||||
@@ -19,9 +18,9 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath 'com.android.tools.build:gradle:3.5.2'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
@@ -49,17 +48,17 @@ allprojects {
|
||||
jcenter()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url("$rootDir/../node_modules/react-native/android")
|
||||
// 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")
|
||||
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")
|
||||
// url "$rootDir/../node_modules/jsc-android/dist"
|
||||
|
||||
// prebuilt libv8android.so
|
||||
// url("$rootDir/../node_modules/v8-android/dist")
|
||||
url("$rootDir/../node_modules/v8-android/dist")
|
||||
}
|
||||
maven {
|
||||
url "https://www.jitpack.io"
|
||||
@@ -67,8 +66,5 @@ allprojects {
|
||||
maven {
|
||||
url ("https://dl.bintray.com/rudderstack/rudderstack")
|
||||
}
|
||||
maven {
|
||||
url "$rootDir/../node_modules/detox/Detox-android"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,4 +30,4 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.37.0
|
||||
FLIPPER_VERSION=0.33.1
|
||||
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
@@ -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-6.5-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
|
||||
|
||||
29
android/gradlew
vendored
29
android/gradlew
vendored
@@ -154,19 +154,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -175,9 +175,14 @@ save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
27
android/gradlew.bat
vendored
27
android/gradlew.bat
vendored
@@ -13,91 +13,64 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -1,5 +1,5 @@
|
||||
rootProject.name = 'Mattermost'
|
||||
include ':reactnativenotifications'
|
||||
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/lib/android/app')
|
||||
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android/app')
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
include ':app'
|
||||
|
||||
@@ -6,7 +6,7 @@ import {DeviceTypes} from 'app/constants';
|
||||
export function connection(isOnline) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
if (isOnline !== undefined && isOnline !== state.device.connection) {
|
||||
if (isOnline !== undefined && isOnline !== state.device.connection) { //eslint-disable-line no-undefined
|
||||
dispatch({
|
||||
type: DeviceTypes.CONNECTION_CHANGED,
|
||||
data: isOnline,
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
import {ChannelTypes, PreferenceTypes, RoleTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Preferences} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId, getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getMyPreferences} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUsers, getUserIdsInChannels} from '@mm-redux/selectors/entities/users';
|
||||
import {getChannelByName as selectChannelByName, getUserIdFromChannelName, isAutoClosed} from '@mm-redux/utils/channel_utils';
|
||||
import {getUserIdFromChannelName, isAutoClosed} from '@mm-redux/utils/channel_utils';
|
||||
import {getPreferenceKey} from '@mm-redux/utils/preference_utils';
|
||||
|
||||
import {ActionResult, GenericAction} from '@mm-redux/types/actions';
|
||||
@@ -281,43 +281,6 @@ export async function getAddedDmUsersIfNecessary(state: GlobalState, preferences
|
||||
return actions;
|
||||
}
|
||||
|
||||
export function lastChannelIdForTeam(state: GlobalState, teamId: string): string {
|
||||
const {channels, myMembers} = state.entities.channels;
|
||||
const {currentUserId} = state.entities.users;
|
||||
const {myPreferences} = state.entities.preferences;
|
||||
const lastChannelForTeam = state.views.team.lastChannelForTeam[teamId];
|
||||
const lastChannelId = lastChannelForTeam && lastChannelForTeam.length ? lastChannelForTeam[0] : '';
|
||||
const lastChannel = channels[lastChannelId];
|
||||
|
||||
const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL &&
|
||||
isDirectChannelVisible(currentUserId, myPreferences, lastChannel);
|
||||
|
||||
const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL &&
|
||||
isGroupChannelVisible(myPreferences, lastChannel);
|
||||
|
||||
if (
|
||||
myMembers[lastChannelId] &&
|
||||
lastChannel &&
|
||||
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)
|
||||
) {
|
||||
return lastChannelId;
|
||||
}
|
||||
|
||||
// Fallback to default channel
|
||||
const channelsInTeam = getChannelsNameMapInTeam(state, teamId);
|
||||
const channel = selectChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
|
||||
|
||||
if (channel) {
|
||||
return channel.id;
|
||||
}
|
||||
|
||||
// Handle case when the default channel cannot be found
|
||||
// so we need to get the first available channel of the team
|
||||
const teamChannels = Object.values(channelsInTeam);
|
||||
const firstChannel = teamChannels.length ? teamChannels[0].id : '';
|
||||
return firstChannel;
|
||||
}
|
||||
|
||||
function fetchDirectMessageProfileIfNeeded(state: GlobalState, channel: Channel, channelMembers: Array<ChannelMembership>, profilesInChannel: Array<string>) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const myPreferences = getMyPreferences(state);
|
||||
|
||||
@@ -7,17 +7,11 @@ import merge from 'deepmerge';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import EventEmmiter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes, NavigationTypes} from '@constants';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import Store from '@store/store';
|
||||
|
||||
Navigation.setDefaultOptions({
|
||||
layout: {
|
||||
orientation: [DeviceTypes.IS_TABLET ? 'all' : 'portrait'],
|
||||
},
|
||||
});
|
||||
const CHANNEL_SCREEN = 'Channel';
|
||||
|
||||
function getThemeFromState() {
|
||||
const state = Store.redux?.getState() || {};
|
||||
@@ -33,8 +27,8 @@ export function resetToChannel(passProps = {}) {
|
||||
const stack = {
|
||||
children: [{
|
||||
component: {
|
||||
id: NavigationTypes.CHANNEL_SCREEN,
|
||||
name: NavigationTypes.CHANNEL_SCREEN,
|
||||
id: CHANNEL_SCREEN,
|
||||
name: CHANNEL_SCREEN,
|
||||
passProps,
|
||||
options: {
|
||||
layout: {
|
||||
@@ -92,8 +86,6 @@ export function resetToChannel(passProps = {}) {
|
||||
export function resetToSelectServer(allowOtherServers) {
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
EphemeralStore.clearNavigationComponents();
|
||||
|
||||
Navigation.setRoot({
|
||||
root: {
|
||||
stack: {
|
||||
@@ -156,8 +148,6 @@ export function resetToTeams(name, title, passProps = {}, options = {}) {
|
||||
},
|
||||
};
|
||||
|
||||
EphemeralStore.clearNavigationComponents();
|
||||
|
||||
Navigation.setRoot({
|
||||
root: {
|
||||
stack: {
|
||||
@@ -192,7 +182,6 @@ export function goToScreen(name, title, passProps = {}, options = {}) {
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
testID: 'screen.back.button',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
@@ -234,17 +223,10 @@ export async function popToRoot() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function dismissAllModalsAndPopToRoot() {
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
|
||||
EventEmmiter.emit(NavigationTypes.NAVIGATION_DISMISS_AND_POP_TO_ROOT);
|
||||
}
|
||||
|
||||
export function showModal(name, title, passProps = {}, options = {}) {
|
||||
const theme = getThemeFromState();
|
||||
const defaultOptions = {
|
||||
modalPresentationStyle: Platform.select({ios: 'pageSheet', android: 'none'}),
|
||||
modalPresentationStyle: Platform.select({ios: 'fullScreen', android: 'none'}),
|
||||
layout: {
|
||||
componentBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
@@ -270,7 +252,6 @@ export function showModal(name, title, passProps = {}, options = {}) {
|
||||
},
|
||||
};
|
||||
|
||||
EphemeralStore.addNavigationModal(name);
|
||||
Navigation.showModal({
|
||||
stack: {
|
||||
children: [{
|
||||
@@ -332,26 +313,16 @@ export function showSearchModal(initialValue = '') {
|
||||
visible: false,
|
||||
height: 0,
|
||||
},
|
||||
...Platform.select({
|
||||
ios: {
|
||||
modalPresentationStyle: 'pageSheet',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
showModal(name, title, passProps, options);
|
||||
}
|
||||
|
||||
export async function dismissModal(options = {}) {
|
||||
if (!EphemeralStore.hasModalsOpened()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
try {
|
||||
await Navigation.dismissModal(componentId, options);
|
||||
EphemeralStore.removeNavigationModal(componentId);
|
||||
} catch (error) {
|
||||
// RNN returns a promise rejection if there is no modal to
|
||||
// dismiss. We'll do nothing in this case.
|
||||
@@ -359,13 +330,8 @@ export async function dismissModal(options = {}) {
|
||||
}
|
||||
|
||||
export async function dismissAllModals(options = {}) {
|
||||
if (!EphemeralStore.hasModalsOpened()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Navigation.dismissAllModals(options);
|
||||
EphemeralStore.clearNavigationModals();
|
||||
} catch (error) {
|
||||
// RNN returns a promise rejection if there are no modals to
|
||||
// dismiss. We'll do nothing in this case.
|
||||
@@ -436,7 +402,7 @@ export function closeMainSideMenu() {
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(NavigationTypes.CHANNEL_SCREEN, {
|
||||
Navigation.mergeOptions(CHANNEL_SCREEN, {
|
||||
sideMenu: {
|
||||
left: {visible: false},
|
||||
},
|
||||
@@ -448,7 +414,7 @@ export function enableMainSideMenu(enabled, visible = true) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigation.mergeOptions(NavigationTypes.CHANNEL_SCREEN, {
|
||||
Navigation.mergeOptions(CHANNEL_SCREEN, {
|
||||
sideMenu: {
|
||||
left: {enabled, visible},
|
||||
},
|
||||
@@ -461,7 +427,7 @@ export function openSettingsSideMenu() {
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(NavigationTypes.CHANNEL_SCREEN, {
|
||||
Navigation.mergeOptions(CHANNEL_SCREEN, {
|
||||
sideMenu: {
|
||||
right: {visible: true},
|
||||
},
|
||||
@@ -474,7 +440,7 @@ export function closeSettingsSideMenu() {
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(NavigationTypes.CHANNEL_SCREEN, {
|
||||
Navigation.mergeOptions(CHANNEL_SCREEN, {
|
||||
sideMenu: {
|
||||
right: {visible: false},
|
||||
},
|
||||
|
||||
@@ -7,21 +7,16 @@ import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import merge from 'deepmerge';
|
||||
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import * as NavigationActions from '@actions/navigation';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import intitialState from '@store/initial_state';
|
||||
import Store from '@store/store';
|
||||
import {NavigationTypes} from '@constants';
|
||||
|
||||
jest.unmock('@actions/navigation');
|
||||
jest.mock('@store/ephemeral_store', () => ({
|
||||
getNavigationTopComponentId: jest.fn(),
|
||||
clearNavigationComponents: jest.fn(),
|
||||
addNavigationModal: jest.fn(),
|
||||
hasModalsOpened: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
@@ -188,7 +183,6 @@ describe('@actions/navigation', () => {
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
testID: 'screen.back.button',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
@@ -235,7 +229,7 @@ describe('@actions/navigation', () => {
|
||||
const showModal = jest.spyOn(Navigation, 'showModal');
|
||||
|
||||
const defaultOptions = {
|
||||
modalPresentationStyle: Platform.select({ios: 'pageSheet', android: 'none'}),
|
||||
modalPresentationStyle: Platform.select({ios: 'fullScreen', android: 'none'}),
|
||||
layout: {
|
||||
componentBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
@@ -372,7 +366,7 @@ describe('@actions/navigation', () => {
|
||||
},
|
||||
};
|
||||
const defaultOptions = {
|
||||
modalPresentationStyle: Platform.select({ios: 'pageSheet', android: 'none'}),
|
||||
modalPresentationStyle: Platform.select({ios: 'fullScreen', android: 'none'}),
|
||||
layout: {
|
||||
componentBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
@@ -484,15 +478,4 @@ describe('@actions/navigation', () => {
|
||||
await NavigationActions.dismissOverlay(topComponentId);
|
||||
expect(dismissOverlay).toHaveBeenCalledWith(topComponentId);
|
||||
});
|
||||
|
||||
test('dismissAllModalsAndPopToRoot should call Navigation.dismissAllModals, Navigation.popToRoot, and emit event', async () => {
|
||||
const dismissAllModals = jest.spyOn(Navigation, 'dismissAllModals');
|
||||
const popToRoot = jest.spyOn(Navigation, 'popToRoot');
|
||||
EventEmitter.emit = jest.fn();
|
||||
|
||||
await NavigationActions.dismissAllModalsAndPopToRoot();
|
||||
expect(dismissAllModals).toHaveBeenCalled();
|
||||
expect(popToRoot).toHaveBeenCalledWith(topComponentId);
|
||||
expect(EventEmitter.emit).toHaveBeenCalledWith(NavigationTypes.NAVIGATION_DISMISS_AND_POP_TO_ROOT);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,35 +5,32 @@ import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
import {ChannelTypes, RoleTypes, GroupTypes} from '@mm-redux/action_types';
|
||||
import {ChannelTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {
|
||||
fetchMyChannelsAndMembers,
|
||||
getChannelByNameAndTeamName,
|
||||
joinChannel,
|
||||
leaveChannel as serviceLeaveChannel,
|
||||
} from '@mm-redux/actions/channels';
|
||||
import {getFilesForPost} from '@mm-redux/actions/files';
|
||||
import {savePreferences} from '@mm-redux/actions/preferences';
|
||||
import {getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {selectTeam} from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Preferences} from '@mm-redux/constants';
|
||||
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {
|
||||
getCurrentChannelId,
|
||||
getRedirectChannelNameForTeam,
|
||||
getChannelsNameMapInTeam,
|
||||
getMyChannelMemberships,
|
||||
isManuallyUnread,
|
||||
} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {getTeamByName, getCurrentTeam} from '@mm-redux/selectors/entities/teams';
|
||||
import {getTeamByName} from '@mm-redux/selectors/entities/teams';
|
||||
|
||||
import {getChannelByName as selectChannelByName, getChannelsIdForTeam} from '@mm-redux/utils/channel_utils';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {lastChannelIdForTeam, loadSidebarDirectMessagesProfiles} from '@actions/helpers/channels';
|
||||
import {getPosts, getPostsBefore, getPostsSince, loadUnreadChannelPosts} from '@actions/views/post';
|
||||
import {loadSidebarDirectMessagesProfiles} from '@actions/helpers/channels';
|
||||
import {getPosts, getPostsBefore, getPostsSince, getPostThread, loadUnreadChannelPosts} from '@actions/views/post';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
|
||||
import {getChannelReachable} from '@selectors/channel';
|
||||
import telemetry from '@telemetry';
|
||||
@@ -46,21 +43,15 @@ export function loadChannelsByTeamName(teamName, errorHandler) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentTeamId} = state.entities.teams;
|
||||
const team = getTeamByName(state, teamName);
|
||||
|
||||
if (teamName) {
|
||||
const team = getTeamByName(state, teamName);
|
||||
|
||||
if (!team && errorHandler) {
|
||||
errorHandler();
|
||||
return {error: true};
|
||||
}
|
||||
|
||||
if (team && team.id !== currentTeamId) {
|
||||
await dispatch(fetchMyChannelsAndMembers(team.id));
|
||||
}
|
||||
if (!team && errorHandler) {
|
||||
errorHandler();
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
if (team && team.id !== currentTeamId) {
|
||||
await dispatch(fetchMyChannelsAndMembers(team.id));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,12 +110,55 @@ export function fetchPostActionWithRetry(action, maxTries = MAX_RETRIES) {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadFilesForPostIfNecessary(postId) {
|
||||
return async (dispatch, getState) => {
|
||||
const {files} = getState().entities;
|
||||
const fileIdsForPost = files.fileIdsByPostId[postId];
|
||||
|
||||
if (!fileIdsForPost?.length) {
|
||||
await dispatch(getFilesForPost(postId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadThreadIfNecessary(rootId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {posts, postsInThread} = state.entities.posts;
|
||||
const threadPosts = postsInThread[rootId];
|
||||
|
||||
if (!posts[rootId] || !threadPosts) {
|
||||
dispatch(getPostThread(rootId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function selectInitialChannel(teamId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const channelId = lastChannelIdForTeam(state, teamId);
|
||||
const {channels, myMembers} = state.entities.channels;
|
||||
const {currentUserId} = state.entities.users;
|
||||
const {myPreferences} = state.entities.preferences;
|
||||
const lastChannelForTeam = state.views.team.lastChannelForTeam[teamId];
|
||||
const lastChannelId = lastChannelForTeam && lastChannelForTeam.length ? lastChannelForTeam[0] : '';
|
||||
const lastChannel = channels[lastChannelId];
|
||||
|
||||
dispatch(handleSelectChannel(channelId));
|
||||
const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL &&
|
||||
isDirectChannelVisible(currentUserId, myPreferences, lastChannel);
|
||||
|
||||
const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL &&
|
||||
isGroupChannelVisible(myPreferences, lastChannel);
|
||||
|
||||
if (
|
||||
myMembers[lastChannelId] &&
|
||||
lastChannel &&
|
||||
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)
|
||||
) {
|
||||
dispatch(handleSelectChannel(lastChannelId));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(selectDefaultChannel(teamId));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -218,15 +252,13 @@ export function handleSelectChannel(channelId) {
|
||||
|
||||
export function handleSelectChannelByName(channelName, teamName, errorHandler) {
|
||||
return async (dispatch, getState) => {
|
||||
let state = getState();
|
||||
const state = getState();
|
||||
const {teams: currentTeams, currentTeamId} = state.entities.teams;
|
||||
const currentTeam = currentTeams[currentTeamId];
|
||||
const currentTeamName = currentTeam?.name;
|
||||
const response = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName));
|
||||
const {error, data: channel} = response;
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
state = getState();
|
||||
const reachable = getChannelReachable(state, channelName, teamName);
|
||||
|
||||
if (!reachable && errorHandler) {
|
||||
@@ -244,17 +276,6 @@ export function handleSelectChannelByName(channelName, teamName, errorHandler) {
|
||||
}
|
||||
|
||||
if (channel && currentChannelId !== channel.id) {
|
||||
if (channel.type === General.OPEN_CHANNEL) {
|
||||
const myMemberships = getMyChannelMemberships(state);
|
||||
if (!myMemberships[channel.id]) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
console.log('joining channel', channel?.display_name, channel.id); //eslint-disable-line
|
||||
const result = await dispatch(joinChannel(currentUserId, teamName, channel.id));
|
||||
if (result.error || !result.data || !result.data.channel) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch(handleSelectChannel(channel.id));
|
||||
}
|
||||
|
||||
@@ -293,8 +314,6 @@ export function markChannelViewedAndRead(channelId, previousChannelId, markOnSer
|
||||
const actions = markAsViewedAndReadBatch(state, channelId, previousChannelId, markOnServer);
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_MARK_CHANNEL_VIEWED_AND_READ'));
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -327,19 +346,12 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
|
||||
}
|
||||
|
||||
if (channel) {
|
||||
const unreadMessageCount = channel.total_msg_count - member.msg_count;
|
||||
actions.push({
|
||||
type: ChannelTypes.SET_UNREAD_MSG_COUNT,
|
||||
data: {
|
||||
channelId,
|
||||
count: unreadMessageCount,
|
||||
},
|
||||
}, {
|
||||
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
|
||||
data: {
|
||||
teamId: channel.team_id,
|
||||
channelId,
|
||||
amount: unreadMessageCount,
|
||||
amount: channel.total_msg_count - member.msg_count,
|
||||
},
|
||||
}, {
|
||||
type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
|
||||
@@ -592,87 +604,6 @@ function setLoadMorePostsVisible(visible) {
|
||||
};
|
||||
}
|
||||
|
||||
function loadGroupData() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const actions = [];
|
||||
const team = getCurrentTeam(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const serverVersion = state.entities.general.serverVersion;
|
||||
const license = getLicense(state);
|
||||
const hasLicense = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
|
||||
|
||||
if (hasLicense && team && isMinimumServerVersion(serverVersion, 5, 24)) {
|
||||
for (let i = 0; i <= MAX_RETRIES; i++) {
|
||||
try {
|
||||
if (team.group_constrained) {
|
||||
const [getAllGroupsAssociatedToChannelsInTeam, getAllGroupsAssociatedToTeam] = await Promise.all([ //eslint-disable-line no-await-in-loop
|
||||
Client4.getAllGroupsAssociatedToChannelsInTeam(team.id, true),
|
||||
Client4.getAllGroupsAssociatedToTeam(team.id, true),
|
||||
]);
|
||||
|
||||
if (getAllGroupsAssociatedToChannelsInTeam.groups) {
|
||||
actions.push({
|
||||
type: GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNELS_IN_TEAM,
|
||||
data: {groupsByChannelId: getAllGroupsAssociatedToChannelsInTeam.groups},
|
||||
});
|
||||
}
|
||||
|
||||
if (getAllGroupsAssociatedToTeam) {
|
||||
actions.push({
|
||||
type: GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_TEAM,
|
||||
data: {...getAllGroupsAssociatedToTeam, teamID: team.id},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const [getAllGroupsAssociatedToChannelsInTeam, getGroups] = await Promise.all([ //eslint-disable-line no-await-in-loop
|
||||
Client4.getAllGroupsAssociatedToChannelsInTeam(team.id, true),
|
||||
Client4.getGroups(true, 0, 0),
|
||||
]);
|
||||
|
||||
if (getAllGroupsAssociatedToChannelsInTeam.groups) {
|
||||
actions.push({
|
||||
type: GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNELS_IN_TEAM,
|
||||
data: {groupsByChannelId: getAllGroupsAssociatedToChannelsInTeam.groups},
|
||||
});
|
||||
}
|
||||
|
||||
if (getGroups) {
|
||||
actions.push({
|
||||
type: GroupTypes.RECEIVED_GROUPS,
|
||||
data: getGroups,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
} catch (err) {
|
||||
if (i === MAX_RETRIES) {
|
||||
return {error: err};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const myGroups = await Client4.getGroupsByUserId(currentUserId);
|
||||
if (myGroups.length) {
|
||||
actions.push({
|
||||
type: GroupTypes.RECEIVED_MY_GROUPS,
|
||||
data: myGroups,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions, 'BATCH_GROUP_DATA'));
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function loadChannelsForTeam(teamId, skipDispatch = false) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@@ -743,15 +674,13 @@ export function loadChannelsForTeam(teamId, skipDispatch = false) {
|
||||
|
||||
dispatch(loadUnreadChannelPosts(data.channels, data.channelMembers));
|
||||
}
|
||||
|
||||
dispatch(loadGroupData());
|
||||
}
|
||||
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
export function loadSidebar(data) {
|
||||
function loadSidebar(data) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {channels, channelMembers} = data;
|
||||
@@ -760,19 +689,5 @@ export function loadSidebar(data) {
|
||||
if (sidebarActions.length) {
|
||||
dispatch(batchActions(sidebarActions, 'BATCH_LOAD_SIDEBAR'));
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function resetUnreadMessageCount(channelId) {
|
||||
return async (dispatch) => {
|
||||
dispatch({
|
||||
type: ChannelTypes.SET_UNREAD_MSG_COUNT,
|
||||
data: {
|
||||
channelId,
|
||||
count: 0,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {setAppCredentials} from 'app/init/credentials';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import {getDeviceTimezone} from 'app/utils/timezone';
|
||||
import {setCSRFFromCookie} from 'app/utils/security';
|
||||
import {loadConfigAndLicense} from 'app/actions/views/root';
|
||||
@@ -94,11 +94,11 @@ export function scheduleExpiredNotification(intl) {
|
||||
});
|
||||
|
||||
if (expiresAt) {
|
||||
PushNotifications.scheduleNotification({
|
||||
fireDate: expiresAt,
|
||||
body: message,
|
||||
PushNotifications.localNotificationSchedule({
|
||||
date: new Date(expiresAt),
|
||||
message,
|
||||
userInfo: {
|
||||
local: true,
|
||||
localNotification: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Keyboard} from 'react-native';
|
||||
|
||||
import {showModalOverCurrentContext} from '@actions/navigation';
|
||||
import {loadChannelsByTeamName} from '@actions/views/channel';
|
||||
import {selectFocusedPostId} from '@mm-redux/actions/posts';
|
||||
import type {DispatchFunc} from '@mm-redux/types/actions';
|
||||
import {permalinkBadTeam} from '@utils/general';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
export let showingPermalink = false;
|
||||
|
||||
export function showPermalink(intl: typeof intlShape, teamName: string, postId: string, openAsPermalink = true) {
|
||||
return async (dispatch: DispatchFunc) => {
|
||||
const loadTeam = await dispatch(loadChannelsByTeamName(teamName, permalinkBadTeam.bind(null, intl)));
|
||||
|
||||
if (!loadTeam.error) {
|
||||
Keyboard.dismiss();
|
||||
dispatch(selectFocusedPostId(postId));
|
||||
|
||||
if (!showingPermalink) {
|
||||
const screen = 'Permalink';
|
||||
const passProps = {
|
||||
isPermalink: openAsPermalink,
|
||||
onClose: () => {
|
||||
dispatch(closePermalink());
|
||||
},
|
||||
};
|
||||
|
||||
const options = {
|
||||
layout: {
|
||||
componentBackgroundColor: changeOpacity('#000', 0.2),
|
||||
},
|
||||
};
|
||||
|
||||
showingPermalink = true;
|
||||
showModalOverCurrentContext(screen, passProps, options);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function closePermalink() {
|
||||
return async (dispatch: DispatchFunc) => {
|
||||
showingPermalink = false;
|
||||
return dispatch(selectFocusedPostId(''));
|
||||
};
|
||||
}
|
||||
@@ -4,19 +4,20 @@
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {NavigationTypes, ViewTypes} from '@constants';
|
||||
import {analytics} from '@init/analytics.ts';
|
||||
import {recordTime} from '@init/analytics.ts';
|
||||
import {ChannelTypes, GeneralTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {fetchMyChannelsAndMembers, getChannelAndMyMember} from '@mm-redux/actions/channels';
|
||||
import {fetchMyChannelsAndMembers} from '@mm-redux/actions/channels';
|
||||
import {getDataRetentionPolicy} from '@mm-redux/actions/general';
|
||||
import {receivedNewPost} from '@mm-redux/actions/posts';
|
||||
import {getMyTeams, getMyTeamMembers} from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import initialState from '@store/initial_state';
|
||||
import {getStateForReset} from '@store/utils';
|
||||
|
||||
import {markAsViewedAndReadBatch} from './channel';
|
||||
import {markChannelViewedAndRead} from './channel';
|
||||
|
||||
export function startDataCleanup() {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -66,17 +67,17 @@ export function loadConfigAndLicense() {
|
||||
export function loadFromPushNotification(notification) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {payload} = notification;
|
||||
const {data} = notification;
|
||||
const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams;
|
||||
const {channels} = state.entities.channels;
|
||||
|
||||
let channelId = '';
|
||||
let teamId = currentTeamId;
|
||||
if (payload) {
|
||||
channelId = payload.channel_id;
|
||||
if (data) {
|
||||
channelId = data.channel_id;
|
||||
|
||||
// when the notification does not have a team id is because its from a DM or GM
|
||||
teamId = payload.team_id || currentTeamId;
|
||||
teamId = data.team_id || currentTeamId;
|
||||
}
|
||||
|
||||
// load any missing data
|
||||
@@ -96,22 +97,18 @@ export function loadFromPushNotification(notification) {
|
||||
}
|
||||
|
||||
dispatch(handleSelectTeamAndChannel(teamId, channelId));
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleSelectTeamAndChannel(teamId, channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const dt = Date.now();
|
||||
await dispatch(getChannelAndMyMember(channelId));
|
||||
|
||||
const state = getState();
|
||||
const {channels, currentChannelId, myMembers} = state.entities.channels;
|
||||
const {currentTeamId} = state.entities.teams;
|
||||
const channel = channels[channelId];
|
||||
const member = myMembers[channelId];
|
||||
const actions = markAsViewedAndReadBatch(state, channelId);
|
||||
const actions = [];
|
||||
|
||||
// when the notification is from a team other than the current team
|
||||
if (teamId !== currentTeamId) {
|
||||
@@ -128,14 +125,17 @@ export function handleSelectTeamAndChannel(teamId, channelId) {
|
||||
teamId: channel.team_id || currentTeamId,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch(markChannelViewedAndRead(channelId));
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions, 'BATCH_SELECT_TEAM_AND_CHANNEL'));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('channel switch from push notification to', channel?.display_name || channel?.id, (Date.now() - dt), 'ms');
|
||||
EphemeralStore.setStartFromNotification(false);
|
||||
|
||||
console.log('channel switch from push notification to', channel?.display_name, (Date.now() - dt), 'ms'); //eslint-disable-line
|
||||
};
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ export function recordLoadTime(screenName, category) {
|
||||
return async (dispatch, getState) => {
|
||||
const {currentUserId} = getState().entities.users;
|
||||
|
||||
analytics.recordTime(screenName, category, currentUserId);
|
||||
recordTime(screenName, category, currentUserId);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {lastChannelIdForTeam} from '@actions/helpers/channels';
|
||||
import {NavigationTypes} from '@constants';
|
||||
import {ChannelTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {getMyTeams} from '@mm-redux/actions/teams';
|
||||
import {RequestStatus} from '@mm-redux/constants';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {selectFirstAvailableTeam} from '@utils/teams';
|
||||
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {selectFirstAvailableTeam} from 'app/utils/teams';
|
||||
|
||||
export function handleTeamChange(teamId) {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -20,28 +20,10 @@ export function handleTeamChange(teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actions = [{type: TeamTypes.SELECT_TEAM, data: teamId}];
|
||||
const {channels, myMembers} = state.entities.channels;
|
||||
const channelId = lastChannelIdForTeam(state, teamId);
|
||||
|
||||
if (channelId) {
|
||||
const channel = channels[channelId];
|
||||
const member = myMembers[channelId];
|
||||
|
||||
actions.push({
|
||||
type: ChannelTypes.SELECT_CHANNEL,
|
||||
data: channelId,
|
||||
extra: {
|
||||
channel,
|
||||
member,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: '', extra: {}});
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_SWITCH_TEAM'));
|
||||
dispatch(batchActions([
|
||||
{type: TeamTypes.SELECT_TEAM, data: teamId},
|
||||
{type: ChannelTypes.SELECT_CHANNEL, data: '', extra: {}},
|
||||
], 'BATCH_SWITCH_TEAM'));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {Client4} from '@mm-redux/client';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
|
||||
const sortByNewest = (a, b) => {
|
||||
if (a.create_at > b.create_at) {
|
||||
@@ -56,11 +56,11 @@ export function scheduleExpiredNotification(intl) {
|
||||
if (expiresAt) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Schedule Session Expiry Local Push Notification', expiresAt);
|
||||
PushNotifications.scheduleNotification({
|
||||
fireDate: expiresAt,
|
||||
body: message,
|
||||
PushNotifications.localNotificationSchedule({
|
||||
date: new Date(expiresAt),
|
||||
message,
|
||||
userInfo: {
|
||||
local: true,
|
||||
localNotification: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {getCurrentUserId, getStatusForUserId} from '@mm-redux/selectors/entities
|
||||
import {setAppCredentials} from 'app/init/credentials';
|
||||
import {setCSRFFromCookie} from '@utils/security';
|
||||
import {getDeviceTimezone} from '@utils/timezone';
|
||||
import {analytics} from '@init/analytics.ts';
|
||||
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
@@ -40,8 +39,8 @@ export function completeLogin(user, deviceToken) {
|
||||
}
|
||||
|
||||
// Data retention
|
||||
if (config?.DataRetentionEnableMessageDeletion && config?.DataRetentionEnableMessageDeletion === 'true' &&
|
||||
license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
|
||||
license.IsLicensed === 'true' && license.DataRetention === 'true') {
|
||||
dispatch(getDataRetentionPolicy());
|
||||
} else {
|
||||
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
@@ -97,8 +96,8 @@ export function loadMe(user, deviceToken, skipDispatch = false) {
|
||||
}
|
||||
|
||||
try {
|
||||
analytics.setUserId(data.user.id);
|
||||
analytics.setUserRoles(data.user.roles);
|
||||
Client4.setUserId(data.user.id);
|
||||
Client4.setUserRoles(data.user.roles);
|
||||
|
||||
// Execute all other requests in parallel
|
||||
const teamsRequest = Client4.getMyTeams();
|
||||
@@ -183,10 +182,14 @@ export function login(loginId, password, mfaToken, ldapOnly = false) {
|
||||
};
|
||||
}
|
||||
|
||||
export function ssoLogin() {
|
||||
export function ssoLogin(token) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const deviceToken = state.entities?.general?.deviceToken;
|
||||
|
||||
Client4.setToken(token);
|
||||
await setCSRFFromCookie(Client4.getUrl());
|
||||
|
||||
const result = await dispatch(loadMe());
|
||||
|
||||
if (!result.error) {
|
||||
|
||||
1094
app/actions/websocket.test.js
Normal file
1094
app/actions/websocket.test.js
Normal file
File diff suppressed because it is too large
Load Diff
1181
app/actions/websocket.ts
Normal file
1181
app/actions/websocket.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,179 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-import-assign */
|
||||
|
||||
import assert from 'assert';
|
||||
import nock from 'nock';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import thunk from 'redux-thunk';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
|
||||
import {ChannelTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import * as ChannelActions from '@mm-redux/actions/channels';
|
||||
import * as TeamActions from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import globalInitialState from '@store/initial_state';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket Chanel Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('Websocket Handle Channel Member Updated', async () => {
|
||||
const channelMember = TestHelper.basicChannelMember;
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const st = mockStore(globalInitialState);
|
||||
await st.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
channelMember.roles = 'channel_user channel_admin';
|
||||
const rolesToLoad = channelMember.roles.split(' ');
|
||||
|
||||
nock(Client4.getRolesRoute()).
|
||||
post('/names', JSON.stringify(rolesToLoad)).
|
||||
reply(200, rolesToLoad);
|
||||
|
||||
mockServer.emit('message', JSON.stringify({
|
||||
event: WebsocketEvents.CHANNEL_MEMBER_UPDATED,
|
||||
data: {
|
||||
channelMember: JSON.stringify(channelMember),
|
||||
},
|
||||
}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
const storeActions = st.getActions();
|
||||
const batch = storeActions.find((a) => a.type === 'BATCH_WS_CHANNEL_MEMBER_UPDATE');
|
||||
expect(batch).not.toBeNull();
|
||||
const memberAction = batch.payload.find((a) => a.type === ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER);
|
||||
expect(memberAction).not.toBeNull();
|
||||
const rolesActions = batch.payload.find((a) => a.type === RoleTypes.RECEIVED_ROLES);
|
||||
expect(rolesActions).not.toBeNull();
|
||||
expect(rolesActions.data).toEqual(rolesToLoad);
|
||||
});
|
||||
|
||||
it('Websocket Handle Channel Created', async () => {
|
||||
const channelId = TestHelper.basicChannel.id;
|
||||
const channel = {id: channelId, display_name: 'test', name: TestHelper.basicChannel.name};
|
||||
await store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
store.dispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: channel});
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.CHANNEL_CREATED, data: {channel_id: channelId, team_id: TestHelper.basicTeam.id}, broadcast: {omit_users: null, user_id: 't36kso9nwtdhbm8dbkd6g4eeby', channel_id: '', team_id: ''}, seq: 57}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const state = store.getState();
|
||||
const entities = state.entities;
|
||||
const {channels} = entities.channels;
|
||||
|
||||
assert.ok(channels[channel.id]);
|
||||
});
|
||||
|
||||
it('Websocket Handle Channel Updated', async () => {
|
||||
const channelName = 'Test name';
|
||||
const channelId = TestHelper.basicChannel.id;
|
||||
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.CHANNEL_UPDATED, data: {channel: `{"id":"${channelId}","create_at":1508253647983,"update_at":1508254198797,"delete_at":0,"team_id":"55pfercbm7bsmd11p5cjpgsbwr","type":"O","display_name":"${channelName}","name":"${TestHelper.basicChannel.name}","header":"header","purpose":"","last_post_at":1508253648004,"total_msg_count":0,"extra_update_at":1508253648001,"creator_id":"${TestHelper.basicUser.id}"}`}, broadcast: {omit_users: null, user_id: '', channel_id: channelId, team_id: ''}, seq: 62}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const state = store.getState();
|
||||
const entities = state.entities;
|
||||
const {channels} = entities.channels;
|
||||
|
||||
assert.strictEqual(channels[channelId].display_name, channelName);
|
||||
});
|
||||
|
||||
it('Websocket Handle Channel Deleted', async () => {
|
||||
const time = Date.now();
|
||||
await store.dispatch(TeamActions.selectTeam(TestHelper.basicTeam));
|
||||
await store.dispatch(ChannelActions.selectChannel(TestHelper.basicChannel.id));
|
||||
|
||||
store.dispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: {id: TestHelper.generateId(), name: General.DEFAULT_CHANNEL, team_id: TestHelper.basicTeam.id, display_name: General.DEFAULT_CHANNEL}});
|
||||
store.dispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: TestHelper.basicChannel});
|
||||
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get(`/teams/${TestHelper.basicTeam.id}/channels/members`).
|
||||
reply(201, [{user_id: TestHelper.basicUser.id, channel_id: TestHelper.basicChannel.id}]);
|
||||
|
||||
mockServer.emit('message', JSON.stringify({
|
||||
event: WebsocketEvents.CHANNEL_DELETED,
|
||||
data: {
|
||||
channel_id: TestHelper.basicChannel.id,
|
||||
delete_at: time,
|
||||
},
|
||||
broadcast: {
|
||||
omit_users: null,
|
||||
user_id: '',
|
||||
channel_id: '',
|
||||
team_id: TestHelper.basicTeam.id,
|
||||
},
|
||||
seq: 68,
|
||||
}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const state = store.getState();
|
||||
const entities = state.entities;
|
||||
const {channels, currentChannelId} = entities.channels;
|
||||
|
||||
assert.ok(channels[currentChannelId].name === General.DEFAULT_CHANNEL);
|
||||
});
|
||||
|
||||
it('Websocket Handle Channel Unarchive', async () => {
|
||||
await store.dispatch(TeamActions.selectTeam(TestHelper.basicTeam));
|
||||
await store.dispatch(ChannelActions.selectChannel(TestHelper.basicChannel.id));
|
||||
|
||||
store.dispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: {id: TestHelper.generateId(), name: General.DEFAULT_CHANNEL, team_id: TestHelper.basicTeam.id, display_name: General.DEFAULT_CHANNEL}});
|
||||
store.dispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: TestHelper.basicChannel});
|
||||
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get(`/teams/${TestHelper.basicTeam.id}/channels/members`).
|
||||
reply(201, [{user_id: TestHelper.basicUser.id, channel_id: TestHelper.basicChannel.id}]);
|
||||
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.CHANNEL_UNARCHIVE, data: {channel_id: TestHelper.basicChannel.id}, broadcast: {omit_users: null, user_id: '', channel_id: '', team_id: TestHelper.basicTeam.id}, seq: 68}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const state = store.getState();
|
||||
const entities = state.entities;
|
||||
const {channels, currentChannelId} = entities.channels;
|
||||
|
||||
assert.ok(channels[currentChannelId].delete_at === 0);
|
||||
});
|
||||
|
||||
it('Websocket Handle Direct Channel', async () => {
|
||||
const channel = {id: TestHelper.generateId(), name: TestHelper.basicUser.id + '__' + TestHelper.generateId(), type: 'D'};
|
||||
|
||||
nock(Client4.getChannelsRoute()).
|
||||
get(`/${channel.id}/members/me`).
|
||||
reply(201, {user_id: TestHelper.basicUser.id, channel_id: channel.id});
|
||||
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.DIRECT_ADDED, data: {teammate_id: 'btaxe5msnpnqurayosn5p8twuw'}, broadcast: {omit_users: null, user_id: '', channel_id: channel.id, team_id: ''}, seq: 2}));
|
||||
store.dispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: channel});
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const {channels} = store.getState().entities.channels;
|
||||
assert.ok(Object.keys(channels).length);
|
||||
});
|
||||
});
|
||||
@@ -1,238 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchChannelAndMyMember} from '@actions/helpers/channels';
|
||||
import {loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {markChannelAsRead} from '@mm-redux/actions/channels';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {ChannelTypes, TeamTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {
|
||||
getAllChannels,
|
||||
getChannel,
|
||||
getChannelsNameMapInTeam,
|
||||
getCurrentChannelId,
|
||||
getRedirectChannelNameForTeam,
|
||||
} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
import {getChannelByName} from '@mm-redux/utils/channel_utils';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
export function handleChannelConvertedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const channelId = msg.data.channel_id;
|
||||
if (channelId) {
|
||||
const channel = getChannel(getState(), channelId);
|
||||
if (channel) {
|
||||
dispatch({
|
||||
type: ChannelTypes.RECEIVED_CHANNEL,
|
||||
data: {...channel, type: General.PRIVATE_CHANNEL},
|
||||
});
|
||||
}
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelCreatedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const {channel_id: channelId, team_id: teamId} = msg.data;
|
||||
const state = getState();
|
||||
const channels = getAllChannels(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
|
||||
if (teamId === currentTeamId && !channels[channelId]) {
|
||||
const channelActions = await fetchChannelAndMyMember(msg.broadcast.channel_id);
|
||||
if (channelActions.length) {
|
||||
dispatch(batchActions(channelActions, 'BATCH_WS_CHANNEL_CREATED'));
|
||||
}
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelDeletedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const state = getState();
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const config = getConfig(state);
|
||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: ChannelTypes.RECEIVED_CHANNEL_DELETED,
|
||||
data: {
|
||||
id: msg.data.channel_id,
|
||||
deleteAt: msg.data.delete_at,
|
||||
team_id: msg.broadcast.team_id,
|
||||
viewArchivedChannels,
|
||||
},
|
||||
}];
|
||||
|
||||
if (msg.broadcast.team_id === currentTeamId) {
|
||||
if (msg.data.channel_id === currentChannelId && !viewArchivedChannels) {
|
||||
const channelsInTeam = getChannelsNameMapInTeam(state, currentTeamId);
|
||||
const channel = getChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, currentTeamId));
|
||||
if (channel && channel.id) {
|
||||
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: channel.id});
|
||||
}
|
||||
EventEmitter.emit(General.DEFAULT_CHANNEL, '');
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_CHANNEL_ARCHIVED'));
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelMemberUpdatedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
try {
|
||||
const channelMember = JSON.parse(msg.data.channelMember);
|
||||
const rolesToLoad = channelMember.roles.split(' ');
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
||||
data: channelMember,
|
||||
}];
|
||||
|
||||
const roles = await Client4.getRolesByNames(rolesToLoad);
|
||||
if (roles.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: roles,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_CHANNEL_MEMBER_UPDATE'));
|
||||
} catch {
|
||||
//do nothing
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelSchemeUpdatedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
const channelActions = await fetchChannelAndMyMember(msg.broadcast.channel_id);
|
||||
if (channelActions.length) {
|
||||
dispatch(batchActions(channelActions, 'BATCH_WS_SCHEME_UPDATE'));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelUnarchiveEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const config = getConfig(state);
|
||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||
|
||||
if (msg.broadcast.team_id === currentTeamId) {
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: ChannelTypes.RECEIVED_CHANNEL_UNARCHIVED,
|
||||
data: {
|
||||
id: msg.data.channel_id,
|
||||
team_id: msg.data.team_id,
|
||||
deleteAt: 0,
|
||||
viewArchivedChannels,
|
||||
},
|
||||
}];
|
||||
|
||||
const {data: myData}: any = await dispatch(loadChannelsForTeam(currentTeamId, true));
|
||||
if (myData?.channels && myData?.channelMembers) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS,
|
||||
data: myData,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_CHANNEL_UNARCHIVED'));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelUpdatedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
let channel;
|
||||
try {
|
||||
channel = msg.data ? JSON.parse(msg.data.channel) : null;
|
||||
} catch (err) {
|
||||
return {error: err};
|
||||
}
|
||||
|
||||
const currentChannelId = getCurrentChannelId(getState());
|
||||
if (channel) {
|
||||
dispatch({
|
||||
type: ChannelTypes.RECEIVED_CHANNEL,
|
||||
data: channel,
|
||||
});
|
||||
|
||||
if (currentChannelId === channel.id) {
|
||||
// Emit an event with the channel received as we need to handle
|
||||
// the changes without listening to the store
|
||||
EventEmitter.emit(WebsocketEvents.CHANNEL_UPDATED, channel);
|
||||
}
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelViewedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const state = getState();
|
||||
const {channel_id: channelId} = msg.data;
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
|
||||
if (channelId !== currentChannelId && currentUserId === msg.broadcast.user_id) {
|
||||
dispatch(markChannelAsRead(channelId, undefined, false));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleDirectAddedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
const channelActions = await fetchChannelAndMyMember(msg.broadcast.channel_id);
|
||||
if (channelActions.length) {
|
||||
dispatch(batchActions(channelActions, 'BATCH_WS_DM_ADDED'));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUpdateMemberRoleEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
const memberData = JSON.parse(msg.data.member);
|
||||
const roles = memberData.roles.split(' ');
|
||||
const actions = [];
|
||||
|
||||
try {
|
||||
const newRoles = await Client4.getRolesByNames(roles);
|
||||
if (newRoles.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: newRoles,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
actions.push({
|
||||
type: TeamTypes.RECEIVED_MY_TEAM_MEMBER,
|
||||
data: memberData,
|
||||
});
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-import-assign */
|
||||
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket General Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('handle license changed', async () => {
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.LICENSE_CHANGED, data: {license: {IsLicensed: 'true'}}}));
|
||||
|
||||
await TestHelper.wait(200);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
const license = state.entities.general.license;
|
||||
assert.ok(license);
|
||||
assert.ok(license.IsLicensed);
|
||||
});
|
||||
|
||||
it('handle config changed', async () => {
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.CONFIG_CHANGED, data: {config: {EnableCustomEmoji: 'true', EnableLinkPreviews: 'false'}}}));
|
||||
|
||||
await TestHelper.wait(200);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
const config = state.entities.general.config;
|
||||
assert.ok(config);
|
||||
assert.ok(config.EnableCustomEmoji === 'true');
|
||||
assert.ok(config.EnableLinkPreviews === 'false');
|
||||
});
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GeneralTypes} from '@mm-redux/action_types';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {GenericAction} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleConfigChangedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const data = msg.data.config;
|
||||
|
||||
EventEmitter.emit(General.CONFIG_CHANGED, data);
|
||||
return {
|
||||
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleLicenseChangedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const data = msg.data.license;
|
||||
|
||||
return {
|
||||
type: GeneralTypes.CLIENT_LICENSE_RECEIVED,
|
||||
data,
|
||||
};
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GroupTypes} from '@mm-redux/action_types';
|
||||
import {ActionResult, DispatchFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleGroupUpdatedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc) : ActionResult => {
|
||||
const data = JSON.parse(msg.data.group);
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: GroupTypes.RECEIVED_GROUP,
|
||||
data,
|
||||
},
|
||||
{
|
||||
type: GroupTypes.RECEIVED_MY_GROUPS,
|
||||
data: [data],
|
||||
},
|
||||
]));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
@@ -1,407 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {getPostsSince} from '@actions/views/post';
|
||||
import {loadMe} from '@actions/views/user';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {ChannelTypes, GeneralTypes, PreferenceTypes, TeamTypes, UserTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId, getUsers, getUserStatuses} from '@mm-redux/selectors/entities/users';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {Channel, ChannelMembership} from '@mm-redux/types/channels';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {TeamMembership} from '@mm-redux/types/teams';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {removeUserFromList} from '@mm-redux/utils/user_utils';
|
||||
import websocketClient from '@websocket';
|
||||
|
||||
import {
|
||||
handleChannelConvertedEvent,
|
||||
handleChannelCreatedEvent,
|
||||
handleChannelDeletedEvent,
|
||||
handleChannelMemberUpdatedEvent,
|
||||
handleChannelSchemeUpdatedEvent,
|
||||
handleChannelUnarchiveEvent,
|
||||
handleChannelUpdatedEvent,
|
||||
handleChannelViewedEvent,
|
||||
handleDirectAddedEvent,
|
||||
handleUpdateMemberRoleEvent,
|
||||
} from './channels';
|
||||
import {handleConfigChangedEvent, handleLicenseChangedEvent} from './general';
|
||||
import {handleGroupUpdatedEvent} from './groups';
|
||||
import {handleOpenDialogEvent} from './integrations';
|
||||
import {handleNewPostEvent, handlePostDeleted, handlePostEdited, handlePostUnread} from './posts';
|
||||
import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences';
|
||||
import {handleAddEmoji, handleReactionAddedEvent, handleReactionRemovedEvent} from './reactions';
|
||||
import {handleRoleAddedEvent, handleRoleRemovedEvent, handleRoleUpdatedEvent} from './roles';
|
||||
import {handleLeaveTeamEvent, handleUpdateTeamEvent, handleTeamAddedEvent} from './teams';
|
||||
import {handleStatusChangedEvent, handleUserAddedEvent, handleUserRemovedEvent, handleUserRoleUpdated, handleUserUpdatedEvent} from './users';
|
||||
import {getChannelSinceValue} from '@utils/channels';
|
||||
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
|
||||
export function init(additionalOptions: any = {}) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const config = getConfig(getState());
|
||||
let connUrl = additionalOptions.websocketUrl || config.WebsocketURL || Client4.getUrl();
|
||||
const authToken = Client4.getToken();
|
||||
|
||||
connUrl += `${Client4.getUrlVersion()}/websocket`;
|
||||
websocketClient.setFirstConnectCallback(() => dispatch(handleFirstConnect()));
|
||||
websocketClient.setEventCallback((evt: WebSocketMessage) => dispatch(handleEvent(evt)));
|
||||
websocketClient.setReconnectCallback(() => dispatch(handleReconnect()));
|
||||
websocketClient.setCloseCallback((connectFailCount: number) => dispatch(handleClose(connectFailCount)));
|
||||
|
||||
const websocketOpts = {
|
||||
connectionUrl: connUrl,
|
||||
...additionalOptions,
|
||||
};
|
||||
|
||||
return websocketClient.initialize(authToken, websocketOpts);
|
||||
};
|
||||
}
|
||||
|
||||
let reconnect = false;
|
||||
export function close(shouldReconnect = false): GenericAction {
|
||||
reconnect = shouldReconnect;
|
||||
websocketClient.close(true);
|
||||
|
||||
return {
|
||||
type: GeneralTypes.WEBSOCKET_CLOSED,
|
||||
timestamp: Date.now(),
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function doFirstConnect(now: number) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const {lastDisconnectAt} = state.websocket;
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
timestamp: now,
|
||||
data: null,
|
||||
}];
|
||||
|
||||
if (isMinimumServerVersion(Client4.getServerVersion(), 5, 14) && lastDisconnectAt) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const users = getUsers(state);
|
||||
const userIds = Object.keys(users);
|
||||
const userUpdates = await Client4.getProfilesByIds(userIds, {since: lastDisconnectAt});
|
||||
|
||||
if (userUpdates.length) {
|
||||
removeUserFromList(currentUserId, userUpdates);
|
||||
actions.push({
|
||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||
data: userUpdates,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_CONNCET'));
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function doReconnect(now: number) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const users = getUsers(state);
|
||||
const {lastDisconnectAt} = state.websocket;
|
||||
const actions: Array<GenericAction> = [];
|
||||
|
||||
dispatch({
|
||||
type: GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
timestamp: now,
|
||||
data: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const {data: me}: any = await dispatch(loadMe(null, null, true));
|
||||
|
||||
if (!me.error) {
|
||||
const roles = [];
|
||||
|
||||
if (me.roles?.length) {
|
||||
roles.push(...me.roles);
|
||||
}
|
||||
|
||||
actions.push({
|
||||
type: PreferenceTypes.RECEIVED_ALL_PREFERENCES,
|
||||
data: me.preferences,
|
||||
}, {
|
||||
type: TeamTypes.RECEIVED_MY_TEAM_UNREADS,
|
||||
data: me.teamUnreads,
|
||||
}, {
|
||||
type: TeamTypes.RECEIVED_TEAMS_LIST,
|
||||
data: me.teams,
|
||||
}, {
|
||||
type: TeamTypes.RECEIVED_MY_TEAM_MEMBERS,
|
||||
data: me.teamMembers,
|
||||
});
|
||||
|
||||
const currentTeamMembership = me.teamMembers.find((tm: TeamMembership) => tm.team_id === currentTeamId && tm.delete_at === 0);
|
||||
|
||||
if (currentTeamMembership) {
|
||||
const {data: myData}: any = await dispatch(loadChannelsForTeam(currentTeamId, true));
|
||||
|
||||
if (myData?.channels && myData?.channelMembers) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS,
|
||||
data: myData,
|
||||
});
|
||||
|
||||
const stillMemberOfCurrentChannel = myData.channelMembers.find((cm: ChannelMembership) => cm.channel_id === currentChannelId);
|
||||
|
||||
const channelStillExists = myData.channels.find((c: Channel) => c.id === currentChannelId);
|
||||
const config = me.config || getConfig(getState());
|
||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||
if (!stillMemberOfCurrentChannel || !channelStillExists || (!viewArchivedChannels && channelStillExists.delete_at !== 0)) {
|
||||
EventEmitter.emit(General.SWITCH_TO_DEFAULT_CHANNEL, currentTeamId);
|
||||
} else {
|
||||
const postIds = getPostIdsInChannel(state, currentChannelId);
|
||||
const since = getChannelSinceValue(state, currentChannelId, postIds);
|
||||
dispatch(getPostsSince(currentChannelId, since));
|
||||
}
|
||||
}
|
||||
|
||||
if (myData.roles?.length) {
|
||||
roles.push(...myData.roles);
|
||||
}
|
||||
} else {
|
||||
// If the user is no longer a member of this team when reconnecting
|
||||
const newMsg = {
|
||||
data: {
|
||||
user_id: currentUserId,
|
||||
team_id: currentTeamId,
|
||||
},
|
||||
};
|
||||
dispatch(handleLeaveTeamEvent(newMsg));
|
||||
}
|
||||
|
||||
if (roles.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: roles,
|
||||
});
|
||||
}
|
||||
|
||||
if (isMinimumServerVersion(Client4.getServerVersion(), 5, 14) && lastDisconnectAt) {
|
||||
const userIds = Object.keys(users);
|
||||
const userUpdates = await Client4.getProfilesByIds(userIds, {since: lastDisconnectAt});
|
||||
|
||||
if (userUpdates.length) {
|
||||
removeUserFromList(currentUserId, userUpdates);
|
||||
actions.push({
|
||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||
data: userUpdates,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions, 'BATCH_WS_RECONNECT'));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUserTypingEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const state = getState();
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
if (currentChannelId === msg.broadcast.channel_id) {
|
||||
const profiles = getUsers(state);
|
||||
const statuses = getUserStatuses(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const config = getConfig(state);
|
||||
const userId = msg.data.user_id;
|
||||
|
||||
const data = {
|
||||
id: msg.broadcast.channel_id + msg.data.parent_id,
|
||||
userId,
|
||||
now: Date.now(),
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: WebsocketEvents.TYPING,
|
||||
data,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const newState = getState();
|
||||
const {typing} = newState.entities;
|
||||
|
||||
if (typing && typing[data.id]) {
|
||||
dispatch({
|
||||
type: WebsocketEvents.STOP_TYPING,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}, parseInt(config.TimeBetweenUserTypingUpdatesMilliseconds!, 10));
|
||||
|
||||
if (!profiles[userId] && userId !== currentUserId) {
|
||||
dispatch(getProfilesByIds([userId]));
|
||||
}
|
||||
|
||||
const status = statuses[userId];
|
||||
if (status !== General.ONLINE) {
|
||||
dispatch(getStatusesByIds([userId]));
|
||||
}
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
function handleFirstConnect() {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
const now = Date.now();
|
||||
|
||||
if (reconnect) {
|
||||
reconnect = false;
|
||||
return dispatch(doReconnect(now));
|
||||
}
|
||||
return dispatch(doFirstConnect(now));
|
||||
};
|
||||
}
|
||||
|
||||
function handleReconnect() {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return dispatch(doReconnect(Date.now()));
|
||||
};
|
||||
}
|
||||
|
||||
function handleClose(connectFailCount: number) {
|
||||
return {
|
||||
type: GeneralTypes.WEBSOCKET_FAILURE,
|
||||
error: connectFailCount,
|
||||
data: null,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function handleEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.POSTED:
|
||||
case WebsocketEvents.EPHEMERAL_MESSAGE:
|
||||
return dispatch(handleNewPostEvent(msg));
|
||||
case WebsocketEvents.POST_EDITED:
|
||||
return dispatch(handlePostEdited(msg));
|
||||
case WebsocketEvents.POST_DELETED:
|
||||
return dispatch(handlePostDeleted(msg));
|
||||
case WebsocketEvents.POST_UNREAD:
|
||||
return dispatch(handlePostUnread(msg));
|
||||
case WebsocketEvents.LEAVE_TEAM:
|
||||
return dispatch(handleLeaveTeamEvent(msg));
|
||||
case WebsocketEvents.UPDATE_TEAM:
|
||||
return dispatch(handleUpdateTeamEvent(msg));
|
||||
case WebsocketEvents.ADDED_TO_TEAM:
|
||||
return dispatch(handleTeamAddedEvent(msg));
|
||||
case WebsocketEvents.USER_ADDED:
|
||||
return dispatch(handleUserAddedEvent(msg));
|
||||
case WebsocketEvents.USER_REMOVED:
|
||||
return dispatch(handleUserRemovedEvent(msg));
|
||||
case WebsocketEvents.USER_UPDATED:
|
||||
return dispatch(handleUserUpdatedEvent(msg));
|
||||
case WebsocketEvents.ROLE_ADDED:
|
||||
return dispatch(handleRoleAddedEvent(msg));
|
||||
case WebsocketEvents.ROLE_REMOVED:
|
||||
return dispatch(handleRoleRemovedEvent(msg));
|
||||
case WebsocketEvents.ROLE_UPDATED:
|
||||
return dispatch(handleRoleUpdatedEvent(msg));
|
||||
case WebsocketEvents.USER_ROLE_UPDATED:
|
||||
return dispatch(handleUserRoleUpdated(msg));
|
||||
case WebsocketEvents.MEMBERROLE_UPDATED:
|
||||
return dispatch(handleUpdateMemberRoleEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_CREATED:
|
||||
return dispatch(handleChannelCreatedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_DELETED:
|
||||
return dispatch(handleChannelDeletedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_UNARCHIVED:
|
||||
return dispatch(handleChannelUnarchiveEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_UPDATED:
|
||||
return dispatch(handleChannelUpdatedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_CONVERTED:
|
||||
return dispatch(handleChannelConvertedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_VIEWED:
|
||||
return dispatch(handleChannelViewedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_MEMBER_UPDATED:
|
||||
return dispatch(handleChannelMemberUpdatedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_SCHEME_UPDATED:
|
||||
return dispatch(handleChannelSchemeUpdatedEvent(msg));
|
||||
case WebsocketEvents.DIRECT_ADDED:
|
||||
return dispatch(handleDirectAddedEvent(msg));
|
||||
case WebsocketEvents.PREFERENCE_CHANGED:
|
||||
return dispatch(handlePreferenceChangedEvent(msg));
|
||||
case WebsocketEvents.PREFERENCES_CHANGED:
|
||||
return dispatch(handlePreferencesChangedEvent(msg));
|
||||
case WebsocketEvents.PREFERENCES_DELETED:
|
||||
return dispatch(handlePreferencesDeletedEvent(msg));
|
||||
case WebsocketEvents.STATUS_CHANGED:
|
||||
return dispatch(handleStatusChangedEvent(msg));
|
||||
case WebsocketEvents.TYPING:
|
||||
return dispatch(handleUserTypingEvent(msg));
|
||||
case WebsocketEvents.HELLO:
|
||||
handleHelloEvent(msg);
|
||||
break;
|
||||
case WebsocketEvents.REACTION_ADDED:
|
||||
return dispatch(handleReactionAddedEvent(msg));
|
||||
case WebsocketEvents.REACTION_REMOVED:
|
||||
return dispatch(handleReactionRemovedEvent(msg));
|
||||
case WebsocketEvents.EMOJI_ADDED:
|
||||
return dispatch(handleAddEmoji(msg));
|
||||
case WebsocketEvents.LICENSE_CHANGED:
|
||||
return dispatch(handleLicenseChangedEvent(msg));
|
||||
case WebsocketEvents.CONFIG_CHANGED:
|
||||
return dispatch(handleConfigChangedEvent(msg));
|
||||
case WebsocketEvents.OPEN_DIALOG:
|
||||
return dispatch(handleOpenDialogEvent(msg));
|
||||
case WebsocketEvents.RECEIVED_GROUP:
|
||||
return dispatch(handleGroupUpdatedEvent(msg));
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
function handleHelloEvent(msg: WebSocketMessage) {
|
||||
const serverVersion = msg.data.server_version;
|
||||
if (serverVersion && Client4.serverVersion !== serverVersion) {
|
||||
Client4.serverVersion = serverVersion;
|
||||
EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion);
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
let lastTimeTypingSent = 0;
|
||||
export function userTyping(state: GlobalState, channelId: string, parentPostId: string): void {
|
||||
const config = getConfig(state);
|
||||
const t = Date.now();
|
||||
const stats = getCurrentChannelStats(state);
|
||||
const membersInChannel = stats ? stats.member_count : 0;
|
||||
|
||||
if (((t - lastTimeTypingSent) > parseInt(config.TimeBetweenUserTypingUpdatesMilliseconds!, 10)) &&
|
||||
(membersInChannel < parseInt(config.MaxNotificationsPerChannel!, 10)) && (config.EnableUserTypingMessages === 'true')) {
|
||||
websocketClient.userTyping(channelId, parentPostId);
|
||||
lastTimeTypingSent = t;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket Integration Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('handle open dialog', async () => {
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.OPEN_DIALOG, data: {dialog: JSON.stringify({url: 'someurl', trigger_id: 'sometriggerid', dialog: {}})}}));
|
||||
|
||||
await TestHelper.wait(200);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
const dialog = state.entities.integrations.dialog;
|
||||
assert.ok(dialog);
|
||||
assert.ok(dialog.url === 'someurl');
|
||||
assert.ok(dialog.trigger_id === 'sometriggerid');
|
||||
assert.ok(dialog.dialog);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntegrationTypes} from '@mm-redux/action_types';
|
||||
import {ActionResult, DispatchFunc} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleOpenDialogEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc): ActionResult => {
|
||||
const data = (msg.data && msg.data.dialog) || {};
|
||||
dispatch({type: IntegrationTypes.RECEIVED_DIALOG, data: JSON.parse(data)});
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-import-assign */
|
||||
|
||||
import assert from 'assert';
|
||||
import nock from 'nock';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
|
||||
import * as ChannelActions from '@mm-redux/actions/channels';
|
||||
import * as PostActions from '@mm-redux/actions/posts';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Posts} from '@mm-redux/constants';
|
||||
import * as PostSelectors from '@mm-redux/selectors/entities/posts';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket Post Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('Websocket Handle New Post if post does not exist', async () => {
|
||||
PostSelectors.getPost = jest.fn();
|
||||
const channelId = TestHelper.basicChannel.id;
|
||||
const message = JSON.stringify({event: WebsocketEvents.POSTED, data: {channel_display_name: TestHelper.basicChannel.display_name, channel_name: TestHelper.basicChannel.name, channel_type: 'O', post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w", "create_at": 1508245311774, "update_at": 1508245311774, "edit_at": 0, "delete_at": 0, "is_pinned": false, "user_id": "${TestHelper.basicUser.id}", "channel_id": "${channelId}", "root_id": "", "parent_id": "", "original_id": "", "message": "Unit Test", "type": "", "props": {}, "hashtags": "", "pending_post_id": "t36kso9nwtdhbm8dbkd6g4eeby: 1508245311749"}`, sender_name: TestHelper.basicUser.username, team_id: TestHelper.basicTeam.id}, broadcast: {omit_users: null, user_id: '', channel_id: channelId, team_id: ''}, seq: 2});
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
post('/users/ids').
|
||||
reply(200, [TestHelper.basicUser.id]);
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
post('/users/status/ids').
|
||||
reply(200, [{user_id: TestHelper.basicUser.id, status: 'online', manual: false, last_activity_at: 1507662212199}]);
|
||||
|
||||
// Mock that post already exists and check it is not added
|
||||
PostSelectors.getPost.mockReturnValueOnce(true);
|
||||
mockServer.emit('message', message);
|
||||
let entities = store.getState().entities;
|
||||
let posts = entities.posts.posts;
|
||||
assert.deepEqual(posts, {});
|
||||
|
||||
// Mock that post does not exist and check it is added
|
||||
PostSelectors.getPost.mockReturnValueOnce(false);
|
||||
mockServer.emit('message', message);
|
||||
await TestHelper.wait(100);
|
||||
entities = store.getState().entities;
|
||||
posts = entities.posts.posts;
|
||||
const postId = Object.keys(posts)[0];
|
||||
assert.ok(posts[postId].message.indexOf('Unit Test') > -1);
|
||||
entities = store.getState().entities;
|
||||
});
|
||||
|
||||
it('Websocket Handle New Post emits INCREASE_POST_VISIBILITY_BY_ONE for current channel when post does not exist', async () => {
|
||||
PostSelectors.getPost = jest.fn();
|
||||
const emit = jest.spyOn(EventEmitter, 'emit');
|
||||
const currentChannelId = TestHelper.generateId();
|
||||
const otherChannelId = TestHelper.generateId();
|
||||
const messageFor = (channelId) => ({event: WebsocketEvents.POSTED, data: {channel_display_name: TestHelper.basicChannel.display_name, channel_name: TestHelper.basicChannel.name, channel_type: 'O', post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w", "create_at": 1508245311774, "update_at": 1508245311774, "edit_at": 0, "delete_at": 0, "is_pinned": false, "user_id": "${TestHelper.basicUser.id}", "channel_id": "${channelId}", "root_id": "", "parent_id": "", "original_id": "", "message": "Unit Test", "type": "", "props": {}, "hashtags": "", "pending_post_id": "t36kso9nwtdhbm8dbkd6g4eeby: 1508245311749"}`, sender_name: TestHelper.basicUser.username, team_id: TestHelper.basicTeam.id}, broadcast: {omit_users: null, user_id: '', channel_id: channelId, team_id: ''}, seq: 2});
|
||||
|
||||
await store.dispatch(ChannelActions.selectChannel(currentChannelId));
|
||||
await TestHelper.wait(100);
|
||||
|
||||
// Post does not exist and is not for current channel
|
||||
PostSelectors.getPost.mockReturnValueOnce(false);
|
||||
mockServer.emit('message', JSON.stringify(messageFor(otherChannelId)));
|
||||
expect(emit).not.toHaveBeenCalled();
|
||||
|
||||
// Post exists and is not for current channel
|
||||
PostSelectors.getPost.mockReturnValueOnce(true);
|
||||
mockServer.emit('message', JSON.stringify(messageFor(otherChannelId)));
|
||||
expect(emit).not.toHaveBeenCalled();
|
||||
|
||||
// Post exists and is for current channel
|
||||
PostSelectors.getPost.mockReturnValueOnce(true);
|
||||
mockServer.emit('message', JSON.stringify(messageFor(currentChannelId)));
|
||||
expect(emit).not.toHaveBeenCalled();
|
||||
|
||||
// Post does not exist and is for current channel
|
||||
PostSelectors.getPost.mockReturnValueOnce(false);
|
||||
mockServer.emit('message', JSON.stringify(messageFor(currentChannelId)));
|
||||
expect(emit).toHaveBeenCalledWith(WebsocketEvents.INCREASE_POST_VISIBILITY_BY_ONE);
|
||||
});
|
||||
|
||||
it('Websocket Handle New Post if status is manually set do not set to online', async () => {
|
||||
const userId = TestHelper.generateId();
|
||||
|
||||
store = await configureStore({
|
||||
entities: {
|
||||
users: {
|
||||
statuses: {
|
||||
[userId]: General.DND,
|
||||
},
|
||||
isManualStatus: {
|
||||
[userId]: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
|
||||
const channelId = TestHelper.basicChannel.id;
|
||||
const message = JSON.stringify({
|
||||
event: WebsocketEvents.POSTED,
|
||||
data: {
|
||||
channel_display_name: TestHelper.basicChannel.display_name,
|
||||
channel_name: TestHelper.basicChannel.name,
|
||||
channel_type: 'O',
|
||||
post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w", "create_at": 1508245311774, "update_at": 1508245311774, "edit_at": 0, "delete_at": 0, "is_pinned": false, "user_id": "${userId}", "channel_id": "${channelId}", "root_id": "", "parent_id": "", "original_id": "", "message": "Unit Test", "type": "", "props": {}, "hashtags": "", "pending_post_id": "t36kso9nwtdhbm8dbkd6g4eeby: 1508245311749"}`,
|
||||
sender_name: TestHelper.basicUser.username,
|
||||
team_id: TestHelper.basicTeam.id,
|
||||
},
|
||||
broadcast: {
|
||||
omit_users: null,
|
||||
user_id: userId,
|
||||
channel_id: channelId,
|
||||
team_id: '',
|
||||
},
|
||||
seq: 2,
|
||||
});
|
||||
|
||||
mockServer.emit('message', message);
|
||||
const entities = store.getState().entities;
|
||||
const statuses = entities.users.statuses;
|
||||
assert.equal(statuses[userId], General.DND);
|
||||
});
|
||||
|
||||
it('Websocket Handle Post Edited', async () => {
|
||||
const post = {id: '71k8gz5ompbpfkrzaxzodffj8w'};
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.POST_EDITED, data: {post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w","create_at": 1508245311774,"update_at": 1585236976007,"edit_at": 1585236976007,"delete_at": 0,"is_pinned": false,"user_id": "${TestHelper.basicUser.id}","channel_id": "${TestHelper.basicChannel.id}","root_id": "","parent_id": "","original_id": "","message": "Unit Test (edited)","type": "","props": {},"hashtags": "","pending_post_id": ""}`}, broadcast: {omit_users: null, user_id: '', channel_id: '18k9ffsuci8xxm7ak68zfdyrce', team_id: ''}, seq: 2}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const {posts} = store.getState().entities.posts;
|
||||
assert.ok(posts);
|
||||
assert.ok(posts[post.id]);
|
||||
assert.ok(posts[post.id].message.indexOf('(edited)') > -1);
|
||||
});
|
||||
|
||||
it('Websocket Handle Post Deleted', async () => {
|
||||
const post = TestHelper.fakePost();
|
||||
post.channel_id = TestHelper.basicChannel.id;
|
||||
|
||||
post.id = '71k8gz5ompbpfkrzaxzodffj8w';
|
||||
store.dispatch(PostActions.receivedPost(post));
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.POST_DELETED, data: {post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w","create_at": 1508245311774,"update_at": 1508247709215,"edit_at": 1508247709215,"delete_at": 0,"is_pinned": false,"user_id": "${TestHelper.basicUser.id}","channel_id": "${post.channel_id}","root_id": "","parent_id": "","original_id": "","message": "Unit Test","type": "","props": {},"hashtags": "","pending_post_id": ""}`}, broadcast: {omit_users: null, user_id: '', channel_id: '18k9ffsuci8xxm7ak68zfdyrce', team_id: ''}, seq: 7}));
|
||||
|
||||
const entities = store.getState().entities;
|
||||
const {posts} = entities.posts;
|
||||
assert.strictEqual(posts[post.id].state, Posts.POST_DELETED);
|
||||
});
|
||||
|
||||
it('Websocket handle Post Unread', async () => {
|
||||
const teamId = TestHelper.generateId();
|
||||
const channelId = TestHelper.generateId();
|
||||
const userId = TestHelper.generateId();
|
||||
|
||||
store = await configureStore({
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {
|
||||
[channelId]: {id: channelId},
|
||||
},
|
||||
myMembers: {
|
||||
[channelId]: {msg_count: 10, mention_count: 0, last_viewed_at: 0},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
myMembers: {
|
||||
[teamId]: {msg_count: 10, mention_count: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
|
||||
mockServer.emit('message', JSON.stringify({
|
||||
event: WebsocketEvents.POST_UNREAD,
|
||||
data: {
|
||||
last_viewed_at: 25,
|
||||
msg_count: 3,
|
||||
mention_count: 2,
|
||||
delta_msg: 7,
|
||||
},
|
||||
broadcast: {omit_users: null, user_id: userId, channel_id: channelId, team_id: teamId},
|
||||
seq: 7,
|
||||
}));
|
||||
|
||||
const state = store.getState();
|
||||
assert.equal(state.entities.channels.manuallyUnread[channelId], true);
|
||||
assert.equal(state.entities.channels.myMembers[channelId].msg_count, 3);
|
||||
assert.equal(state.entities.channels.myMembers[channelId].mention_count, 2);
|
||||
assert.equal(state.entities.channels.myMembers[channelId].last_viewed_at, 25);
|
||||
assert.equal(state.entities.teams.myMembers[teamId].msg_count, 3);
|
||||
assert.equal(state.entities.teams.myMembers[teamId].mention_count, 2);
|
||||
});
|
||||
|
||||
it('Websocket handle Post Unread When marked on the same client', async () => {
|
||||
const teamId = TestHelper.generateId();
|
||||
const channelId = TestHelper.generateId();
|
||||
const userId = TestHelper.generateId();
|
||||
|
||||
store = await configureStore({
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {
|
||||
[channelId]: {id: channelId},
|
||||
},
|
||||
myMembers: {
|
||||
[channelId]: {msg_count: 5, mention_count: 4, last_viewed_at: 14},
|
||||
},
|
||||
manuallyUnread: {
|
||||
[channelId]: true,
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
myMembers: {
|
||||
[teamId]: {msg_count: 5, mention_count: 4},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
|
||||
mockServer.emit('message', JSON.stringify({
|
||||
event: WebsocketEvents.POST_UNREAD,
|
||||
data: {
|
||||
last_viewed_at: 25,
|
||||
msg_count: 5,
|
||||
mention_count: 4,
|
||||
delta_msg: 1,
|
||||
},
|
||||
broadcast: {omit_users: null, user_id: userId, channel_id: channelId, team_id: teamId},
|
||||
seq: 17,
|
||||
}));
|
||||
|
||||
const state = store.getState();
|
||||
assert.equal(state.entities.channels.manuallyUnread[channelId], true);
|
||||
assert.equal(state.entities.channels.myMembers[channelId].msg_count, 5);
|
||||
assert.equal(state.entities.channels.myMembers[channelId].mention_count, 4);
|
||||
assert.equal(state.entities.channels.myMembers[channelId].last_viewed_at, 14);
|
||||
assert.equal(state.entities.teams.myMembers[teamId].msg_count, 5);
|
||||
assert.equal(state.entities.teams.myMembers[teamId].mention_count, 4);
|
||||
});
|
||||
});
|
||||
@@ -1,209 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {
|
||||
fetchMyChannel,
|
||||
fetchMyChannelMember,
|
||||
makeDirectChannelVisibleIfNecessary,
|
||||
makeGroupMessageVisibleIfNecessary,
|
||||
markChannelAsUnread,
|
||||
} from '@actions/helpers/channels';
|
||||
import {markAsViewedAndReadBatch} from '@actions/views/channel';
|
||||
import {getPostsAdditionalDataBatch, getPostThread} from '@actions/views/post';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {ChannelTypes} from '@mm-redux/action_types';
|
||||
import {getUnreadPostData, postDeleted, receivedNewPost, receivedPost} from '@mm-redux/actions/posts';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {
|
||||
getChannel,
|
||||
getCurrentChannelId,
|
||||
getMyChannelMember as selectMyChannelMember,
|
||||
isManuallyUnread,
|
||||
} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {getPost as selectPost} from '@mm-redux/selectors/entities/posts';
|
||||
import {getUserIdFromChannelName} from '@mm-redux/utils/channel_utils';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {isFromWebhook, isSystemMessage, shouldIgnorePost} from '@mm-redux/utils/post_utils';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleNewPostEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const data = JSON.parse(msg.data.post);
|
||||
const post = {
|
||||
...data,
|
||||
ownPost: data.user_id === currentUserId,
|
||||
};
|
||||
|
||||
const actions: Array<GenericAction> = [];
|
||||
|
||||
const exists = selectPost(state, post.pending_post_id);
|
||||
|
||||
if (!exists) {
|
||||
if (getCurrentChannelId(state) === post.channel_id) {
|
||||
EventEmitter.emit(WebsocketEvents.INCREASE_POST_VISIBILITY_BY_ONE);
|
||||
}
|
||||
|
||||
const myChannel = getChannel(state, post.channel_id);
|
||||
if (!myChannel) {
|
||||
const channel = await fetchMyChannel(post.channel_id);
|
||||
if (channel.data) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_CHANNEL,
|
||||
data: channel.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const myChannelMember = selectMyChannelMember(state, post.channel_id);
|
||||
if (!myChannelMember) {
|
||||
const member = await fetchMyChannelMember(post.channel_id);
|
||||
if (member.data) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
||||
data: member.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
actions.push(receivedNewPost(post));
|
||||
|
||||
// If we don't have the thread for this post, fetch it from the server
|
||||
// and include the actions in the batch
|
||||
if (post.root_id) {
|
||||
const rootPost = selectPost(state, post.root_id);
|
||||
|
||||
if (!rootPost) {
|
||||
const thread: any = await dispatch(getPostThread(post.root_id, true));
|
||||
if (thread.data?.length) {
|
||||
actions.push(...thread.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (post.channel_id === currentChannelId) {
|
||||
const id = post.channel_id + post.root_id;
|
||||
const {typing} = state.entities;
|
||||
|
||||
if (typing[id]) {
|
||||
actions.push({
|
||||
type: WebsocketEvents.STOP_TYPING,
|
||||
data: {
|
||||
id,
|
||||
userId: post.user_id,
|
||||
now: Date.now(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch and batch additional post data
|
||||
const additional: any = await dispatch(getPostsAdditionalDataBatch([post]));
|
||||
if (additional.data.length) {
|
||||
actions.push(...additional.data);
|
||||
}
|
||||
|
||||
if (msg.data.channel_type === General.DM_CHANNEL) {
|
||||
const otherUserId = getUserIdFromChannelName(currentUserId, msg.data.channel_name);
|
||||
const dmAction = makeDirectChannelVisibleIfNecessary(state, otherUserId);
|
||||
if (dmAction) {
|
||||
actions.push(dmAction);
|
||||
}
|
||||
} else if (msg.data.channel_type === General.GM_CHANNEL) {
|
||||
const gmActions = await makeGroupMessageVisibleIfNecessary(state, post.channel_id);
|
||||
if (gmActions) {
|
||||
actions.push(...gmActions);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldIgnorePost(post)) {
|
||||
let markAsRead = false;
|
||||
let markAsReadOnServer = false;
|
||||
|
||||
if (!isManuallyUnread(state, post.channel_id)) {
|
||||
if (
|
||||
post.user_id === getCurrentUserId(state) &&
|
||||
!isSystemMessage(post) &&
|
||||
!isFromWebhook(post)
|
||||
) {
|
||||
markAsRead = true;
|
||||
markAsReadOnServer = false;
|
||||
} else if (post.channel_id === currentChannelId) {
|
||||
markAsRead = true;
|
||||
markAsReadOnServer = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (markAsRead) {
|
||||
const readActions = markAsViewedAndReadBatch(state, post.channel_id, undefined, markAsReadOnServer);
|
||||
actions.push(...readActions);
|
||||
} else {
|
||||
const unreadActions = markChannelAsUnread(state, msg.data.team_id, post.channel_id, msg.data.mentions);
|
||||
actions.push(...unreadActions);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_NEW_POST'));
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handlePostEdited(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const data = JSON.parse(msg.data.post);
|
||||
const post = {
|
||||
...data,
|
||||
ownPost: data.user_id === currentUserId,
|
||||
};
|
||||
const actions = [receivedPost(post)];
|
||||
|
||||
const additional: any = await dispatch(getPostsAdditionalDataBatch([post]));
|
||||
if (additional.data.length) {
|
||||
actions.push(...additional.data);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_POST_EDITED'));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handlePostDeleted(msg: WebSocketMessage): GenericAction {
|
||||
const data = JSON.parse(msg.data.post);
|
||||
|
||||
return postDeleted(data);
|
||||
}
|
||||
|
||||
export function handlePostUnread(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const state = getState();
|
||||
const manual = isManuallyUnread(state, msg.broadcast.channel_id);
|
||||
|
||||
if (!manual) {
|
||||
const member = selectMyChannelMember(state, msg.broadcast.channel_id);
|
||||
const delta = member ? member.msg_count - msg.data.msg_count : msg.data.msg_count;
|
||||
const info = {
|
||||
...msg.data,
|
||||
user_id: msg.broadcast.user_id,
|
||||
team_id: msg.broadcast.team_id,
|
||||
channel_id: msg.broadcast.channel_id,
|
||||
deltaMsgs: delta,
|
||||
};
|
||||
const data = getUnreadPostData(info, state);
|
||||
dispatch({
|
||||
type: ChannelTypes.POST_UNREAD_SUCCESS,
|
||||
data,
|
||||
});
|
||||
return {data};
|
||||
}
|
||||
|
||||
return {data: null};
|
||||
};
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getAddedDmUsersIfNecessary} from '@actions/helpers/channels';
|
||||
import {getPost} from '@actions/views/post';
|
||||
import {PreferenceTypes} from '@mm-redux/action_types';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getAllPosts} from '@mm-redux/selectors/entities/posts';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {PreferenceType} from '@mm-redux/types/preferences';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handlePreferenceChangedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const preference = JSON.parse(msg.data.preference);
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: PreferenceTypes.RECEIVED_PREFERENCES,
|
||||
data: [preference],
|
||||
}];
|
||||
|
||||
const dmActions = await getAddedDmUsersIfNecessary(getState(), [preference]);
|
||||
if (dmActions.length) {
|
||||
actions.push(...dmActions);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_PREFERENCE_CHANGED'));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handlePreferencesChangedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const preferences: PreferenceType[] = JSON.parse(msg.data.preferences);
|
||||
const posts = getAllPosts(getState());
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: PreferenceTypes.RECEIVED_PREFERENCES,
|
||||
data: preferences,
|
||||
}];
|
||||
|
||||
preferences.forEach((pref) => {
|
||||
if (pref.category === Preferences.CATEGORY_FLAGGED_POST && !posts[pref.name]) {
|
||||
dispatch(getPost(pref.name));
|
||||
}
|
||||
});
|
||||
|
||||
const dmActions = await getAddedDmUsersIfNecessary(getState(), preferences);
|
||||
if (dmActions.length) {
|
||||
actions.push(...dmActions);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_PREFERENCES_CHANGED'));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handlePreferencesDeletedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const preferences = JSON.parse(msg.data.preferences);
|
||||
|
||||
return {type: PreferenceTypes.DELETED_PREFERENCES, data: preferences};
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket Reaction Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('Websocket Handle Reaction Added to Post', async () => {
|
||||
const emoji = '+1';
|
||||
const post = {id: 'w7yo9377zbfi9mgiq5gbfpn3ha'};
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.REACTION_ADDED, data: {reaction: `{"user_id":"${TestHelper.basicUser.id}","post_id":"w7yo9377zbfi9mgiq5gbfpn3ha","emoji_name":"${emoji}","create_at":1508249125852}`}, broadcast: {omit_users: null, user_id: '', channel_id: TestHelper.basicChannel.id, team_id: ''}, seq: 12}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
const nextEntities = store.getState().entities;
|
||||
const {reactions} = nextEntities.posts;
|
||||
const reactionsForPost = reactions[post.id];
|
||||
|
||||
assert.ok(reactionsForPost.hasOwnProperty(`${TestHelper.basicUser.id}-${emoji}`));
|
||||
});
|
||||
|
||||
it('Websocket handle emoji added', async () => {
|
||||
const created = {id: '1mmgakhhupfgfm8oug6pooc5no'};
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.EMOJI_ADDED, data: {emoji: `{"id":"1mmgakhhupfgfm8oug6pooc5no","create_at":1508263941321,"update_at":1508263941321,"delete_at":0,"creator_id":"t36kso9nwtdhbm8dbkd6g4eeby","name":"${TestHelper.generateId()}"}`}, broadcast: {omit_users: null, user_id: '', channel_id: '', team_id: ''}, seq: 2}));
|
||||
|
||||
await TestHelper.wait(200);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
const emojis = state.entities.emojis.customEmoji;
|
||||
assert.ok(emojis);
|
||||
assert.ok(emojis[created.id]);
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {EmojiTypes, PostTypes} from '@mm-redux/action_types';
|
||||
import {getCustomEmojiForReaction} from '@mm-redux/actions/posts';
|
||||
import {ActionResult, DispatchFunc, GenericAction} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleAddEmoji(msg: WebSocketMessage): GenericAction {
|
||||
const data = JSON.parse(msg.data.emoji);
|
||||
|
||||
return {
|
||||
type: EmojiTypes.RECEIVED_CUSTOM_EMOJI,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleReactionAddedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc): ActionResult => {
|
||||
const {data} = msg;
|
||||
const reaction = JSON.parse(data.reaction);
|
||||
|
||||
dispatch(getCustomEmojiForReaction(reaction.emoji_name));
|
||||
|
||||
dispatch({
|
||||
type: PostTypes.RECEIVED_REACTION,
|
||||
data: reaction,
|
||||
});
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleReactionRemovedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const {data} = msg;
|
||||
const reaction = JSON.parse(data.reaction);
|
||||
|
||||
return {
|
||||
type: PostTypes.REACTION_DELETED,
|
||||
data: reaction,
|
||||
};
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {RoleTypes} from '@mm-redux/action_types';
|
||||
import {GenericAction} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleRoleAddedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const role = JSON.parse(msg.data.role);
|
||||
|
||||
return {
|
||||
type: RoleTypes.RECEIVED_ROLE,
|
||||
data: role,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleRoleRemovedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const role = JSON.parse(msg.data.role);
|
||||
|
||||
return {
|
||||
type: RoleTypes.ROLE_DELETED,
|
||||
data: role,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleRoleUpdatedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const role = JSON.parse(msg.data.role);
|
||||
|
||||
return {
|
||||
type: RoleTypes.RECEIVED_ROLE,
|
||||
data: role,
|
||||
};
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
import nock from 'nock';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {TeamTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket Team Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
store.dispatch(batchActions([
|
||||
{type: UserTypes.RECEIVED_ME, data: TestHelper.basicUser},
|
||||
{type: TeamTypes.RECEIVED_TEAM, data: TestHelper.basicTeam},
|
||||
{type: TeamTypes.RECEIVED_MY_TEAM_MEMBER, data: TestHelper.basicTeamMember},
|
||||
{type: TeamTypes.RECEIVED_MY_TEAM_UNREADS, data: [TestHelper.basicTeamMember]},
|
||||
]));
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
// If we move this test lower it will fail cause of a permissions issue
|
||||
it('Websocket handle team updated', async () => {
|
||||
const team = {id: '55pfercbm7bsmd11p5cjpgsbwr'};
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.UPDATE_TEAM, data: {team: `{"id":"55pfercbm7bsmd11p5cjpgsbwr","create_at":1495553950859,"update_at":1508250370054,"delete_at":0,"display_name":"${TestHelper.basicTeam.display_name}","name":"${TestHelper.basicTeam.name}","description":"description","email":"","type":"O","company_name":"","allowed_domains":"","invite_id":"m93f54fu5bfntewp8ctwonw19w","allow_open_invite":true}`}, broadcast: {omit_users: null, user_id: '', channel_id: '', team_id: ''}, seq: 26}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const entities = store.getState().entities;
|
||||
const {teams} = entities.teams;
|
||||
const updated = teams[team.id];
|
||||
assert.ok(updated);
|
||||
assert.strictEqual(updated.allow_open_invite, true);
|
||||
});
|
||||
|
||||
it('Websocket handle team patched', async () => {
|
||||
const team = {id: '55pfercbm7bsmd11p5cjpgsbwr'};
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.UPDATE_TEAM, data: {team: `{"id":"55pfercbm7bsmd11p5cjpgsbwr","create_at":1495553950859,"update_at":1508250370054,"delete_at":0,"display_name":"${TestHelper.basicTeam.display_name}","name":"${TestHelper.basicTeam.name}","description":"description","email":"","type":"O","company_name":"","allowed_domains":"","invite_id":"m93f54fu5bfntewp8ctwonw19w","allow_open_invite":true}`}, broadcast: {omit_users: null, user_id: '', channel_id: '', team_id: ''}, seq: 26}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const entities = store.getState().entities;
|
||||
const {teams} = entities.teams;
|
||||
const updated = teams[team.id];
|
||||
assert.ok(updated);
|
||||
assert.strictEqual(updated.allow_open_invite, true);
|
||||
});
|
||||
|
||||
it('Websocket handle user added to team', async () => {
|
||||
const team = TestHelper.basicTeam;
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
get(`/teams/${team.id}`).
|
||||
reply(200, team);
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
get('/users/me/teams/unread').
|
||||
reply(200, [{team_id: team.id, msg_count: 0, mention_count: 0}]);
|
||||
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.ADDED_TO_TEAM, data: {team_id: team.id, user_id: TestHelper.basicUser.id}, broadcast: {omit_users: null, user_id: TestHelper.basicUser.id, channel_id: '', team_id: ''}, seq: 2}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const {teams, myMembers} = store.getState().entities.teams;
|
||||
assert.ok(teams[team.id]);
|
||||
assert.ok(myMembers[team.id]);
|
||||
|
||||
const member = myMembers[team.id];
|
||||
assert.ok(member.hasOwnProperty('mention_count'));
|
||||
});
|
||||
|
||||
it('WebSocket Leave Team', async () => {
|
||||
const team = TestHelper.basicTeam;
|
||||
store.dispatch(batchActions([
|
||||
{type: UserTypes.RECEIVED_ME, data: TestHelper.basicUser},
|
||||
{type: TeamTypes.RECEIVED_TEAM, data: TestHelper.basicTeam},
|
||||
{type: TeamTypes.RECEIVED_MY_TEAM_MEMBER, data: TestHelper.basicTeamMember},
|
||||
]));
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.LEAVE_TEAM, data: {team_id: team.id, user_id: TestHelper.basicUser.id}, broadcast: {omit_users: null, user_id: '', channel_id: '', team_id: team.id}, seq: 35}));
|
||||
|
||||
const {myMembers} = store.getState().entities.teams;
|
||||
assert.ifError(myMembers[team.id]);
|
||||
});
|
||||
});
|
||||
@@ -1,106 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {RoleTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getCurrentTeamId, getTeams as getTeamsSelector} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from '@mm-redux/selectors/entities/users';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {isGuest} from '@mm-redux/utils/user_utils';
|
||||
|
||||
export function handleLeaveTeamEvent(msg: Partial<WebSocketMessage>) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const teams = getTeamsSelector(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentUser = getCurrentUser(state);
|
||||
|
||||
if (currentUser.id === msg.data.user_id) {
|
||||
const actions: Array<GenericAction> = [{type: TeamTypes.LEAVE_TEAM, data: teams[msg.data.team_id]}];
|
||||
if (isGuest(currentUser.roles)) {
|
||||
const notVisible = await notVisibleUsersActions(state);
|
||||
if (notVisible.length) {
|
||||
actions.push(...notVisible);
|
||||
}
|
||||
}
|
||||
dispatch(batchActions(actions, 'BATCH_WS_LEAVE_TEAM'));
|
||||
|
||||
// if they are on the team being removed deselect the current team and channel
|
||||
if (currentTeamId === msg.data.team_id) {
|
||||
EventEmitter.emit('leave_team');
|
||||
}
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUpdateTeamEvent(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: TeamTypes.UPDATED_TEAM,
|
||||
data: JSON.parse(msg.data.team),
|
||||
};
|
||||
}
|
||||
|
||||
export function handleTeamAddedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
try {
|
||||
const teamId = msg.data.team_id;
|
||||
const userId = msg.data.user_id;
|
||||
const [team, member, teamUnreads] = await Promise.all([
|
||||
Client4.getTeam(msg.data.team_id),
|
||||
Client4.getTeamMember(teamId, userId),
|
||||
Client4.getMyTeamUnreads(),
|
||||
]);
|
||||
|
||||
const actions = [];
|
||||
if (team) {
|
||||
actions.push({
|
||||
type: TeamTypes.RECEIVED_TEAM,
|
||||
data: team,
|
||||
});
|
||||
|
||||
if (member) {
|
||||
actions.push({
|
||||
type: TeamTypes.RECEIVED_MY_TEAM_MEMBER,
|
||||
data: member,
|
||||
});
|
||||
|
||||
if (member.roles) {
|
||||
const rolesToLoad = new Set<string>();
|
||||
for (const role of member.roles.split(' ')) {
|
||||
rolesToLoad.add(role);
|
||||
}
|
||||
|
||||
if (rolesToLoad.size > 0) {
|
||||
const roles = await Client4.getRolesByNames(Array.from(rolesToLoad));
|
||||
if (roles.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: roles,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (teamUnreads) {
|
||||
actions.push({
|
||||
type: TeamTypes.RECEIVED_MY_TEAM_UNREADS,
|
||||
data: teamUnreads,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions, 'BATCH_WS_TEAM_ADDED'));
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
import {TeamTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket User Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
store.dispatch(batchActions([
|
||||
{type: UserTypes.RECEIVED_ME, data: TestHelper.basicUser},
|
||||
{type: TeamTypes.RECEIVED_TEAM, data: TestHelper.basicTeam},
|
||||
{type: TeamTypes.RECEIVED_MY_TEAM_MEMBER, data: TestHelper.basicTeamMember},
|
||||
]));
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('Websocket Handle User Added', async () => {
|
||||
const user = {...TestHelper.fakeUser(), id: TestHelper.generateId()};
|
||||
store.dispatch({type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, data: {id: TestHelper.basicChannel.id, user_id: user.id}});
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.USER_ADDED, data: {team_id: TestHelper.basicTeam.id, user_id: user.id}, broadcast: {omit_users: null, user_id: '', channel_id: TestHelper.basicChannel.id, team_id: ''}, seq: 42}));
|
||||
|
||||
const entities = store.getState().entities;
|
||||
const profilesInChannel = entities.users.profilesInChannel;
|
||||
assert.ok(profilesInChannel[TestHelper.basicChannel.id].has(user.id));
|
||||
});
|
||||
|
||||
it('Websocket Handle User Removed', async () => {
|
||||
const user = {...TestHelper.fakeUser(), id: TestHelper.generateId()};
|
||||
store.dispatch({type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, data: {id: TestHelper.basicChannel.id, user_id: user.id}});
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.USER_REMOVED, data: {remover_id: TestHelper.basicUser.id, user_id: user.id}, broadcast: {omit_users: null, user_id: '', channel_id: TestHelper.basicChannel.id, team_id: ''}, seq: 42}));
|
||||
|
||||
const state = store.getState();
|
||||
const entities = state.entities;
|
||||
const profilesNotInChannel = entities.users.profilesNotInChannel;
|
||||
|
||||
assert.ok(profilesNotInChannel[TestHelper.basicChannel.id].has(user.id));
|
||||
});
|
||||
|
||||
it('Websocket Handle User Removed when Current is Guest', async () => {
|
||||
const basicGuestUser = TestHelper.fakeUserWithId();
|
||||
basicGuestUser.roles = 'system_guest';
|
||||
|
||||
const user = {...TestHelper.fakeUser(), id: TestHelper.generateId()};
|
||||
|
||||
// add user first
|
||||
store.dispatch({type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, data: {id: TestHelper.basicChannel.id, user_id: user.id}});
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.USER_ADDED, data: {team_id: TestHelper.basicTeam.id, user_id: user.id}, broadcast: {omit_users: null, user_id: '', channel_id: TestHelper.basicChannel.id, team_id: ''}, seq: 42}));
|
||||
|
||||
assert.ok(store.getState().entities.users.profilesInChannel[TestHelper.basicChannel.id].has(user.id));
|
||||
|
||||
// remove user
|
||||
store.dispatch({type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, data: {id: TestHelper.basicChannel.id, user_id: user.id}});
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.USER_REMOVED, data: {remover_id: basicGuestUser.id, user_id: user.id}, broadcast: {omit_users: null, user_id: '', channel_id: TestHelper.basicChannel.id, team_id: ''}, seq: 42}));
|
||||
|
||||
assert.ok(!store.getState().entities.users.profilesInChannel[TestHelper.basicChannel.id].has(user.id));
|
||||
});
|
||||
|
||||
it('Websocket Handle User Updated', async () => {
|
||||
const user = {...TestHelper.fakeUser(), id: TestHelper.generateId()};
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.USER_UPDATED, data: {user: {id: user.id, create_at: 1495570297229, update_at: 1508253268652, delete_at: 0, username: 'tim', auth_data: '', auth_service: '', email: 'tim@bladekick.com', nickname: '', first_name: 'tester4', last_name: '', position: '', roles: 'system_user', locale: 'en'}}, broadcast: {omit_users: null, user_id: '', channel_id: '', team_id: ''}, seq: 53}));
|
||||
|
||||
store.subscribe(() => {
|
||||
const state = store.getState();
|
||||
const entities = state.entities;
|
||||
const profiles = entities.users.profiles;
|
||||
|
||||
assert.strictEqual(profiles[user.id].first_name, 'tester4');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,205 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchChannelAndMyMember} from '@actions/helpers/channels';
|
||||
import {loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {getMe} from '@actions/views/user';
|
||||
import {ChannelTypes, TeamTypes, UserTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getAllChannels, getCurrentChannelId, getChannelMembersInChannels} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUser, getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
import {isGuest} from '@mm-redux/utils/user_utils';
|
||||
|
||||
export function handleStatusChangedEvent(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: [{user_id: msg.data.user_id, status: msg.data.status}],
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUserAddedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
try {
|
||||
const state = getState();
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const teamId = msg.data.team_id;
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: ChannelTypes.CHANNEL_MEMBER_ADDED,
|
||||
data: {
|
||||
channel_id: msg.broadcast.channel_id,
|
||||
user_id: msg.data.user_id,
|
||||
},
|
||||
}];
|
||||
|
||||
if (msg.broadcast.channel_id === currentChannelId) {
|
||||
const stat = await Client4.getChannelStats(currentChannelId);
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_CHANNEL_STATS,
|
||||
data: stat,
|
||||
});
|
||||
}
|
||||
|
||||
if (teamId === currentTeamId && msg.data.user_id === currentUserId) {
|
||||
const channelActions = await fetchChannelAndMyMember(msg.broadcast.channel_id);
|
||||
|
||||
if (channelActions.length) {
|
||||
actions.push(...channelActions);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_USER_ADDED'));
|
||||
} catch (error) {
|
||||
//do nothing
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUserRemovedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
try {
|
||||
const state = getState();
|
||||
const channels = getAllChannels(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentUser = getCurrentUser(state);
|
||||
const actions: Array<GenericAction> = [];
|
||||
let channelId;
|
||||
let userId;
|
||||
|
||||
if (msg.data.user_id) {
|
||||
userId = msg.data.user_id;
|
||||
channelId = msg.broadcast.channel_id;
|
||||
} else if (msg.broadcast.user_id) {
|
||||
channelId = msg.data.channel_id;
|
||||
userId = msg.broadcast.user_id;
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
actions.push({
|
||||
type: ChannelTypes.CHANNEL_MEMBER_REMOVED,
|
||||
data: {
|
||||
channel_id: channelId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const channel = channels[currentChannelId];
|
||||
|
||||
if (msg.data?.user_id !== currentUser.id) {
|
||||
const members = getChannelMembersInChannels(state);
|
||||
const isMember = Object.values(members).some((member) => member[msg.data.user_id]);
|
||||
if (channel && isGuest(currentUser.roles) && !isMember) {
|
||||
actions.push({
|
||||
type: UserTypes.PROFILE_NO_LONGER_VISIBLE,
|
||||
data: {user_id: msg.data.user_id},
|
||||
}, {
|
||||
type: TeamTypes.REMOVE_MEMBER_FROM_TEAM,
|
||||
data: {team_id: channel.team_id, user_id: msg.data.user_id},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let redirectToDefaultChannel = false;
|
||||
if (msg.broadcast.user_id === currentUser.id && currentTeamId) {
|
||||
const {data: myData}: any = await dispatch(loadChannelsForTeam(currentTeamId, true));
|
||||
|
||||
if (myData?.channels && myData?.channelMembers) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS,
|
||||
data: myData,
|
||||
});
|
||||
}
|
||||
|
||||
if (channel) {
|
||||
actions.push({
|
||||
type: ChannelTypes.LEAVE_CHANNEL,
|
||||
data: {
|
||||
id: msg.data.channel_id,
|
||||
user_id: currentUser.id,
|
||||
team_id: channel.team_id,
|
||||
type: channel.type,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (msg.data.channel_id === currentChannelId) {
|
||||
// emit the event so the client can change his own state
|
||||
redirectToDefaultChannel = true;
|
||||
}
|
||||
if (isGuest(currentUser.roles)) {
|
||||
const notVisible = await notVisibleUsersActions(state);
|
||||
if (notVisible.length) {
|
||||
actions.push(...notVisible);
|
||||
}
|
||||
}
|
||||
} else if (msg.data.channel_id === currentChannelId) {
|
||||
const stat = await Client4.getChannelStats(currentChannelId);
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_CHANNEL_STATS,
|
||||
data: stat,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_USER_REMOVED'));
|
||||
if (redirectToDefaultChannel) {
|
||||
EventEmitter.emit(General.REMOVED_FROM_CHANNEL, channel.display_name);
|
||||
EventEmitter.emit(General.SWITCH_TO_DEFAULT_CHANNEL, currentTeamId);
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUserRoleUpdated(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
try {
|
||||
const roles = msg.data.roles.split(' ');
|
||||
const data = await Client4.getRolesByNames(roles);
|
||||
|
||||
dispatch({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: data.roles,
|
||||
});
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUserUpdatedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const currentUser = getCurrentUser(getState());
|
||||
const user = msg.data.user;
|
||||
|
||||
if (user.id === currentUser.id) {
|
||||
if (user.update_at > currentUser.update_at) {
|
||||
// Need to request me to make sure we don't override with sanitized fields from the
|
||||
// websocket event
|
||||
dispatch(getMe());
|
||||
}
|
||||
} else {
|
||||
dispatch({
|
||||
type: UserTypes.RECEIVED_PROFILES,
|
||||
data: {
|
||||
[user.id]: user,
|
||||
},
|
||||
});
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
@@ -1,552 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-import-assign */
|
||||
|
||||
import assert from 'assert';
|
||||
import nock from 'nock';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import thunk from 'redux-thunk';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
|
||||
import {GeneralTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Posts, RequestStatus} from '@mm-redux/constants';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
const mockConfigRequest = (config = {}) => {
|
||||
nock(Client4.getBaseRoute()).
|
||||
get('/config/client?format=old').
|
||||
reply(200, config);
|
||||
};
|
||||
|
||||
const mockChanelsRequest = (teamId, channels = []) => {
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get(`/teams/${teamId}/channels?include_deleted=true`).
|
||||
reply(200, channels);
|
||||
};
|
||||
|
||||
const mockGetKnownUsersRequest = (userIds = []) => {
|
||||
nock(Client4.getBaseRoute()).
|
||||
get('/users/known').
|
||||
reply(200, userIds);
|
||||
};
|
||||
|
||||
const mockRolesRequest = (rolesToLoad = []) => {
|
||||
nock(Client4.getRolesRoute()).
|
||||
post('/names', JSON.stringify(rolesToLoad)).
|
||||
reply(200, rolesToLoad);
|
||||
};
|
||||
|
||||
const mockTeamMemberRequest = (tm = []) => {
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get('/teams/members').
|
||||
reply(200, tm);
|
||||
};
|
||||
|
||||
describe('Actions.Websocket', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('WebSocket Connect', () => {
|
||||
const ws = store.getState().requests.general.websocket;
|
||||
assert.ok(ws.status === RequestStatus.SUCCESS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions.Websocket doReconnect', () => {
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const me = TestHelper.fakeUserWithId();
|
||||
const team = TestHelper.fakeTeamWithId();
|
||||
const teamMember = TestHelper.fakeTeamMember(me.id, team.id);
|
||||
const channel1 = TestHelper.fakeChannelWithId(team.id);
|
||||
const channel2 = TestHelper.fakeChannelWithId(team.id);
|
||||
const cMember1 = TestHelper.fakeChannelMember(me.id, channel1.id);
|
||||
const cMember2 = TestHelper.fakeChannelMember(me.id, channel2.id);
|
||||
|
||||
const currentTeamId = team.id;
|
||||
const currentUserId = me.id;
|
||||
const currentChannelId = channel1.id;
|
||||
|
||||
const initialState = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId,
|
||||
myMembers: {
|
||||
[currentTeamId]: teamMember,
|
||||
},
|
||||
teams: {
|
||||
[currentTeamId]: team,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
currentChannelId,
|
||||
channels: {
|
||||
currentChannelId: channel1,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {
|
||||
[me.id]: me,
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
posts: {
|
||||
posts: {},
|
||||
postsInChannel: {},
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
connected: false,
|
||||
lastConnectAt: 0,
|
||||
lastDisconnectAt: 0,
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
return TestHelper.initBasic(Client4);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nock(Client4.getBaseRoute()).
|
||||
get('/users/me').
|
||||
reply(200, me);
|
||||
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get('/teams').
|
||||
reply(200, [team]);
|
||||
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get('/teams/unread').
|
||||
reply(200, [{id: team.id, msg_count: 0, mention_count: 0}]);
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
get('/users/me/preferences').
|
||||
reply(200, []);
|
||||
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get(`/teams/${team.id}/channels/members`).
|
||||
reply(200, [cMember1, cMember2]);
|
||||
|
||||
nock(Client4.getChannelRoute(channel1.id)).
|
||||
get(`/posts?page=0&per_page=${Posts.POST_CHUNK_SIZE}`).
|
||||
reply(200, {
|
||||
posts: {
|
||||
post1: {id: 'post1', create_at: 0, message: 'hey'},
|
||||
},
|
||||
order: ['post1'],
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('handle doReconnect', async () => {
|
||||
const state = {...initialState};
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
|
||||
mockConfigRequest();
|
||||
mockTeamMemberRequest([teamMember]);
|
||||
mockChanelsRequest(team.id, [channel1, channel2]);
|
||||
|
||||
let rolesToLoad = Array.from(new Set(me.roles.split(' ').
|
||||
concat(teamMember.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
|
||||
concat(cMember2.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
await testStore.dispatch(Actions.doReconnect(timestamp));
|
||||
await TestHelper.wait(300);
|
||||
const actionTypes = testStore.getActions().map((a) => a.type);
|
||||
expect(actionTypes).toEqual(expectedActions);
|
||||
expect(actionTypes).not.toEqual(expect.arrayContaining(expectedMissingActions));
|
||||
});
|
||||
|
||||
it('handle doReconnect after the current channel was archived or the user left it', async () => {
|
||||
const state = {
|
||||
...initialState,
|
||||
entities: {
|
||||
...initialState.entities,
|
||||
channels: {
|
||||
...initialState.entities.channels,
|
||||
currentChannelId: 'channel-3',
|
||||
},
|
||||
},
|
||||
};
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_GET_POSTS_SINCE',
|
||||
];
|
||||
|
||||
mockConfigRequest();
|
||||
mockTeamMemberRequest([teamMember]);
|
||||
mockChanelsRequest(team.id, [channel1, channel2]);
|
||||
|
||||
let rolesToLoad = Array.from(new Set(me.roles.split(' ').
|
||||
concat(teamMember.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
|
||||
concat(cMember2.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
await testStore.dispatch(Actions.doReconnect(timestamp));
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const actions = testStore.getActions().map((a) => a.type);
|
||||
|
||||
expect(actions).toEqual(expect.arrayContaining(expectedActions));
|
||||
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
|
||||
});
|
||||
|
||||
it('handle doReconnect after the current channel was archived and setting is on', async () => {
|
||||
const archived = {
|
||||
...channel1,
|
||||
delete_at: 123,
|
||||
};
|
||||
const state = {
|
||||
...initialState,
|
||||
channels: {
|
||||
currentChannelId,
|
||||
channels: {
|
||||
currentChannelId: archived,
|
||||
},
|
||||
},
|
||||
};
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
|
||||
mockConfigRequest({ExperimentalViewArchivedChannels: 'true'});
|
||||
mockTeamMemberRequest([teamMember]);
|
||||
mockChanelsRequest(team.id, [archived, channel2]);
|
||||
|
||||
let rolesToLoad = Array.from(new Set(me.roles.split(' ').
|
||||
concat(teamMember.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
|
||||
concat(cMember2.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
await testStore.dispatch(Actions.doReconnect(timestamp));
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const actions = testStore.getActions().map((a) => a.type);
|
||||
expect(actions).toEqual(expect.arrayContaining(expectedActions));
|
||||
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
|
||||
});
|
||||
|
||||
it('handle doReconnect after the current channel was archived and setting is off', async () => {
|
||||
const archived = {
|
||||
...channel1,
|
||||
delete_at: 123,
|
||||
};
|
||||
|
||||
const state = {
|
||||
...initialState,
|
||||
channels: {
|
||||
currentChannelId,
|
||||
channels: {
|
||||
currentChannelId: archived,
|
||||
},
|
||||
},
|
||||
};
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_GET_POSTS_SINCE',
|
||||
];
|
||||
|
||||
mockConfigRequest({ExperimentalViewArchivedChannels: 'false'});
|
||||
mockTeamMemberRequest([teamMember]);
|
||||
mockChanelsRequest(team.id, [archived, channel2]);
|
||||
|
||||
let rolesToLoad = Array.from(new Set(me.roles.split(' ').
|
||||
concat(teamMember.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
|
||||
concat(cMember2.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
await testStore.dispatch(Actions.doReconnect(timestamp));
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const actions = testStore.getActions().map((a) => a.type);
|
||||
expect(actions).toEqual(expectedActions);
|
||||
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
|
||||
});
|
||||
|
||||
it('handle doReconnect after user left current team', async () => {
|
||||
const state = {...initialState};
|
||||
state.entities.teams.myMembers = {};
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
'BATCH_WS_LEAVE_TEAM',
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_GET_POSTS_SINCE',
|
||||
];
|
||||
|
||||
mockConfigRequest();
|
||||
mockTeamMemberRequest([]);
|
||||
mockChanelsRequest(team.id, [channel1, channel2]);
|
||||
|
||||
let rolesToLoad = me.roles.split(' ');
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
|
||||
concat(cMember2.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
await testStore.dispatch(Actions.doReconnect(timestamp));
|
||||
await TestHelper.wait(300);
|
||||
const actions = testStore.getActions().map((a) => a.type);
|
||||
expect(actions).toEqual(expectedActions);
|
||||
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions.Websocket notVisibleUsersActions', () => {
|
||||
configureMockStore([thunk]);
|
||||
|
||||
const me = TestHelper.fakeUserWithId();
|
||||
const user = TestHelper.fakeUserWithId();
|
||||
const user2 = TestHelper.fakeUserWithId();
|
||||
const user3 = TestHelper.fakeUserWithId();
|
||||
const user4 = TestHelper.fakeUserWithId();
|
||||
const user5 = TestHelper.fakeUserWithId();
|
||||
|
||||
it('should do nothing if the known users and the profiles list are the same', async () => {
|
||||
const profiles = {
|
||||
[me.id]: me,
|
||||
[user.id]: user,
|
||||
[user2.id]: user2,
|
||||
[user3.id]: user3,
|
||||
};
|
||||
Client4.serverVersion = '5.23.0';
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: me.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockGetKnownUsersRequest([user.id, user2.id, user3.id]);
|
||||
|
||||
const actions = await notVisibleUsersActions(state);
|
||||
expect(actions.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should do nothing if there are known users in my memberships but not in the profiles list', async () => {
|
||||
const profiles = {
|
||||
[me.id]: me,
|
||||
[user3.id]: user3,
|
||||
};
|
||||
Client4.serverVersion = '5.23.0';
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: me.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockGetKnownUsersRequest([user.id, user2.id, user3.id]);
|
||||
|
||||
const actions = await notVisibleUsersActions(state);
|
||||
expect(actions.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should remove the users if there are unknown users in the profiles list', async () => {
|
||||
const profiles = {
|
||||
[me.id]: me,
|
||||
[user.id]: user,
|
||||
[user2.id]: user2,
|
||||
[user3.id]: user3,
|
||||
[user4.id]: user4,
|
||||
[user5.id]: user5,
|
||||
};
|
||||
Client4.serverVersion = '5.23.0';
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: me.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockGetKnownUsersRequest([user.id, user3.id]);
|
||||
|
||||
const expectedAction = [
|
||||
{type: UserTypes.PROFILE_NO_LONGER_VISIBLE, data: {user_id: user2.id}},
|
||||
{type: UserTypes.PROFILE_NO_LONGER_VISIBLE, data: {user_id: user4.id}},
|
||||
{type: UserTypes.PROFILE_NO_LONGER_VISIBLE, data: {user_id: user5.id}},
|
||||
];
|
||||
const actions = await notVisibleUsersActions(state);
|
||||
expect(actions.length).toEqual(3);
|
||||
expect(actions).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should do nothing if the server version is less than 5.23', async () => {
|
||||
const profiles = {
|
||||
[me.id]: me,
|
||||
[user.id]: user,
|
||||
[user2.id]: user2,
|
||||
[user3.id]: user3,
|
||||
[user4.id]: user4,
|
||||
[user5.id]: user5,
|
||||
};
|
||||
Client4.serverVersion = '5.22.0';
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: me.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockGetKnownUsersRequest([user.id, user3.id]);
|
||||
|
||||
const actions = await notVisibleUsersActions(state);
|
||||
expect(actions.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions.Websocket handleUserTypingEvent', () => {
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
const currentUserId = 'user-id';
|
||||
const otherUserId = 'other-user-id';
|
||||
const currentChannelId = 'channel-id';
|
||||
const otherChannelId = 'other-channel-id';
|
||||
|
||||
const initialState = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
channels: {
|
||||
currentChannelId,
|
||||
channels: {
|
||||
currentChannelId: {
|
||||
id: currentChannelId,
|
||||
name: 'channel',
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {
|
||||
[currentUserId]: {},
|
||||
[otherUserId]: {},
|
||||
},
|
||||
statuses: {
|
||||
[currentUserId]: General.ONLINE,
|
||||
[otherUserId]: General.OFFLINE,
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('dispatches actions for current channel if other user is typing', async () => {
|
||||
const state = {...initialState};
|
||||
const testStore = await mockStore(state);
|
||||
const msg = {broadcast: {channel_id: currentChannelId}, data: {parent_id: 'parent-id', user_id: otherUserId}};
|
||||
|
||||
nock(Client4.getUsersRoute()).
|
||||
post('/status/ids', JSON.stringify([otherUserId])).
|
||||
reply(200, ['away']);
|
||||
|
||||
const expectedActionsTypes = [
|
||||
WebsocketEvents.TYPING,
|
||||
UserTypes.RECEIVED_STATUSES,
|
||||
];
|
||||
|
||||
await testStore.dispatch(Actions.handleUserTypingEvent(msg));
|
||||
await TestHelper.wait(300);
|
||||
const actionTypes = testStore.getActions().map((action) => action.type);
|
||||
expect(actionTypes).toEqual(expectedActionsTypes);
|
||||
});
|
||||
|
||||
it('does not dispatch actions for non current channel', async () => {
|
||||
const state = {...initialState};
|
||||
const testStore = await mockStore(state);
|
||||
const msg = {broadcast: {channel_id: otherChannelId}, data: {parent_id: 'parent-id', user_id: otherUserId}};
|
||||
|
||||
const expectedActionsTypes = [];
|
||||
|
||||
await testStore.dispatch(Actions.handleUserTypingEvent(msg));
|
||||
const actionTypes = testStore.getActions().map((action) => action.type);
|
||||
expect(actionTypes).toEqual(expectedActionsTypes);
|
||||
});
|
||||
});
|
||||
@@ -1,43 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FormattedTime should fallback to default short format for unsupported locale of react-intl 1`] = `
|
||||
<Text>
|
||||
8:47 AM
|
||||
</Text>
|
||||
`;
|
||||
|
||||
exports[`FormattedTime should fallback to default short format for unsupported locale of react-intl 2`] = `
|
||||
<Text>
|
||||
8:47
|
||||
</Text>
|
||||
`;
|
||||
|
||||
exports[`FormattedTime should render correctly 1`] = `
|
||||
<Text>
|
||||
7:02 PM
|
||||
</Text>
|
||||
`;
|
||||
|
||||
exports[`FormattedTime should render correctly 2`] = `
|
||||
<Text>
|
||||
19:02
|
||||
</Text>
|
||||
`;
|
||||
|
||||
exports[`FormattedTime should support localization 1`] = `
|
||||
<Text>
|
||||
7:02 PM
|
||||
</Text>
|
||||
`;
|
||||
|
||||
exports[`FormattedTime should support localization 2`] = `
|
||||
<Text>
|
||||
오후 7:02
|
||||
</Text>
|
||||
`;
|
||||
|
||||
exports[`FormattedTime should support localization 3`] = `
|
||||
<Text>
|
||||
19:02
|
||||
</Text>
|
||||
<View
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={true}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text>
|
||||
7:02 PM
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
exports[`profile_picture_button should match snapshot 1`] = `
|
||||
<AttachmentButton
|
||||
blurTextBox={[MockFunction]}
|
||||
browseFileTypes="public.item"
|
||||
canBrowseFiles={true}
|
||||
canBrowsePhotoLibrary={true}
|
||||
|
||||
@@ -1,133 +1,64 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AnnouncementBanner should match snapshot 1`] = `
|
||||
<Unknown
|
||||
bannerColor="#ddd"
|
||||
bannerDismissed={false}
|
||||
bannerEnabled={true}
|
||||
bannerText="Banner Text"
|
||||
bannerTextColor="#fff"
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 10,
|
||||
"position": "absolute",
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
}
|
||||
Object {
|
||||
"backgroundColor": "#ddd",
|
||||
"height": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
>
|
||||
<Component
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
},
|
||||
null,
|
||||
]
|
||||
}
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"marginRight": 5,
|
||||
},
|
||||
Object {
|
||||
"color": "#fff",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<RemoveMarkdown
|
||||
value="Banner Text"
|
||||
/>
|
||||
</Text>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="#fff"
|
||||
name="info"
|
||||
size={16}
|
||||
/>
|
||||
</Component>
|
||||
</ForwardRef(AnimatedComponentWrapper)>
|
||||
`;
|
||||
|
||||
exports[`AnnouncementBanner should match snapshot 2`] = `
|
||||
<Unknown
|
||||
bannerColor="#ddd"
|
||||
bannerDismissed={false}
|
||||
bannerEnabled={false}
|
||||
bannerText="Banner Text"
|
||||
bannerTextColor="#fff"
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
exports[`AnnouncementBanner should match snapshot 2`] = `null`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
@@ -9,25 +9,53 @@ import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import {injectIntl} from 'react-intl';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {intlShape} from 'react-intl';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import {goToScreen} from '@actions/navigation';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import RemoveMarkdown from '@components/remove_markdown';
|
||||
import {ViewTypes} from '@constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import RemoveMarkdown from 'app/components/remove_markdown';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
import {goToScreen} from 'app/actions/navigation';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
|
||||
const AnnouncementBanner = injectIntl((props) => {
|
||||
const {bannerColor, bannerDismissed, bannerEnabled, bannerText, bannerTextColor, intl} = props;
|
||||
const insets = useSafeAreaInsets();
|
||||
const translateY = useRef(new Animated.Value(0)).current;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [navHeight, setNavHeight] = useState(0);
|
||||
export default class AnnouncementBanner extends PureComponent {
|
||||
static propTypes = {
|
||||
bannerColor: PropTypes.string,
|
||||
bannerDismissed: PropTypes.bool,
|
||||
bannerEnabled: PropTypes.bool,
|
||||
bannerText: PropTypes.string,
|
||||
bannerTextColor: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
state = {
|
||||
bannerHeight: new Animated.Value(0),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const {bannerDismissed, bannerEnabled, bannerText} = this.props;
|
||||
const showBanner = bannerEnabled && !bannerDismissed && Boolean(bannerText);
|
||||
this.toggleBanner(showBanner);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.bannerText !== prevProps.bannerText ||
|
||||
this.props.bannerEnabled !== prevProps.bannerEnabled ||
|
||||
this.props.bannerDismissed !== prevProps.bannerDismissed
|
||||
) {
|
||||
const showBanner = this.props.bannerEnabled && !this.props.bannerDismissed && Boolean(this.props.bannerText);
|
||||
this.toggleBanner(showBanner);
|
||||
}
|
||||
}
|
||||
|
||||
handlePress = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
const handlePress = () => {
|
||||
const screen = 'ExpandedAnnouncementBanner';
|
||||
const title = intl.formatMessage({
|
||||
id: 'mobile.announcement_banner.title',
|
||||
@@ -37,88 +65,70 @@ const AnnouncementBanner = injectIntl((props) => {
|
||||
goToScreen(screen, title);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleNavbarHeight = (height) => {
|
||||
setNavHeight(height);
|
||||
toggleBanner = (show = true) => {
|
||||
const value = show ? 38 : 0;
|
||||
Animated.timing(this.state.bannerHeight, {
|
||||
toValue: value,
|
||||
duration: 350,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.bannerEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {bannerHeight} = this.state;
|
||||
const {
|
||||
bannerColor,
|
||||
bannerText,
|
||||
bannerTextColor,
|
||||
isLandscape,
|
||||
} = this.props;
|
||||
|
||||
const bannerStyle = {
|
||||
backgroundColor: bannerColor,
|
||||
height: bannerHeight,
|
||||
};
|
||||
|
||||
EventEmitter.on(ViewTypes.CHANNEL_NAV_BAR_CHANGED, handleNavbarHeight);
|
||||
const bannerTextStyle = {
|
||||
color: bannerTextColor,
|
||||
};
|
||||
|
||||
return () => EventEmitter.off(ViewTypes.CHANNEL_NAV_BAR_CHANGED, handleNavbarHeight);
|
||||
}, [insets]);
|
||||
|
||||
useEffect(() => {
|
||||
const showBanner = bannerEnabled && !bannerDismissed && Boolean(bannerText);
|
||||
setVisible(showBanner);
|
||||
EventEmitter.emit(ViewTypes.INDICATOR_BAR_VISIBLE, showBanner);
|
||||
}, [bannerDismissed, bannerEnabled, bannerText]);
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(translateY, {
|
||||
toValue: visible ? navHeight : insets.top,
|
||||
duration: 50,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [visible, navHeight]);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bannerStyle = {
|
||||
backgroundColor: bannerColor,
|
||||
height: ViewTypes.INDICATOR_BAR_HEIGHT,
|
||||
transform: [{translateY}],
|
||||
};
|
||||
|
||||
const bannerTextStyle = {
|
||||
color: bannerTextColor,
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatedView
|
||||
style={[style.bannerContainer, bannerStyle]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
style={[style.wrapper, {marginLeft: insets.left, marginRight: insets.right}]}
|
||||
return (
|
||||
<AnimatedView
|
||||
style={[style.bannerContainer, bannerStyle]}
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={[style.bannerText, bannerTextStyle]}
|
||||
<TouchableOpacity
|
||||
onPress={this.handlePress}
|
||||
style={[style.wrapper, padding(isLandscape)]}
|
||||
>
|
||||
<RemoveMarkdown value={bannerText}/>
|
||||
</Text>
|
||||
<CompassIcon
|
||||
color={bannerTextColor}
|
||||
name='information-outline'
|
||||
size={16}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</AnimatedView>
|
||||
);
|
||||
});
|
||||
|
||||
AnnouncementBanner.propTypes = {
|
||||
bannerColor: PropTypes.string,
|
||||
bannerDismissed: PropTypes.bool,
|
||||
bannerEnabled: PropTypes.bool,
|
||||
bannerText: PropTypes.string,
|
||||
bannerTextColor: PropTypes.string,
|
||||
};
|
||||
|
||||
export default AnnouncementBanner;
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={[style.bannerText, bannerTextStyle]}
|
||||
>
|
||||
<RemoveMarkdown value={bannerText}/>
|
||||
</Text>
|
||||
<MaterialIcons
|
||||
color={bannerTextColor}
|
||||
name='info'
|
||||
size={16}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</AnimatedView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
bannerContainer: {
|
||||
elevation: 2,
|
||||
paddingHorizontal: 10,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
zIndex: 2,
|
||||
},
|
||||
wrapper: {
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
@@ -18,10 +18,11 @@ describe('AnnouncementBanner', () => {
|
||||
bannerText: 'Banner Text',
|
||||
bannerTextColor: '#fff',
|
||||
theme: Preferences.THEMES.default,
|
||||
isLandscape: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
const wrapper = shallow(
|
||||
<AnnouncementBanner {...baseProps}/>,
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
@@ -20,6 +21,7 @@ function mapStateToProps(state) {
|
||||
bannerEnabled: config.EnableBanner === 'true' && license.IsLicensed === 'true',
|
||||
bannerText: config.BannerText,
|
||||
bannerTextColor: config.BannerTextColor || '#000',
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
};
|
||||
}
|
||||
|
||||
44
app/components/app_icon.js
Normal file
44
app/components/app_icon.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Svg, {
|
||||
G,
|
||||
Path,
|
||||
} from 'react-native-svg';
|
||||
|
||||
export default class AppIcon extends PureComponent {
|
||||
static propTypes = {
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Svg
|
||||
height={this.props.height}
|
||||
width={this.props.width}
|
||||
viewBox='0 0 500 500'
|
||||
>
|
||||
<G id='XMLID_1_'>
|
||||
<G id='XMLID_3_'>
|
||||
<Path
|
||||
id='XMLID_4_'
|
||||
class='st0'
|
||||
d='M396.9,47.7l2.6,53.1c43,47.5,60,114.8,38.6,178.1c-32,94.4-137.4,144.1-235.4,110.9 S51.1,253.1,83,158.7C104.5,95.2,159.2,52,222.5,40.5l34.2-40.4C150-2.8,49.3,63.4,13.3,169.9C-31,300.6,39.1,442.5,169.9,486.7 s272.6-25.8,316.9-156.6C522.7,223.9,483.1,110.3,396.9,47.7z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
</G>
|
||||
<Path
|
||||
id='XMLID_2_'
|
||||
class='st0'
|
||||
d='M335.6,204.3l-1.8-74.2l-1.5-42.7l-1-37c0,0,0.2-17.8-0.4-22c-0.1-0.9-0.4-1.6-0.7-2.2 c0-0.1-0.1-0.2-0.1-0.3c0-0.1-0.1-0.2-0.1-0.2c-0.7-1.2-1.8-2.1-3.1-2.6c-1.4-0.5-2.9-0.4-4.2,0.2c0,0-0.1,0-0.1,0 c-0.2,0.1-0.3,0.1-0.4,0.2c-0.6,0.3-1.2,0.7-1.8,1.3c-3,3-13.7,17.2-13.7,17.2l-23.2,28.8l-27.1,33l-46.5,57.8 c0,0-21.3,26.6-16.6,59.4s29.1,48.7,48,55.1c18.9,6.4,48,8.5,71.6-14.7C336.4,238.4,335.6,204.3,335.6,204.3z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
</G>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
|
||||
const style = StyleSheet.create({
|
||||
info: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
version: {
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
|
||||
const AppVersion = () => {
|
||||
return (
|
||||
<View pointerEvents='none'>
|
||||
<View style={style.info}>
|
||||
<FormattedText
|
||||
id='mobile.about.appVersion'
|
||||
defaultMessage='App Version: {version} (Build {number})'
|
||||
style={style.version}
|
||||
values={{
|
||||
version: DeviceInfo.getVersion(),
|
||||
number: DeviceInfo.getBuildNumber(),
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppVersion;
|
||||
@@ -1,20 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AtMention should match snapshot, no highlight 1`] = `
|
||||
<Text
|
||||
style={Object {}}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "yellow",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
@John.Smith
|
||||
</Text>
|
||||
<Text>
|
||||
@John.Smith
|
||||
</Text>
|
||||
`;
|
||||
|
||||
@@ -22,22 +10,13 @@ exports[`AtMention should match snapshot, with highlight 1`] = `
|
||||
<Text
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#ff0000",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "yellow",
|
||||
},
|
||||
]
|
||||
}
|
||||
style={null}
|
||||
>
|
||||
@John.Smith
|
||||
</Text>
|
||||
|
||||
</Text>
|
||||
`;
|
||||
|
||||
@@ -45,18 +24,16 @@ exports[`AtMention should match snapshot, without highlight 1`] = `
|
||||
<Text
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#ff0000",
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"color": "#ff0000",
|
||||
}
|
||||
}
|
||||
>
|
||||
@Victor.Welch
|
||||
</Text>
|
||||
|
||||
</Text>
|
||||
`;
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {StyleSheet, Text} from 'react-native';
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
import {Clipboard, Text} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
|
||||
import {showModal} from '@actions/navigation';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomPropTypes from '@constants/custom_prop_types';
|
||||
import BottomSheet from '@utils/bottom_sheet';
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import BottomSheet from 'app/utils/bottom_sheet';
|
||||
import {goToScreen} from 'app/actions/navigation';
|
||||
|
||||
export default class AtMention extends React.PureComponent {
|
||||
static propTypes = {
|
||||
@@ -25,7 +24,6 @@ export default class AtMention extends React.PureComponent {
|
||||
teammateNameDisplay: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
usersByUsername: PropTypes.object.isRequired,
|
||||
groupsByName: PropTypes.object,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -47,29 +45,15 @@ export default class AtMention extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
goToUserProfile = async () => {
|
||||
goToUserProfile = () => {
|
||||
const {intl} = this.context;
|
||||
const {theme} = this.props;
|
||||
const screen = 'UserProfile';
|
||||
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
||||
const passProps = {
|
||||
userId: this.state.user.id,
|
||||
};
|
||||
|
||||
if (!this.closeButton) {
|
||||
this.closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
}
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-settings',
|
||||
icon: this.closeButton,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
showModal(screen, title, passProps, options);
|
||||
goToScreen(screen, title, passProps);
|
||||
};
|
||||
|
||||
getUserDetailsFromMentionName() {
|
||||
@@ -94,12 +78,6 @@ export default class AtMention extends React.PureComponent {
|
||||
};
|
||||
}
|
||||
|
||||
getGroupFromMentionName() {
|
||||
const {groupsByName, mentionName} = this.props;
|
||||
const mentionNameTrimmed = mentionName.toLowerCase().replace(/[._-]*$/, '');
|
||||
return groupsByName?.[mentionNameTrimmed] || {};
|
||||
}
|
||||
|
||||
handleLongPress = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
@@ -141,85 +119,24 @@ export default class AtMention extends React.PureComponent {
|
||||
render() {
|
||||
const {isSearchResult, mentionName, mentionStyle, onPostPress, teammateNameDisplay, textStyle, mentionKeys} = this.props;
|
||||
const {user} = this.state;
|
||||
const mentionTextStyle = [];
|
||||
|
||||
let backgroundColor;
|
||||
let canPress = false;
|
||||
let highlighted;
|
||||
let isMention = false;
|
||||
let mention;
|
||||
let onLongPress;
|
||||
let onPress;
|
||||
let suffix;
|
||||
let suffixElement;
|
||||
let styleText;
|
||||
|
||||
if (textStyle) {
|
||||
const {backgroundColor: bg, ...otherStyles} = StyleSheet.flatten(textStyle);
|
||||
backgroundColor = bg;
|
||||
styleText = otherStyles;
|
||||
if (!user.username) {
|
||||
return <Text style={textStyle}>{'@' + mentionName}</Text>;
|
||||
}
|
||||
|
||||
if (user?.username) {
|
||||
suffix = this.props.mentionName.substring(user.username.length);
|
||||
highlighted = mentionKeys.some((item) => item.key.includes(user.username));
|
||||
mention = displayUsername(user, teammateNameDisplay);
|
||||
isMention = true;
|
||||
canPress = true;
|
||||
} else {
|
||||
const group = this.getGroupFromMentionName();
|
||||
if (group.allow_reference) {
|
||||
highlighted = mentionKeys.some((item) => item.key === `@${group.name}`);
|
||||
isMention = true;
|
||||
mention = group.name;
|
||||
suffix = this.props.mentionName.substring(group.name.length);
|
||||
} else {
|
||||
const pattern = new RegExp(/\b(all|channel|here)(?:\.\B|_\b|\b)/, 'i');
|
||||
const mentionMatch = pattern.exec(mentionName);
|
||||
highlighted = true;
|
||||
|
||||
if (mentionMatch) {
|
||||
mention = mentionMatch.length > 1 ? mentionMatch[1] : mentionMatch[0];
|
||||
suffix = mentionName.replace(mention, '');
|
||||
isMention = true;
|
||||
} else {
|
||||
mention = mentionName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canPress) {
|
||||
onLongPress = this.handleLongPress;
|
||||
onPress = isSearchResult ? onPostPress : this.goToUserProfile;
|
||||
}
|
||||
|
||||
if (suffix) {
|
||||
const suffixStyle = {...styleText, color: this.props.theme.centerChannelColor};
|
||||
suffixElement = (
|
||||
<Text style={suffixStyle}>
|
||||
{suffix}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMention) {
|
||||
mentionTextStyle.push(mentionStyle);
|
||||
}
|
||||
|
||||
if (highlighted) {
|
||||
mentionTextStyle.push({backgroundColor});
|
||||
}
|
||||
const suffix = this.props.mentionName.substring(user.username.length);
|
||||
const highlighted = mentionKeys.some((item) => item.key === user.username);
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={styleText}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
style={textStyle}
|
||||
onPress={isSearchResult ? onPostPress : this.goToUserProfile}
|
||||
onLongPress={this.handleLongPress}
|
||||
>
|
||||
<Text style={mentionTextStyle}>
|
||||
{'@' + mention}
|
||||
<Text style={highlighted ? null : mentionStyle}>
|
||||
{'@' + displayUsername(user, teammateNameDisplay)}
|
||||
</Text>
|
||||
{suffixElement}
|
||||
{suffix}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ describe('AtMention', () => {
|
||||
teammateNameDisplay: '',
|
||||
mentionName: 'John.Smith',
|
||||
mentionStyle: {color: '#ff0000'},
|
||||
textStyle: {backgroundColor: 'yellow'},
|
||||
theme: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,23 +3,18 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getUsersByUsername} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {getAllUserMentionKeys} from '@mm-redux/selectors/entities/search';
|
||||
import {getUsersByUsername, getCurrentUserMentionKeys} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {getAllGroupsForReferenceByName} from '@mm-redux/selectors/entities/groups';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
usersByUsername: getUsersByUsername(state),
|
||||
mentionKeys: ownProps.mentionKeys || getAllUserMentionKeys(state),
|
||||
mentionKeys: getCurrentUserMentionKeys(state),
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
groupsByName: getAllGroupsForReferenceByName(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,10 @@ exports[`AttachmentButton should match snapshot 1`] = `
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<CompassIcon
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="rgba(61,60,64,0.9)"
|
||||
name="plus"
|
||||
name="md-add"
|
||||
size={30}
|
||||
style={
|
||||
Object {
|
||||
|
||||
@@ -14,24 +14,24 @@ import RNFetchBlob from 'rn-fetch-blob';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import AndroidOpenSettings from 'react-native-android-open-settings';
|
||||
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {showModalOverCurrentContext} from '@actions/navigation';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {NavigationTypes} from '@constants';
|
||||
import emmProvider from '@init/emm_provider';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {lookupMimeType} from '@mm-redux/utils/file_utils';
|
||||
import {t} from '@utils/i18n';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import emmProvider from 'app/init/emm_provider';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {t} from 'app/utils/i18n';
|
||||
import {showModalOverCurrentContext} from 'app/actions/navigation';
|
||||
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
|
||||
export default class AttachmentButton extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
browseFileTypes: PropTypes.string,
|
||||
validMimeTypes: PropTypes.array,
|
||||
canBrowseFiles: PropTypes.bool,
|
||||
@@ -414,7 +414,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_DRAFT);
|
||||
this.props.blurTextBox();
|
||||
const items = [];
|
||||
|
||||
if (canTakePhoto) {
|
||||
@@ -424,7 +424,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
id: t('mobile.file_upload.camera_photo'),
|
||||
defaultMessage: 'Take Photo',
|
||||
},
|
||||
icon: 'camera-outline',
|
||||
icon: 'camera',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -435,7 +435,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
id: t('mobile.file_upload.camera_video'),
|
||||
defaultMessage: 'Take Video',
|
||||
},
|
||||
icon: 'video-outline',
|
||||
icon: 'video-camera',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -446,7 +446,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
id: t('mobile.file_upload.library'),
|
||||
defaultMessage: 'Photo Library',
|
||||
},
|
||||
icon: 'file-image-outline',
|
||||
icon: 'photo',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -457,7 +457,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
id: t('mobile.file_upload.video'),
|
||||
defaultMessage: 'Video Library',
|
||||
},
|
||||
icon: 'file-video-outline',
|
||||
icon: 'file-video-o',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -468,7 +468,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
id: t('mobile.file_upload.browse'),
|
||||
defaultMessage: 'Browse Files',
|
||||
},
|
||||
icon: 'file-outline',
|
||||
icon: 'file',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -503,11 +503,11 @@ export default class AttachmentButton extends PureComponent {
|
||||
style={style.buttonContainer}
|
||||
type={'opacity'}
|
||||
>
|
||||
<CompassIcon
|
||||
<Icon
|
||||
size={30}
|
||||
style={style.attachIcon}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.9)}
|
||||
name='plus'
|
||||
name='md-add'
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('AttachmentButton', () => {
|
||||
const formatMessage = jest.fn();
|
||||
const baseProps = {
|
||||
theme: Preferences.THEMES.default,
|
||||
blurTextBox: jest.fn(),
|
||||
maxFileSize: 10,
|
||||
uploadFiles: jest.fn(),
|
||||
};
|
||||
|
||||
@@ -9,9 +9,9 @@ import {RequestStatus} from '@mm-redux/constants';
|
||||
|
||||
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
|
||||
import AtMentionItem from 'app/components/autocomplete/at_mention_item';
|
||||
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
|
||||
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
|
||||
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
|
||||
import GroupMentionItem from 'app/components/autocomplete/at_mention_group/at_mention_group';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
@@ -35,9 +35,9 @@ export default class AtMention extends PureComponent {
|
||||
teamMembers: PropTypes.array,
|
||||
theme: PropTypes.object.isRequired,
|
||||
value: PropTypes.string,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
nestedScrollEnabled: PropTypes.bool,
|
||||
useChannelMentions: PropTypes.bool.isRequired,
|
||||
groups: PropTypes.array,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -56,40 +56,71 @@ export default class AtMention extends PureComponent {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {groups, inChannel, outChannel, teamMembers, isSearch, matchTerm, requestStatus} = nextProps;
|
||||
|
||||
// Not invoked, render nothing.
|
||||
if (matchTerm === null) {
|
||||
const {inChannel, outChannel, teamMembers, isSearch, matchTerm, requestStatus} = nextProps;
|
||||
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
|
||||
// if the term changes but is null or the mention has been completed we render this component as null
|
||||
this.setState({
|
||||
mentionComplete: false,
|
||||
sections: [],
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(0);
|
||||
|
||||
return;
|
||||
} else if (matchTerm === null) {
|
||||
// if the terms did not change but is null then we don't need to do anything
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchTerm !== this.props.matchTerm) {
|
||||
const sections = this.buildSections(nextProps);
|
||||
this.setState({
|
||||
sections,
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
|
||||
|
||||
// Update user autocomplete list with results of server request
|
||||
// if the term changed and we haven't made the request do that first
|
||||
const {currentTeamId, currentChannelId} = this.props;
|
||||
const channelId = isSearch ? '' : currentChannelId;
|
||||
this.props.actions.autocompleteUsers(matchTerm, currentTeamId, channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Server request is complete
|
||||
if (
|
||||
groups !== this.props.groups ||
|
||||
(
|
||||
requestStatus !== RequestStatus.STARTED &&
|
||||
(inChannel !== this.props.inChannel || outChannel !== this.props.outChannel || teamMembers !== this.props.teamMembers)
|
||||
)
|
||||
) {
|
||||
const sections = this.buildSections(nextProps);
|
||||
if (requestStatus !== RequestStatus.STARTED &&
|
||||
(inChannel !== this.props.inChannel || outChannel !== this.props.outChannel || teamMembers !== this.props.teamMembers)) {
|
||||
// if the request is complete and the term is not null we show the autocomplete
|
||||
const sections = [];
|
||||
if (isSearch) {
|
||||
sections.push({
|
||||
id: t('mobile.suggestion.members'),
|
||||
defaultMessage: 'Members',
|
||||
data: teamMembers,
|
||||
key: 'teamMembers',
|
||||
});
|
||||
} else {
|
||||
if (inChannel.length) {
|
||||
sections.push({
|
||||
id: t('suggestion.mention.members'),
|
||||
defaultMessage: 'Channel Members',
|
||||
data: inChannel,
|
||||
key: 'inChannel',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.useChannelMentions && this.checkSpecialMentions(matchTerm)) {
|
||||
sections.push({
|
||||
id: t('suggestion.mention.special'),
|
||||
defaultMessage: 'Special Mentions',
|
||||
data: this.getSpecialMentions(),
|
||||
key: 'special',
|
||||
renderItem: this.renderSpecialMentions,
|
||||
});
|
||||
}
|
||||
|
||||
if (outChannel.length) {
|
||||
sections.push({
|
||||
id: t('suggestion.mention.nonmembers'),
|
||||
defaultMessage: 'Not in Channel',
|
||||
data: outChannel,
|
||||
key: 'outChannel',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
sections,
|
||||
});
|
||||
@@ -98,66 +129,6 @@ export default class AtMention extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.sections.length !== this.state.sections.length && this.state.sections.length === 0) {
|
||||
this.props.onResultCountChange(0);
|
||||
}
|
||||
}
|
||||
|
||||
buildSections = (props) => {
|
||||
const {isSearch, inChannel, outChannel, teamMembers, matchTerm, groups} = props;
|
||||
const sections = [];
|
||||
|
||||
if (isSearch) {
|
||||
sections.push({
|
||||
id: t('mobile.suggestion.members'),
|
||||
defaultMessage: 'Members',
|
||||
data: teamMembers,
|
||||
key: 'teamMembers',
|
||||
});
|
||||
} else {
|
||||
if (inChannel.length) {
|
||||
sections.push({
|
||||
id: t('suggestion.mention.members'),
|
||||
defaultMessage: 'Channel Members',
|
||||
data: inChannel,
|
||||
key: 'inChannel',
|
||||
});
|
||||
}
|
||||
|
||||
if (groups.length) {
|
||||
sections.push({
|
||||
id: t('suggestion.mention.groups'),
|
||||
defaultMessage: 'Group Mentions',
|
||||
data: groups,
|
||||
key: 'groups',
|
||||
renderItem: this.renderGroupMentions,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.useChannelMentions && this.checkSpecialMentions(matchTerm)) {
|
||||
sections.push({
|
||||
id: t('suggestion.mention.special'),
|
||||
defaultMessage: 'Special Mentions',
|
||||
data: this.getSpecialMentions(),
|
||||
key: 'special',
|
||||
renderItem: this.renderSpecialMentions,
|
||||
});
|
||||
}
|
||||
|
||||
if (outChannel.length) {
|
||||
sections.push({
|
||||
id: t('suggestion.mention.nonmembers'),
|
||||
defaultMessage: 'Not in Channel',
|
||||
data: outChannel,
|
||||
key: 'outChannel',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
};
|
||||
|
||||
keyExtractor = (item) => {
|
||||
return item.id || item;
|
||||
};
|
||||
@@ -195,24 +166,22 @@ export default class AtMention extends PureComponent {
|
||||
} else {
|
||||
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
|
||||
}
|
||||
|
||||
if (value.length > cursorPosition) {
|
||||
completedDraft += value.substring(cursorPosition);
|
||||
}
|
||||
|
||||
onChangeText(completedDraft);
|
||||
this.setState({
|
||||
sections: [],
|
||||
});
|
||||
this.setState({mentionComplete: true});
|
||||
};
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
const isFirstSection = section.id === this.state.sections[0].id;
|
||||
return (
|
||||
<AutocompleteSectionHeader
|
||||
id={section.id}
|
||||
defaultMessage={section.defaultMessage}
|
||||
theme={this.props.theme}
|
||||
isFirstSection={isFirstSection}
|
||||
isLandscape={this.props.isLandscape}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -220,7 +189,6 @@ export default class AtMention extends PureComponent {
|
||||
renderItem = ({item}) => {
|
||||
return (
|
||||
<AtMentionItem
|
||||
testID={`autocomplete.at_mention.item.${item}`}
|
||||
onPress={this.completeMention}
|
||||
userId={item}
|
||||
/>
|
||||
@@ -240,21 +208,11 @@ export default class AtMention extends PureComponent {
|
||||
);
|
||||
};
|
||||
|
||||
renderGroupMentions = ({item}) => {
|
||||
return (
|
||||
<GroupMentionItem
|
||||
key={`autocomplete-group-${item.name}`}
|
||||
completeHandle={item.name}
|
||||
onPress={this.completeMention}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {maxListHeight, theme, nestedScrollEnabled} = this.props;
|
||||
const {sections} = this.state;
|
||||
if (sections.length === 0) {
|
||||
const {mentionComplete, sections} = this.state;
|
||||
|
||||
if (sections.length === 0 || mentionComplete) {
|
||||
// If we are not in an active state or the mention has been completed return null so nothing is rendered
|
||||
// other components are not blocked.
|
||||
return null;
|
||||
@@ -264,13 +222,13 @@ export default class AtMention extends PureComponent {
|
||||
|
||||
return (
|
||||
<SectionList
|
||||
testID='at_mention_suggestion.list'
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={this.keyExtractor}
|
||||
style={[style.listView, {maxHeight: maxListHeight}]}
|
||||
sections={sections}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
ItemSeparatorComponent={AutocompleteDivider}
|
||||
initialNumToRender={10}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
/>
|
||||
@@ -282,7 +240,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
listView: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderRadius: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,10 +6,9 @@ import {connect} from 'react-redux';
|
||||
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {autocompleteUsers} from '@mm-redux/actions/users';
|
||||
import {getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {getCurrentChannelId, getDefaultChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getAssociatedGroupsForReference, searchAssociatedGroupsForReferenceLocal} from '@mm-redux/selectors/entities/groups';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import {
|
||||
filterMembersInChannel,
|
||||
@@ -27,16 +26,13 @@ import AtMention from './at_mention';
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {cursorPosition, isSearch} = ownProps;
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const license = getLicense(state);
|
||||
const hasLicense = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
|
||||
|
||||
let useChannelMentions = true;
|
||||
if (isMinimumServerVersion(state.entities.general.serverVersion, 5, 22)) {
|
||||
useChannelMentions = haveIChannelPermission(
|
||||
state,
|
||||
{
|
||||
channel: currentChannelId,
|
||||
team: currentTeamId,
|
||||
permission: Permissions.USE_CHANNEL_MENTIONS,
|
||||
default: true,
|
||||
},
|
||||
@@ -49,7 +45,6 @@ function mapStateToProps(state, ownProps) {
|
||||
let teamMembers;
|
||||
let inChannel;
|
||||
let outChannel;
|
||||
let groups = [];
|
||||
if (isSearch) {
|
||||
teamMembers = filterMembersInCurrentTeam(state, matchTerm);
|
||||
} else {
|
||||
@@ -57,17 +52,9 @@ function mapStateToProps(state, ownProps) {
|
||||
outChannel = filterMembersNotInChannel(state, matchTerm);
|
||||
}
|
||||
|
||||
if (haveIChannelPermission(state, {channel: currentChannelId, team: currentTeamId, permission: Permissions.USE_GROUP_MENTIONS, default: true}) && hasLicense && isMinimumServerVersion(state.entities.general.serverVersion, 5, 24)) {
|
||||
if (matchTerm) {
|
||||
groups = searchAssociatedGroupsForReferenceLocal(state, matchTerm, currentTeamId, currentChannelId);
|
||||
} else {
|
||||
groups = getAssociatedGroupsForReference(state, currentTeamId, currentChannelId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
currentChannelId,
|
||||
currentTeamId,
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
defaultChannel: getDefaultChannel(state),
|
||||
matchTerm,
|
||||
teamMembers,
|
||||
@@ -75,8 +62,8 @@ function mapStateToProps(state, ownProps) {
|
||||
outChannel,
|
||||
requestStatus: state.requests.users.autocompleteUsers.status,
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
useChannelMentions,
|
||||
groups,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
row: {
|
||||
paddingVertical: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
rowPicture: {
|
||||
marginHorizontal: 8,
|
||||
width: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
rowIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.7),
|
||||
fontSize: 14,
|
||||
},
|
||||
rowUsername: {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
rowFullname: {
|
||||
color: theme.centerChannelColor,
|
||||
flex: 1,
|
||||
opacity: 0.6,
|
||||
},
|
||||
textWrapper: {
|
||||
flex: 1,
|
||||
flexWrap: 'wrap',
|
||||
paddingRight: 8,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const GroupMentionItem = (props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const {onPress, completeHandle, theme} = props;
|
||||
|
||||
const completeMention = () => {
|
||||
onPress(completeHandle);
|
||||
};
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={completeMention}
|
||||
style={[style.row, {marginLeft: insets.left, marginRight: insets.right}]}
|
||||
type={'opacity'}
|
||||
>
|
||||
<View style={style.rowPicture}>
|
||||
<CompassIcon
|
||||
name='account-group-outline'
|
||||
style={style.rowIcon}
|
||||
/>
|
||||
</View>
|
||||
<Text style={style.rowUsername}>{`@${completeHandle}`}</Text>
|
||||
<Text style={style.rowUsername}>{' - '}</Text>
|
||||
<Text style={style.rowFullname}>{`${completeHandle}`}</Text>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
GroupMentionItem.propTypes = {
|
||||
completeHandle: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default GroupMentionItem;
|
||||
@@ -1,106 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {BotTag, GuestTag} from '@components/tag';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
import {BotTag, GuestTag} from 'app/components/tag';
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
row: {
|
||||
height: 40,
|
||||
paddingVertical: 8,
|
||||
paddingTop: 4,
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
rowPicture: {
|
||||
marginRight: 10,
|
||||
marginLeft: 2,
|
||||
width: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
rowFullname: {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor,
|
||||
paddingLeft: 4,
|
||||
},
|
||||
rowUsername: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
opacity: 0.56,
|
||||
flex: 1,
|
||||
},
|
||||
export default class AtMentionItem extends PureComponent {
|
||||
static propTypes = {
|
||||
firstName: PropTypes.string,
|
||||
lastName: PropTypes.string,
|
||||
nickname: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
username: PropTypes.string,
|
||||
isGuest: PropTypes.bool,
|
||||
isBot: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
isCurrentUser: PropTypes.bool.isRequired,
|
||||
};
|
||||
});
|
||||
|
||||
const AtMentionItem = (props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const {
|
||||
firstName,
|
||||
isBot,
|
||||
isCurrentUser,
|
||||
isGuest,
|
||||
lastName,
|
||||
nickname,
|
||||
onPress,
|
||||
showFullName,
|
||||
testID,
|
||||
theme,
|
||||
userId,
|
||||
username,
|
||||
} = props;
|
||||
static defaultProps = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
};
|
||||
|
||||
const completeMention = () => {
|
||||
completeMention = () => {
|
||||
const {onPress, username} = this.props;
|
||||
onPress(username);
|
||||
};
|
||||
|
||||
const renderNameBlock = () => {
|
||||
let name = '';
|
||||
render() {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
nickname,
|
||||
userId,
|
||||
username,
|
||||
theme,
|
||||
isBot,
|
||||
isLandscape,
|
||||
isGuest,
|
||||
isCurrentUser,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
const hasFullName = firstName.length > 0 && lastName.length > 0;
|
||||
const hasNickname = nickname.length > 0;
|
||||
|
||||
if (showFullName === 'true') {
|
||||
name += `${firstName} ${lastName} `;
|
||||
}
|
||||
|
||||
if (hasNickname) {
|
||||
name += `(${nickname})`;
|
||||
}
|
||||
|
||||
return name.trim();
|
||||
};
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
const name = renderNameBlock();
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
testID={testID}
|
||||
key={userId}
|
||||
onPress={completeMention}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
style={{marginLeft: insets.left, marginRight: insets.right}}
|
||||
type={'native'}
|
||||
>
|
||||
<View style={style.row}>
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
key={userId}
|
||||
onPress={this.completeMention}
|
||||
style={[style.row, padding(isLandscape)]}
|
||||
type={'opacity'}
|
||||
>
|
||||
<View style={style.rowPicture}>
|
||||
<ProfilePicture
|
||||
userId={userId}
|
||||
theme={theme}
|
||||
size={24}
|
||||
size={20}
|
||||
status={null}
|
||||
showStatus={false}
|
||||
/>
|
||||
</View>
|
||||
<Text style={style.rowUsername}>{`@${username}`}</Text>
|
||||
<BotTag
|
||||
show={isBot}
|
||||
theme={theme}
|
||||
@@ -109,48 +82,46 @@ const AtMentionItem = (props) => {
|
||||
show={isGuest}
|
||||
theme={theme}
|
||||
/>
|
||||
{Boolean(name.length) &&
|
||||
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
|
||||
<Text
|
||||
style={style.rowFullname}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{name}
|
||||
{hasFullName && `${firstName} ${lastName}`}
|
||||
{hasNickname && ` (${nickname}) `}
|
||||
{isCurrentUser &&
|
||||
<FormattedText
|
||||
id='suggestion.mention.you'
|
||||
defaultMessage='(you)'
|
||||
/>}
|
||||
</Text>
|
||||
}
|
||||
<Text
|
||||
style={style.rowUsername}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{` @${username}`}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AtMentionItem.propTypes = {
|
||||
firstName: PropTypes.string,
|
||||
lastName: PropTypes.string,
|
||||
nickname: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
username: PropTypes.string,
|
||||
isGuest: PropTypes.bool,
|
||||
isBot: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isCurrentUser: PropTypes.bool.isRequired,
|
||||
showFullName: PropTypes.string,
|
||||
testID: PropTypes.string,
|
||||
};
|
||||
|
||||
AtMentionItem.defaultProps = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
};
|
||||
|
||||
export default AtMentionItem;
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
row: {
|
||||
paddingVertical: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
rowPicture: {
|
||||
marginHorizontal: 8,
|
||||
width: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
rowUsername: {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
rowFullname: {
|
||||
color: theme.centerChannelColor,
|
||||
opacity: 0.6,
|
||||
flex: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,25 +3,27 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {isGuest} from '@utils/users';
|
||||
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import AtMentionItem from './at_mention_item';
|
||||
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import {isGuest} from 'app/utils/users';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const user = getUser(state, ownProps.userId);
|
||||
const config = getConfig(state);
|
||||
|
||||
return {
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
nickname: user.nickname,
|
||||
username: user.username,
|
||||
showFullName: config.ShowFullName,
|
||||
isBot: Boolean(user.is_bot),
|
||||
isGuest: isGuest(user),
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
isCurrentUser: getCurrentUserId(state) === user.id,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@ import {
|
||||
Keyboard,
|
||||
Platform,
|
||||
View,
|
||||
ViewPropTypes,
|
||||
} from 'react-native';
|
||||
|
||||
import {DeviceTypes} from '@constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
import ChannelMention from './channel_mention';
|
||||
import EmojiSuggestion from './emoji_suggestion';
|
||||
@@ -35,10 +35,8 @@ export default class Autocomplete extends PureComponent {
|
||||
valueEvent: PropTypes.string,
|
||||
cursorPositionEvent: PropTypes.string,
|
||||
nestedScrollEnabled: PropTypes.bool,
|
||||
expandDown: PropTypes.bool,
|
||||
onVisible: PropTypes.func,
|
||||
offsetY: PropTypes.number,
|
||||
onKeyboardOffsetChanged: PropTypes.func,
|
||||
style: ViewPropTypes.style,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -47,8 +45,6 @@ export default class Autocomplete extends PureComponent {
|
||||
enableDateSuggestion: false,
|
||||
nestedScrollEnabled: false,
|
||||
onVisible: emptyFunction,
|
||||
onKeyboardOffsetChanged: emptyFunction,
|
||||
offsetY: 80,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
@@ -151,12 +147,10 @@ export default class Autocomplete extends PureComponent {
|
||||
keyboardDidShow = (e) => {
|
||||
const {height} = e.endCoordinates;
|
||||
this.setState({keyboardOffset: height});
|
||||
this.props.onKeyboardOffsetChanged(height);
|
||||
};
|
||||
|
||||
keyboardDidHide = () => {
|
||||
this.setState({keyboardOffset: 0});
|
||||
this.props.onKeyboardOffsetChanged(0);
|
||||
};
|
||||
|
||||
maxListHeight() {
|
||||
@@ -170,44 +164,41 @@ export default class Autocomplete extends PureComponent {
|
||||
offset = 90;
|
||||
}
|
||||
|
||||
maxHeight = (this.props.deviceHeight / 2) - offset;
|
||||
maxHeight = this.props.deviceHeight - offset - this.state.keyboardOffset;
|
||||
}
|
||||
|
||||
return maxHeight;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {atMentionCount, channelMentionCount, emojiCount, commandCount, dateCount, cursorPosition, value} = this.state;
|
||||
const {theme, isSearch, offsetY} = this.props;
|
||||
const {theme, isSearch, expandDown} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
const maxListHeight = this.maxListHeight();
|
||||
|
||||
const wrapperStyles = [];
|
||||
const containerStyles = [style.borders];
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
wrapperStyles.push(style.shadow);
|
||||
}
|
||||
|
||||
const containerStyles = [];
|
||||
if (isSearch) {
|
||||
wrapperStyles.push(style.base, style.searchContainer, {height: maxListHeight});
|
||||
wrapperStyles.push(style.base, style.searchContainer);
|
||||
containerStyles.push(style.content);
|
||||
} else {
|
||||
const containerStyle = {bottom: offsetY};
|
||||
const containerStyle = expandDown ? style.containerExpandDown : style.container;
|
||||
containerStyles.push(style.base, containerStyle);
|
||||
}
|
||||
|
||||
// Hide when there are no active autocompletes
|
||||
if (atMentionCount + channelMentionCount + emojiCount + commandCount + dateCount === 0) {
|
||||
wrapperStyles.push(style.hidden);
|
||||
containerStyles.push(style.hidden);
|
||||
// We always need to render something, but we only draw the borders when we have results to show
|
||||
const {atMentionCount, channelMentionCount, emojiCount, commandCount, dateCount, cursorPosition, value} = this.state;
|
||||
if (atMentionCount + channelMentionCount + emojiCount + commandCount + dateCount > 0) {
|
||||
if (this.props.isSearch) {
|
||||
wrapperStyles.push(style.bordersSearch);
|
||||
} else {
|
||||
containerStyles.push(style.borders);
|
||||
}
|
||||
}
|
||||
|
||||
const maxListHeight = this.maxListHeight();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={wrapperStyles}
|
||||
edges={['left', 'right']}
|
||||
>
|
||||
<View style={wrapperStyles}>
|
||||
<View
|
||||
testID='autocomplete'
|
||||
ref={this.containerRef}
|
||||
style={containerStyles}
|
||||
>
|
||||
@@ -264,37 +255,39 @@ export default class Autocomplete extends PureComponent {
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
base: {
|
||||
left: 8,
|
||||
left: 0,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
right: 0,
|
||||
},
|
||||
borders: {
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
overflow: 'hidden',
|
||||
borderRadius: 4,
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
hidden: {
|
||||
display: 'none',
|
||||
bordersSearch: {
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
},
|
||||
container: {
|
||||
bottom: 0,
|
||||
},
|
||||
containerExpandDown: {
|
||||
top: 0,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
searchContainer: {
|
||||
flex: 1,
|
||||
...Platform.select({
|
||||
android: {
|
||||
top: 42,
|
||||
top: 46,
|
||||
},
|
||||
ios: {
|
||||
top: 55,
|
||||
top: 44,
|
||||
},
|
||||
}),
|
||||
},
|
||||
shadow: {
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 8,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 8,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class AutocompleteDivider extends PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={style.divider}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
},
|
||||
};
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user