forked from Ivasoft/mattermost-mobile
Compare commits
44 Commits
gitpod
...
recent-sea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51425f9bb5 | ||
|
|
49b2a332e5 | ||
|
|
14bd424d18 | ||
|
|
e13e2a618d | ||
|
|
b6fc5abf92 | ||
|
|
423b405e4f | ||
|
|
5b36199976 | ||
|
|
0094fc9a32 | ||
|
|
7902483072 | ||
|
|
1232cf45d8 | ||
|
|
91525b6eb0 | ||
|
|
a3bece2776 | ||
|
|
2afe6fb9d6 | ||
|
|
fe042bf53c | ||
|
|
58370c48cc | ||
|
|
20f262af8d | ||
|
|
a15e76bf3c | ||
|
|
6a03482406 | ||
|
|
27ef5838b4 | ||
|
|
eb01ec1368 | ||
|
|
cca1e160f6 | ||
|
|
f1b05cfcae | ||
|
|
26904aa9e5 | ||
|
|
678d3ed711 | ||
|
|
3df715644c | ||
|
|
13652dfe2d | ||
|
|
efc6cb8cc8 | ||
|
|
c4d2ffe347 | ||
|
|
e6932781b3 | ||
|
|
e134941de8 | ||
|
|
8b7a9b32b1 | ||
|
|
bd87d80baf | ||
|
|
3752f9a516 | ||
|
|
cd16ec6308 | ||
|
|
7429f4799f | ||
|
|
501e03647a | ||
|
|
809a18a87d | ||
|
|
64a2d21a73 | ||
|
|
1eaecfcc8e | ||
|
|
f79ae62ead | ||
|
|
77eb6774c9 | ||
|
|
a8250ec8af | ||
|
|
fd3dea386d | ||
|
|
f1f1fa5484 |
@@ -1,20 +1,20 @@
|
||||
version: 2.1
|
||||
orbs:
|
||||
owasp: entur/owasp@0.0.10
|
||||
node: circleci/node@5.0.3
|
||||
node: circleci/node@5.0.2
|
||||
|
||||
executors:
|
||||
android:
|
||||
parameters:
|
||||
resource_class:
|
||||
default: xlarge
|
||||
default: large
|
||||
type: string
|
||||
environment:
|
||||
NODE_OPTIONS: --max_old_space_size=12000
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
docker:
|
||||
- image: cimg/android:2022.09.2-node
|
||||
- image: cimg/android:2022.03-node
|
||||
working_directory: ~/mattermost-mobile
|
||||
resource_class: <<parameters.resource_class>>
|
||||
|
||||
@@ -28,7 +28,7 @@ executors:
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
macos:
|
||||
xcode: "14.0.0"
|
||||
xcode: "13.3.0"
|
||||
working_directory: ~/mattermost-mobile
|
||||
shell: /bin/bash --login -o pipefail
|
||||
resource_class: <<parameters.resource_class>>
|
||||
@@ -105,7 +105,7 @@ commands:
|
||||
description: "Get JavaScript dependencies"
|
||||
steps:
|
||||
- node/install:
|
||||
node-version: '18.7.0'
|
||||
node-version: '16.14.2'
|
||||
- restore_cache:
|
||||
name: Restore npm cache
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"./eslint/eslint-mattermost",
|
||||
"./eslint/eslint-react",
|
||||
"plugin:mattermost/react",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
@@ -9,6 +8,7 @@
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"mattermost",
|
||||
"import"
|
||||
],
|
||||
"settings": {
|
||||
|
||||
@@ -67,4 +67,4 @@ untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.182.0
|
||||
^0.176.3
|
||||
|
||||
13
.github/codeql/codeql-config.yml
vendored
13
.github/codeql/codeql-config.yml
vendored
@@ -1,13 +0,0 @@
|
||||
name: "CodeQL config"
|
||||
|
||||
query-filters:
|
||||
- exclude:
|
||||
problem.severity:
|
||||
- warning
|
||||
- recommendation
|
||||
- exclude:
|
||||
id: js/insecure-randomness
|
||||
|
||||
paths-ignore:
|
||||
- test
|
||||
- '**/*.test.*'
|
||||
23
.github/workflows/codeql-analysis.yml
vendored
23
.github/workflows/codeql-analysis.yml
vendored
@@ -9,13 +9,8 @@ on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
permissions:
|
||||
security-events: write
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -23,20 +18,26 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -44,8 +44,6 @@ ios/.xcode.env.local
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
android/app/bin
|
||||
android/app/build
|
||||
android/build
|
||||
|
||||
159
.gitpod.Dockerfile
vendored
159
.gitpod.Dockerfile
vendored
@@ -1,159 +0,0 @@
|
||||
FROM gitpod/workspace-full-vnc
|
||||
|
||||
ENV CYPRESS_CACHE_FOLDER=/workspace/.cypress-cache
|
||||
|
||||
# Install Cypress dependencies.
|
||||
RUN sudo apt-get update \
|
||||
&& sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
libgtk2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libnotify-dev \
|
||||
libgconf-2-4 \
|
||||
libnss3 \
|
||||
libxss1 \
|
||||
libasound2 \
|
||||
libxtst6 \
|
||||
xauth \
|
||||
xvfb \
|
||||
&& sudo rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /workspace/persist/.cache/go-build
|
||||
ENV GOCACHE=/workspace/persist/.cache/go-build
|
||||
|
||||
ENV MM_SERVICESETTINGS_ENABLEDEVELOPER=true
|
||||
|
||||
# Copied from https://github.com/react-native-community/docker-android/blob/master/Dockerfile
|
||||
|
||||
LABEL Description="This image provides a base Android development environment for React Native, and may be used to run tests."
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# set default build arguments
|
||||
# https://developer.android.com/studio#command-tools
|
||||
ARG SDK_VERSION=commandlinetools-linux-8512546_latest.zip
|
||||
ARG ANDROID_BUILD_VERSION=31
|
||||
ARG ANDROID_TOOLS_VERSION=31.0.0
|
||||
ARG BUCK_VERSION=2022.05.05.01
|
||||
# Buck doesn't support versions beyond NDK 21
|
||||
# Therefore we need to diverge the NDK version and set NDK_HOME
|
||||
# for Buck to pick it up correctly.
|
||||
ARG NDK_VERSION_BUCK=21.4.7075529
|
||||
ARG NDK_VERSION_GRADLE=23.1.7779620
|
||||
ARG NODE_VERSION=14.x
|
||||
ARG WATCHMAN_VERSION=4.9.0
|
||||
ARG CMAKE_VERSION=3.18.1
|
||||
|
||||
# set default environment variables, please don't remove old env for compatibilty issue
|
||||
ENV ADB_INSTALL_TIMEOUT=10
|
||||
ENV ANDROID_HOME=/opt/android
|
||||
ENV ANDROID_SDK_ROOT=${ANDROID_HOME}
|
||||
ENV ANDROID_NDK_BUCK=${ANDROID_HOME}/ndk/$NDK_VERSION_BUCK
|
||||
ENV ANDROID_NDK_GRADLE=${ANDROID_HOME}/ndk/$NDK_VERSION_GRADLE
|
||||
# this is needed for Buck to be able to recognize NDK 21
|
||||
ENV NDK_HOME=${ANDROID_HOME}/ndk/$NDK_VERSION_BUCK
|
||||
ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
|
||||
ENV CMAKE_BIN_PATH=${ANDROID_HOME}/cmake/$CMAKE_VERSION/bin
|
||||
|
||||
ENV PATH=${CMAKE_BIN_PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/emulator:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:/opt/buck/bin/:${PATH}
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt update -qq && apt install -qq -y --no-install-recommends \
|
||||
apt-transport-https \
|
||||
curl \
|
||||
file \
|
||||
gcc \
|
||||
git \
|
||||
g++ \
|
||||
gnupg2 \
|
||||
libc++1-10 \
|
||||
libgl1 \
|
||||
libtcmalloc-minimal4 \
|
||||
make \
|
||||
openjdk-11-jdk-headless \
|
||||
openssh-client \
|
||||
patch \
|
||||
python3 \
|
||||
python3-distutils \
|
||||
rsync \
|
||||
ruby \
|
||||
ruby-dev \
|
||||
tzdata \
|
||||
unzip \
|
||||
sudo \
|
||||
ninja-build \
|
||||
zip \
|
||||
# Dev libraries requested by Hermes
|
||||
libicu-dev \
|
||||
# Emulator & video bridge dependencies
|
||||
libc6 \
|
||||
libdbus-1-3 \
|
||||
libfontconfig1 \
|
||||
libgcc1 \
|
||||
libpulse0 \
|
||||
libtinfo5 \
|
||||
libx11-6 \
|
||||
libxcb1 \
|
||||
libxdamage1 \
|
||||
libnss3 \
|
||||
libxcomposite1 \
|
||||
libxcursor1 \
|
||||
libxi6 \
|
||||
libxext6 \
|
||||
libxfixes3 \
|
||||
zlib1g \
|
||||
libgl1 \
|
||||
pulseaudio \
|
||||
socat \
|
||||
&& gem install bundler \
|
||||
&& rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
# install nodejs and yarn packages from nodesource
|
||||
RUN curl -sL https://deb.nodesource.com/setup_${NODE_VERSION} | bash - \
|
||||
&& apt-get update -qq \
|
||||
&& apt-get install -qq -y --no-install-recommends nodejs \
|
||||
&& npm i -g yarn \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# download and install buck using the java11 pex from Jitpack
|
||||
RUN curl -L https://jitpack.io/com/github/facebook/buck/v${BUCK_VERSION}/buck-v${BUCK_VERSION}-java11.pex -o /tmp/buck.pex \
|
||||
&& mv /tmp/buck.pex /usr/local/bin/buck \
|
||||
&& chmod +x /usr/local/bin/buck
|
||||
|
||||
# Full reference at https://dl.google.com/android/repository/repository2-1.xml
|
||||
# download and unpack android
|
||||
# workaround buck clang version detection by symlinking
|
||||
RUN curl -sS https://dl.google.com/android/repository/${SDK_VERSION} -o /tmp/sdk.zip \
|
||||
&& mkdir -p ${ANDROID_HOME}/cmdline-tools \
|
||||
&& unzip -q -d ${ANDROID_HOME}/cmdline-tools /tmp/sdk.zip \
|
||||
&& mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest \
|
||||
&& rm /tmp/sdk.zip \
|
||||
&& yes | sdkmanager --licenses \
|
||||
&& yes | sdkmanager "platform-tools" \
|
||||
"emulator" \
|
||||
"platforms;android-$ANDROID_BUILD_VERSION" \
|
||||
"build-tools;$ANDROID_TOOLS_VERSION" \
|
||||
"cmake;$CMAKE_VERSION" \
|
||||
"system-images;android-21;google_apis;armeabi-v7a" \
|
||||
"ndk;$NDK_VERSION_BUCK" \
|
||||
"ndk;$NDK_VERSION_GRADLE" \
|
||||
&& rm -rf ${ANDROID_HOME}/.android \
|
||||
&& chmod 777 -R /opt/android \
|
||||
&& ln -s ${ANDROID_NDK_BUCK}/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/9.0.9 ${ANDROID_NDK_BUCK}/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/9.0.8
|
||||
|
||||
|
||||
|
||||
# Copied from https://github.com/gengjiawen/ci-sample/blob/master/.gitpod.Dockerfile
|
||||
|
||||
# FROM reactnativecommunity/react-native-android
|
||||
|
||||
### Gitpod user ###
|
||||
# '-l': see https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user
|
||||
RUN useradd -l -u 33333 -G sudo -md /home/gitpod -s /bin/bash -p gitpod gitpod \
|
||||
# passwordless sudo for users in the 'sudo' group
|
||||
&& sed -i.bkp -e 's/%sudo\s\+ALL=(ALL\(:ALL\)\?)\s\+ALL/%sudo ALL=NOPASSWD:ALL/g' /etc/sudoers
|
||||
|
||||
# Install custom tools, runtimes, etc.
|
||||
# For example "bastet", a command-line tetris clone:
|
||||
# RUN brew install bastet
|
||||
#
|
||||
# More information: https://www.gitpod.io/docs/config-docker/
|
||||
@@ -1,6 +0,0 @@
|
||||
image:
|
||||
file: .gitpod.Dockerfile
|
||||
|
||||
tasks:
|
||||
- init: npm install
|
||||
- command: npm run android
|
||||
79
.solidarity
79
.solidarity
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/solidaritySchema",
|
||||
"config" : {
|
||||
"output" : "moderate"
|
||||
},
|
||||
"requirements": {
|
||||
"Node": [
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "node",
|
||||
"semver": ">=16.0.0",
|
||||
"error": "install node using nvm https://github.com/nvm-sh/nvm#installing-and-updating"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "npm",
|
||||
"semver": ">=8.5.5 <9.0.0",
|
||||
"error": "install npm 8.5.5 `npm i -g npm@8.5.5"
|
||||
}
|
||||
],
|
||||
"Android": [
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "emulator"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "android"
|
||||
},
|
||||
{
|
||||
"rule": "env",
|
||||
"variable": "ANDROID_HOME",
|
||||
"error": "The ANDROID_HOME environment variable must be set to your local SDK. Refer to getting started docs for help."
|
||||
}
|
||||
],
|
||||
"iOS": [
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "watchman",
|
||||
"error": "install watchman `brew install watchman`",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "xcodebuild",
|
||||
"semver": ">=13.0",
|
||||
"error": "install xcode",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "ruby",
|
||||
"semver": ">=2.7.1 <3.0.0",
|
||||
"error": "visit rvm install https://rvm.io/rvm/install",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "bundler",
|
||||
"semver": "2.1.4",
|
||||
"error": "install watchman `gem install bundler --version 2.1.4`",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "pod",
|
||||
"semver": "1.11.3",
|
||||
"platform": "darwin"
|
||||
}
|
||||
],
|
||||
"Git email": [
|
||||
{
|
||||
"rule": "shell",
|
||||
"command": "git config user.email",
|
||||
"match": ".+@.+"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
6
.storybook/main.js
Normal file
6
.storybook/main.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
"stories": [
|
||||
"../app/components/**/*.stories.mdx",
|
||||
"../app/components/**/*.stories.@(js|jsx|ts|tsx)"
|
||||
],
|
||||
}
|
||||
5
.storybook/preview.js
Normal file
5
.storybook/preview.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
}
|
||||
|
||||
136
NOTICE.txt
136
NOTICE.txt
@@ -165,41 +165,6 @@ React Native TextInput component have functionality to capture text input from a
|
||||
* LICENSE: MIT
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @mattermost/react-native-turbo-log
|
||||
|
||||
This product contains '@mattermost/react-native-turbo-log' by Mattermost, Inc..
|
||||
|
||||
test
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/mattermost/react-native-turbo-log#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Mattermost, Inc.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @msgpack/msgpack
|
||||
@@ -224,18 +189,18 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
|
||||
|
||||
## @nozbe/watermelondb
|
||||
|
||||
This product contains '@nozbe/watermelondb' by Nozbe.
|
||||
This product contains 'cameraroll' by Bartol Karuza.
|
||||
|
||||
Build powerful React and React Native apps that scale from hundreds to tens of thousands of records and remain fast ⚡️
|
||||
React-native native module that provides access to the local camera roll or photo library
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/Nozbe/WatermelonDB/
|
||||
* https://github.com/react-native-community/react-native-cameraroll
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) Nozbe
|
||||
Copyright (c) 2020 Elias Nahum
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -293,14 +258,50 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @react-native-cameraroll/react-native-cameraroll
|
||||
## @react-native-community/art
|
||||
|
||||
This product contains 'react-native-cameraroll' by Bartol Karuza.
|
||||
This product contains '@react-native-community/art' by react-native-art.
|
||||
|
||||
React Native module that allows you to draw vector graphics
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-art/art
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 react-native-community
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @react-native-community/cameraroll
|
||||
|
||||
This product contains 'cameraroll' by Bartol Karuza.
|
||||
|
||||
React-native native module that provides access to the local camera roll or photo library
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-cameraroll/react-native-cameraroll
|
||||
* https://github.com/react-native-community/react-native-cameraroll
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
@@ -329,14 +330,14 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @react-native-clipboard/clipboard
|
||||
## @react-native-community/clipboard
|
||||
|
||||
This product contains '@react-native-clipboard/clipboard' by React Native Community.
|
||||
This product contains '@react-native-community/clipboard' by React Native Community.
|
||||
|
||||
React Native Clipboard API for both iOS and Android
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-clipboard/clipboard
|
||||
* https://github.com/react-native-community/clipboard
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
@@ -2314,20 +2315,20 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-shadow-2
|
||||
## react-native-neomorph-shadows
|
||||
|
||||
This product contains a modified version of 'react-native-shadow-2' by Henrique Bruno Fantauzzi de Almeida.
|
||||
This product contains a modified version of 'react-native-neomorph-shadows' by Daniel.
|
||||
|
||||
Cross-platform shadow for React Native. Supports Android, iOS, Web and Expo.
|
||||
Shadows and neumorphism/neomorphism for iOS & Android (like iOS).
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/SrBrahma/react-native-shadow-2
|
||||
* https://github.com/tokkozhin/react-native-neomorph-shadows
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Henrique Bruno Fantauzzi de Almeida
|
||||
Copyright (c) 2020 tokkozhin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -2921,6 +2922,41 @@ The above copyright notice and this permission notice shall be included in all c
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## rn-placeholder
|
||||
|
||||
This product contains a modified version of 'rn-placeholder' by Marvin FRACHET.
|
||||
|
||||
Display some placeholder stuff before rendering your text or media content in React Native
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/mfrachet/rn-placeholder
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
Copyright (c) 2004-Today Marvin Frachet
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## semver
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: 'kotlin-android'
|
||||
import com.android.build.OutputFile
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
/**
|
||||
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
|
||||
@@ -145,21 +144,33 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 428
|
||||
versionCode 417
|
||||
versionName "2.0.0"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
// We configure the CMake build only if you decide to opt-in for the New Architecture.
|
||||
// We configure the NDK build only if you decide to opt-in for the New Architecture.
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DPROJECT_BUILD_DIR=$buildDir",
|
||||
"-DREACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
|
||||
"-DREACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build",
|
||||
"-DNODE_MODULES_DIR=$rootDir/../node_modules",
|
||||
"-DANDROID_STL=c++_shared"
|
||||
ndkBuild {
|
||||
arguments "APP_PLATFORM=android-21",
|
||||
"APP_STL=c++_shared",
|
||||
"NDK_TOOLCHAIN_VERSION=clang",
|
||||
"GENERATED_SRC_DIR=$buildDir/generated/source",
|
||||
"PROJECT_BUILD_DIR=$buildDir",
|
||||
"REACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
|
||||
"REACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build",
|
||||
"NODE_MODULES_DIR=$rootDir/../node_modules"
|
||||
cFlags "-Wall", "-Werror", "-fexceptions", "-frtti", "-DWITH_INSPECTOR=1"
|
||||
cppFlags "-std=c++17"
|
||||
// Make sure this target name is the same you specify inside the
|
||||
// src/main/jni/Android.mk file for the `LOCAL_MODULE` variable.
|
||||
targets "rndiffapp_appmodules"
|
||||
// Fix for windows limit on number of character in file paths and in command lines
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
arguments "NDK_APP_SHORT_COMMANDS=true"
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!enableSeparateBuildPerCPUArchitecture) {
|
||||
@@ -173,8 +184,8 @@ android {
|
||||
if (isNewArchitectureEnabled()) {
|
||||
// We configure the NDK build only if you decide to opt-in for the New Architecture.
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "$projectDir/src/main/jni/CMakeLists.txt"
|
||||
ndkBuild {
|
||||
path "$projectDir/src/main/jni/Android.mk"
|
||||
}
|
||||
}
|
||||
def reactAndroidProjectDir = project(':ReactAndroid').projectDir
|
||||
@@ -196,15 +207,15 @@ android {
|
||||
preReleaseBuild.dependsOn(packageReactNdkReleaseLibs)
|
||||
|
||||
// Due to a bug inside AGP, we have to explicitly set a dependency
|
||||
// between configureCMakeDebug* tasks and the preBuild tasks.
|
||||
// between configureNdkBuild* tasks and the preBuild tasks.
|
||||
// This can be removed once this is solved: https://issuetracker.google.com/issues/207403732
|
||||
configureCMakeDebugRelease.dependsOn(preReleaseBuild)
|
||||
configureCMakeDebugDebug.dependsOn(preDebugBuild)
|
||||
configureNdkBuildRelease.dependsOn(preReleaseBuild)
|
||||
configureNdkBuildDebug.dependsOn(preDebugBuild)
|
||||
reactNativeArchitectures().each { architecture ->
|
||||
tasks.findByName("configureCMakeDebugDebug[${architecture}]")?.configure {
|
||||
tasks.findByName("configureNdkBuildDebug[${architecture}]")?.configure {
|
||||
dependsOn("preDebugBuild")
|
||||
}
|
||||
tasks.findByName("configureCMakeDebugRelease[${architecture}]")?.configure {
|
||||
tasks.findByName("configureNdkBuildRelease[${architecture}]")?.configure {
|
||||
dependsOn("preReleaseBuild")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,21 +5,10 @@
|
||||
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
||||
android:maxSdkVersion="30" />
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
|
||||
@@ -46,9 +46,6 @@ public class CustomPushNotificationHelper {
|
||||
public static final int MESSAGE_NOTIFICATION_ID = 435345;
|
||||
public static final String NOTIFICATION_ID = "notificationId";
|
||||
public static final String NOTIFICATION = "notification";
|
||||
public static final String PUSH_TYPE_MESSAGE = "message";
|
||||
public static final String PUSH_TYPE_CLEAR = "clear";
|
||||
public static final String PUSH_TYPE_SESSION = "session";
|
||||
|
||||
private static NotificationChannel mHighImportanceChannel;
|
||||
private static NotificationChannel mMinImportanceChannel;
|
||||
@@ -57,30 +54,40 @@ public class CustomPushNotificationHelper {
|
||||
String message = bundle.getString("message", bundle.getString("body"));
|
||||
String senderId = bundle.getString("sender_id");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
String type = bundle.getString("type");
|
||||
if (senderId == null) {
|
||||
senderId = "sender_id";
|
||||
}
|
||||
Bundle userInfoBundle = bundle.getBundle("userInfo");
|
||||
String senderName = getSenderName(bundle);
|
||||
if (userInfoBundle != null) {
|
||||
boolean localPushNotificationTest = userInfoBundle.getBoolean("test");
|
||||
if (localPushNotificationTest) {
|
||||
senderName = "Test";
|
||||
}
|
||||
}
|
||||
|
||||
if (conversationTitle == null || !android.text.TextUtils.isEmpty(senderName.trim())) {
|
||||
message = removeSenderNameFromMessage(message, senderName);
|
||||
}
|
||||
|
||||
long timestamp = new Date().getTime();
|
||||
Person.Builder sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName(senderName);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messagingStyle.addMessage(message, timestamp, senderName);
|
||||
} else {
|
||||
Person.Builder sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName(senderName);
|
||||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, senderId))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
if (serverUrl != null) {
|
||||
try {
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, senderId))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messagingStyle.addMessage(message, timestamp, sender.build());
|
||||
messagingStyle.addMessage(message, timestamp, sender.build());
|
||||
}
|
||||
}
|
||||
|
||||
private static void addNotificationExtras(NotificationCompat.Builder notification, Bundle bundle) {
|
||||
@@ -104,16 +111,6 @@ public class CustomPushNotificationHelper {
|
||||
userInfoBundle.putString("root_id", rootId);
|
||||
}
|
||||
|
||||
String crtEnabled = bundle.getString("is_crt_enabled");
|
||||
if (crtEnabled != null) {
|
||||
userInfoBundle.putString("is_crt_enabled", crtEnabled);
|
||||
}
|
||||
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
if (serverUrl != null) {
|
||||
userInfoBundle.putString("server_url", serverUrl);
|
||||
}
|
||||
|
||||
notification.addExtras(userInfoBundle);
|
||||
}
|
||||
|
||||
@@ -169,7 +166,7 @@ public class CustomPushNotificationHelper {
|
||||
String rootId = bundle.getString("root_id");
|
||||
int notificationId = postId != null ? postId.hashCode() : MESSAGE_NOTIFICATION_ID;
|
||||
|
||||
boolean is_crt_enabled = bundle.containsKey("is_crt_enabled") && bundle.getString("is_crt_enabled").equals("true");
|
||||
Boolean is_crt_enabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true");
|
||||
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
|
||||
|
||||
addNotificationExtras(notification, bundle);
|
||||
@@ -243,10 +240,6 @@ public class CustomPushNotificationHelper {
|
||||
title = bundle.getString("sender_name");
|
||||
}
|
||||
|
||||
if (android.text.TextUtils.isEmpty(title)) {
|
||||
title = bundle.getString("title", "");
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
@@ -260,23 +253,26 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
private static NotificationCompat.MessagingStyle getMessagingStyle(Context context, Bundle bundle) {
|
||||
NotificationCompat.MessagingStyle messagingStyle;
|
||||
final String senderId = "me";
|
||||
String senderId = "me";
|
||||
final String serverUrl = bundle.getString("server_url");
|
||||
final String type = bundle.getString("type");
|
||||
|
||||
Person.Builder sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName("Me");
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messagingStyle = new NotificationCompat.MessagingStyle("Me");
|
||||
} else {
|
||||
Person.Builder sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName("Me");
|
||||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, "me"))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
if (serverUrl != null) {
|
||||
try {
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, "me"))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messagingStyle = new NotificationCompat.MessagingStyle(sender.build());
|
||||
messagingStyle = new NotificationCompat.MessagingStyle(sender.build());
|
||||
}
|
||||
|
||||
String conversationTitle = getConversationTitle(bundle);
|
||||
setMessagingStyleConversationTitle(messagingStyle, conversationTitle, bundle);
|
||||
@@ -364,6 +360,19 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
|
||||
NotificationChannel notificationChannel = mHighImportanceChannel;
|
||||
|
||||
boolean testNotification = false;
|
||||
boolean localNotification = false;
|
||||
Bundle userInfoBundle = bundle.getBundle("userInfo");
|
||||
if (userInfoBundle != null) {
|
||||
testNotification = userInfoBundle.getBoolean("test");
|
||||
localNotification = userInfoBundle.getBoolean("local");
|
||||
}
|
||||
|
||||
if (testNotification || localNotification) {
|
||||
notificationChannel = mMinImportanceChannel;
|
||||
}
|
||||
|
||||
notification.setChannelId(notificationChannel.getId());
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ import java.lang.Exception
|
||||
import java.util.*
|
||||
|
||||
class DatabaseHelper {
|
||||
private var defaultDatabase: Database? = null
|
||||
var defaultDatabase: Database? = null
|
||||
private set
|
||||
|
||||
val onlyServerUrl: String?
|
||||
get() {
|
||||
@@ -82,15 +83,12 @@ class DatabaseHelper {
|
||||
|
||||
fun queryIds(db: Database, tableName: String, ids: Array<String>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(ids).map { "?" }.toArray())
|
||||
val args = TextUtils.join(",", Arrays.stream(ids).map { value: String? -> "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("select distinct id from $tableName where id IN ($args)", ids as Array<Any?>).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex("id")
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
list.add(cursor.getString(cursor.getColumnIndex("id")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,15 +100,12 @@ class DatabaseHelper {
|
||||
|
||||
fun queryByColumn(db: Database, tableName: String, columnName: String, values: Array<Any?>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(values).map { "?" }.toArray())
|
||||
val args = TextUtils.join(",", Arrays.stream(values).map { value: Any? -> "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("select distinct $columnName from $tableName where $columnName IN ($args)", values).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex(columnName)
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
list.add(cursor.getString(cursor.getColumnIndex(columnName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +120,7 @@ class DatabaseHelper {
|
||||
return result.getString("value")
|
||||
}
|
||||
|
||||
private fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
|
||||
fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
|
||||
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
|
||||
@@ -180,7 +175,7 @@ class DatabaseHelper {
|
||||
val firstId = ordered.first()
|
||||
val lastId = ordered.last()
|
||||
lastFetchedAt = postList.fold(0.0) { acc, next ->
|
||||
val post = next.second as Map<*, *>
|
||||
val post = next.second as Map<String, Any?>
|
||||
val createAt = post["create_at"] as Double
|
||||
val updateAt = post["update_at"] as Double
|
||||
val deleteAt = post["delete_at"] as Double
|
||||
@@ -191,38 +186,38 @@ class DatabaseHelper {
|
||||
var prevPostId = ""
|
||||
|
||||
val sortedPosts = postList.sortedBy { (_, value) ->
|
||||
((value as Map<*, *>)["create_at"] as Double)
|
||||
((value as Map<*, *>).get("create_at") as Double)
|
||||
}
|
||||
|
||||
sortedPosts.forEachIndexed { index, it ->
|
||||
val key = it.first
|
||||
if (it.second != null) {
|
||||
val post = it.second as MutableMap<String, Any?>
|
||||
val post = (it.second as MutableMap<String, Any?>)
|
||||
|
||||
if (index == 0) {
|
||||
post.putIfAbsent("prev_post_id", previousPostId)
|
||||
} else if (prevPostId.isNotEmpty()) {
|
||||
} else if (!prevPostId.isNullOrEmpty()) {
|
||||
post.putIfAbsent("prev_post_id", prevPostId)
|
||||
}
|
||||
|
||||
if (lastId == key) {
|
||||
earliest = post["create_at"] as Double
|
||||
earliest = post.get("create_at") as Double
|
||||
}
|
||||
if (firstId == key) {
|
||||
latest = post["create_at"] as Double
|
||||
latest = post.get("create_at") as Double
|
||||
}
|
||||
|
||||
val jsonPost = JSONObject(post)
|
||||
val rootId = post["root_id"] as? String
|
||||
val rootId = post.get("root_id") as? String
|
||||
|
||||
if (!rootId.isNullOrEmpty()) {
|
||||
var thread = postsInThread[rootId]?.toMutableList()
|
||||
var thread = postsInThread.get(rootId)?.toMutableList()
|
||||
if (thread == null) {
|
||||
thread = mutableListOf()
|
||||
}
|
||||
|
||||
thread.add(jsonPost)
|
||||
postsInThread[rootId] = thread.toList()
|
||||
postsInThread.put(rootId, thread.toList())
|
||||
}
|
||||
|
||||
if (find(db, "Post", key) == null) {
|
||||
@@ -313,6 +308,27 @@ class DatabaseHelper {
|
||||
}
|
||||
}
|
||||
|
||||
fun getServerVersion(db: Database): String? {
|
||||
val config = getSystemConfig(db)
|
||||
if (config != null) {
|
||||
return config.getString("Version")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getSystemConfig(db: Database): JSONObject? {
|
||||
val configRecord = find(db, "System", "config")
|
||||
if (configRecord != null) {
|
||||
val value = configRecord.getString("value");
|
||||
try {
|
||||
return JSONObject(value)
|
||||
} catch(e: JSONException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun setDefaultDatabase(context: Context) {
|
||||
val databaseName = "app.db"
|
||||
val databasePath = Uri.fromFile(context.filesDir).toString() + "/" + databaseName
|
||||
@@ -320,7 +336,7 @@ class DatabaseHelper {
|
||||
}
|
||||
|
||||
private fun insertPost(db: Database, post: JSONObject) {
|
||||
var metadata: JSONObject?
|
||||
var metadata: JSONObject? = null
|
||||
var reactions: JSONArray? = null
|
||||
var customEmojis: JSONArray? = null
|
||||
var files: JSONArray? = null
|
||||
@@ -374,7 +390,7 @@ class DatabaseHelper {
|
||||
}
|
||||
|
||||
private fun updatePost(db: Database, post: JSONObject) {
|
||||
var metadata: JSONObject?
|
||||
var metadata: JSONObject? = null
|
||||
var reactions: JSONArray? = null
|
||||
var customEmojis: JSONArray? = null
|
||||
|
||||
@@ -425,12 +441,10 @@ class DatabaseHelper {
|
||||
|
||||
private fun insertThread(db: Database, thread: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { false }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { false };
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 };
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 };
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 };
|
||||
|
||||
db.execute(
|
||||
"insert into Thread " +
|
||||
@@ -438,9 +452,9 @@ class DatabaseHelper {
|
||||
" values (?, ?, 0, ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
thread.getString("id"),
|
||||
lastReplyAt,
|
||||
thread.getDouble("last_reply_at") ?: 0,
|
||||
lastViewedAt,
|
||||
replyCount,
|
||||
thread.getInt("reply_count") ?: 0,
|
||||
isFollowing,
|
||||
unreadReplies,
|
||||
unreadMentions
|
||||
@@ -450,19 +464,17 @@ class DatabaseHelper {
|
||||
|
||||
private fun updateThread(db: Database, thread: ReadableMap, existingRecord: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { existingRecord.getInt("is_following") == 1 }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { existingRecord.getInt("is_following") == 1 };
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") };
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") };
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") };
|
||||
|
||||
db.execute(
|
||||
"update Thread SET last_reply_at = ?, last_viewed_at = ?, reply_count = ?, is_following = ?, unread_replies = ?, unread_mentions = ?, _status = 'updated' where id = ?",
|
||||
arrayOf(
|
||||
lastReplyAt,
|
||||
thread.getDouble("last_reply_at") ?: 0,
|
||||
lastViewedAt,
|
||||
replyCount,
|
||||
thread.getInt("reply_count") ?: 0,
|
||||
isFollowing,
|
||||
unreadReplies,
|
||||
unreadMentions,
|
||||
@@ -506,9 +518,9 @@ class DatabaseHelper {
|
||||
private fun insertFiles(db: Database, files: JSONArray) {
|
||||
for (i in 0 until files.length()) {
|
||||
val file = files.getJSONObject(i)
|
||||
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" }
|
||||
val height = try { file.getInt("height") } catch (e: JSONException) { 0 }
|
||||
val width = try { file.getInt("width") } catch (e: JSONException) { 0 }
|
||||
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" };
|
||||
val height = try { file.getInt("height") } catch (e: JSONException) { 0 };
|
||||
val width = try { file.getInt("width") } catch (e: JSONException) { 0 };
|
||||
db.execute(
|
||||
"insert into File (id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _status) " +
|
||||
"values (?, ?, ?, ?, '', ?, ?, ?, ?, ?, 'created')",
|
||||
@@ -592,11 +604,11 @@ class DatabaseHelper {
|
||||
for (i in 0 until chunks.size()) {
|
||||
val chunk = chunks.getMap(i)
|
||||
if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) {
|
||||
return chunk
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
private fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap {
|
||||
@@ -654,7 +666,7 @@ class DatabaseHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.toMap(): Map<String, *> = keys().asSequence().associateWith { it ->
|
||||
private fun JSONObject.toMap(): Map<String, *> = keys().asSequence().associateWith {
|
||||
when (val value = this[it])
|
||||
{
|
||||
is JSONArray ->
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.mattermost.helpers;
|
||||
|
||||
import com.facebook.react.bridge.Dynamic;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* KeysReadableArray: Helper class that abstracts boilerplate
|
||||
*/
|
||||
public class KeysReadableArray implements ReadableArray {
|
||||
@Override
|
||||
public int size() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNull(int index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(int index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(int index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(int index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableArray getArray(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableMap getMap(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dynamic getDynamic(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableType getType(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<Object> toArrayList() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
package com.mattermost.helpers;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class NotificationHelper {
|
||||
public static final String PUSH_NOTIFICATIONS = "PUSH_NOTIFICATIONS";
|
||||
public static final String NOTIFICATIONS_IN_GROUP = "notificationsInGroup";
|
||||
private static final String VERSION_PREFERENCE = "VERSION_PREFERENCE";
|
||||
|
||||
public static void cleanNotificationPreferencesIfNeeded(Context context) {
|
||||
try {
|
||||
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
||||
String version = String.valueOf(pInfo.versionCode);
|
||||
String storedVersion = null;
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(VERSION_PREFERENCE, Context.MODE_PRIVATE);
|
||||
if (pSharedPref != null) {
|
||||
storedVersion = pSharedPref.getString("Version", "");
|
||||
}
|
||||
|
||||
if (!version.equals(storedVersion)) {
|
||||
if (pSharedPref != null) {
|
||||
SharedPreferences.Editor editor = pSharedPref.edit();
|
||||
editor.putString("Version", version);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
Map<String, JSONObject> inputMap = new HashMap<>();
|
||||
saveMap(context, inputMap);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static int getNotificationId(Bundle notification) {
|
||||
final String postId = notification.getString("post_id");
|
||||
final String channelId = notification.getString("channel_id");
|
||||
|
||||
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
if (postId != null) {
|
||||
notificationId = postId.hashCode();
|
||||
} else if (channelId != null) {
|
||||
notificationId = channelId.hashCode();
|
||||
}
|
||||
|
||||
return notificationId;
|
||||
}
|
||||
|
||||
public static StatusBarNotification[] getDeliveredNotifications(Context context) {
|
||||
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
return notificationManager.getActiveNotifications();
|
||||
}
|
||||
|
||||
public static boolean addNotificationToPreferences(Context context, int notificationId, Bundle notification) {
|
||||
try {
|
||||
boolean createSummary = true;
|
||||
final String serverUrl = notification.getString("server_url");
|
||||
final String channelId = notification.getString("channel_id");
|
||||
final String rootId = notification.getString("root_id");
|
||||
final boolean isCRTEnabled = notification.containsKey("is_crt_enabled") && notification.getString("is_crt_enabled").equals("true");
|
||||
|
||||
final boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
final String groupId = isThreadNotification ? rootId : channelId;
|
||||
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
|
||||
if (notificationsInServer == null) {
|
||||
notificationsInServer = new JSONObject();
|
||||
}
|
||||
|
||||
JSONObject notificationsInGroup = notificationsInServer.optJSONObject(groupId);
|
||||
if (notificationsInGroup == null) {
|
||||
notificationsInGroup = new JSONObject();
|
||||
}
|
||||
|
||||
if (notificationsInGroup.length() > 0) {
|
||||
createSummary = false;
|
||||
}
|
||||
|
||||
notificationsInGroup.put(String.valueOf(notificationId), false);
|
||||
|
||||
if (createSummary) {
|
||||
// Add the summary notification id as well
|
||||
notificationsInGroup.put(String.valueOf(notificationId + 1), true);
|
||||
}
|
||||
notificationsInServer.put(groupId, notificationsInGroup);
|
||||
notificationsPerServer.put(serverUrl, notificationsInServer);
|
||||
saveMap(context, notificationsPerServer);
|
||||
|
||||
return createSummary;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void dismissNotification(Context context, Bundle notification) {
|
||||
final boolean isCRTEnabled = notification.containsKey("is_crt_enabled") && notification.getString("is_crt_enabled").equals("true");
|
||||
final String serverUrl = notification.getString("server_url");
|
||||
final String channelId = notification.getString("channel_id");
|
||||
final String rootId = notification.getString("root_id");
|
||||
|
||||
int notificationId = getNotificationId(notification);
|
||||
|
||||
if (!android.text.TextUtils.isEmpty(serverUrl) && !android.text.TextUtils.isEmpty(channelId)) {
|
||||
boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
String notificationIdStr = String.valueOf(notificationId);
|
||||
String groupId = isThreadNotification ? rootId : channelId;
|
||||
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
|
||||
if (notificationsInServer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
JSONObject notificationsInGroup = notificationsInServer.optJSONObject(groupId);
|
||||
if (notificationsInGroup == null) {
|
||||
return;
|
||||
}
|
||||
boolean isSummary = notificationsInGroup.optBoolean(notificationIdStr);
|
||||
notificationsInGroup.remove(notificationIdStr);
|
||||
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
notificationManager.cancel(notificationId);
|
||||
StatusBarNotification[] statusNotifications = getDeliveredNotifications(context);
|
||||
boolean hasMore = false;
|
||||
|
||||
for (final StatusBarNotification status : statusNotifications) {
|
||||
Bundle bundle = status.getNotification().extras;
|
||||
if (isThreadNotification) {
|
||||
hasMore = bundle.getString("root_id").equals(rootId);
|
||||
} else {
|
||||
hasMore = bundle.getString("channel_id").equals(channelId);
|
||||
}
|
||||
if (hasMore) break;
|
||||
}
|
||||
|
||||
if (!hasMore || isSummary) {
|
||||
notificationsInServer.remove(groupId);
|
||||
} else {
|
||||
try {
|
||||
notificationsInServer.put(groupId, notificationsInGroup);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
notificationsPerServer.put(serverUrl, notificationsInServer);
|
||||
saveMap(context, notificationsPerServer);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeChannelNotifications(Context context, String serverUrl, String channelId) {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
|
||||
|
||||
if (notificationsInServer != null) {
|
||||
notificationsInServer.remove(channelId);
|
||||
notificationsPerServer.put(serverUrl, notificationsInServer);
|
||||
saveMap(context, notificationsPerServer);
|
||||
}
|
||||
|
||||
StatusBarNotification[] notifications = getDeliveredNotifications(context);
|
||||
for (StatusBarNotification sbn:notifications) {
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String cId = bundle.getString("channel_id");
|
||||
String rootId = bundle.getString("root_id");
|
||||
boolean isCRTEnabled = bundle.containsKey("is_crt_enabled") && bundle.getString("is_crt_enabled").equals("true");
|
||||
boolean skipThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
if (Objects.equals(cId, channelId) && !skipThreadNotification) {
|
||||
notificationManager.cancel(sbn.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeThreadNotifications(Context context, String serverUrl, String threadId) {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
|
||||
|
||||
StatusBarNotification[] notifications = getDeliveredNotifications(context);
|
||||
for (StatusBarNotification sbn:notifications) {
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String rootId = bundle.getString("root_id");
|
||||
String postId = bundle.getString("post_id");
|
||||
if (Objects.equals(rootId, threadId)) {
|
||||
notificationManager.cancel(sbn.getId());
|
||||
}
|
||||
|
||||
if (Objects.equals(postId, threadId)) {
|
||||
String channelId = bundle.getString("channel_id");
|
||||
int id = sbn.getId();
|
||||
if (notificationsInServer != null && channelId != null) {
|
||||
JSONObject notificationsInChannel = notificationsInServer.optJSONObject(channelId);
|
||||
if (notificationsInChannel != null) {
|
||||
notificationsInChannel.remove(String.valueOf(id));
|
||||
try {
|
||||
notificationsInServer.put(channelId, notificationsInChannel);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
notificationManager.cancel(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationsInServer != null) {
|
||||
notificationsInServer.remove(threadId);
|
||||
notificationsPerServer.put(serverUrl, notificationsInServer);
|
||||
saveMap(context, notificationsPerServer);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeServerNotifications(Context context, String serverUrl) {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
notificationsPerServer.remove(serverUrl);
|
||||
saveMap(context, notificationsPerServer);
|
||||
StatusBarNotification[] notifications = getDeliveredNotifications(context);
|
||||
for (StatusBarNotification sbn:notifications) {
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String url = bundle.getString("server_url");
|
||||
if (Objects.equals(url, serverUrl)) {
|
||||
notificationManager.cancel(sbn.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearChannelOrThreadNotifications(Context context, Bundle notification) {
|
||||
final String serverUrl = notification.getString("server_url");
|
||||
final String channelId = notification.getString("channel_id");
|
||||
final String rootId = notification.getString("root_id");
|
||||
if (channelId != null) {
|
||||
final boolean isCRTEnabled = notification.containsKey("is_crt_enabled") && notification.getString("is_crt_enabled").equals("true");
|
||||
// rootId is available only when CRT is enabled & clearing the thread
|
||||
final boolean isClearThread = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
|
||||
if (isClearThread) {
|
||||
removeThreadNotifications(context, serverUrl, rootId);
|
||||
} else {
|
||||
removeChannelNotifications(context, serverUrl, channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Map Structure
|
||||
*
|
||||
* { serverUrl: { groupId: { notification1: true, notification2: false } } }
|
||||
* summary notification has a value of true
|
||||
*
|
||||
*/
|
||||
|
||||
private static void saveMap(Context context, Map<String, JSONObject> inputMap) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
if (pSharedPref != null) {
|
||||
JSONObject json = new JSONObject(inputMap);
|
||||
String jsonString = json.toString();
|
||||
SharedPreferences.Editor editor = pSharedPref.edit();
|
||||
editor.remove(NOTIFICATIONS_IN_GROUP).apply();
|
||||
editor.putString(NOTIFICATIONS_IN_GROUP, jsonString);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, JSONObject> loadMap(Context context) {
|
||||
Map<String, JSONObject> outputMap = new HashMap<>();
|
||||
if (context != null) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
try {
|
||||
if (pSharedPref != null) {
|
||||
String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_GROUP, (new JSONObject()).toString());
|
||||
JSONObject json = new JSONObject(jsonString);
|
||||
Iterator<String> servers = json.keys();
|
||||
|
||||
while (servers.hasNext()) {
|
||||
String serverUrl = servers.next();
|
||||
JSONObject notificationGroup = json.getJSONObject(serverUrl);
|
||||
outputMap.put(serverUrl, notificationGroup);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return outputMap;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package com.mattermost.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
@@ -26,8 +25,7 @@ class PushNotificationDataHelper(private val context: Context) {
|
||||
|
||||
class PushNotificationDataRunnable {
|
||||
companion object {
|
||||
private val specialMentions = listOf("all", "here", "channel")
|
||||
|
||||
private val specialMentions = listOf<String>("all", "here", "channel")
|
||||
@Synchronized
|
||||
suspend fun start(context: Context, initialData: Bundle) {
|
||||
try {
|
||||
@@ -36,7 +34,6 @@ class PushNotificationDataRunnable {
|
||||
val rootId = initialData.getString("root_id")
|
||||
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
|
||||
val db = DatabaseHelper.instance!!.getDatabaseForServer(context, serverUrl)
|
||||
Log.i("ReactNative", "Start fetching notification data in server="+serverUrl+" for channel="+channelId)
|
||||
|
||||
if (db != null) {
|
||||
var postData: ReadableMap?
|
||||
@@ -93,7 +90,6 @@ class PushNotificationDataRunnable {
|
||||
}
|
||||
|
||||
db.close()
|
||||
Log.i("ReactNative", "Done processing push notification="+serverUrl+" for channel="+channelId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -112,13 +108,14 @@ class PushNotificationDataRunnable {
|
||||
additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true"
|
||||
}
|
||||
|
||||
var endpoint: String
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val endpoint = if (receivingThreads) {
|
||||
val queryParams = "?skipFetchThreads=false&perPage=60&fromCreatedAt=0&direction=up"
|
||||
"/api/v4/posts/$rootId/thread$queryParams$additionalParams"
|
||||
if (receivingThreads) {
|
||||
var queryParams = "?skipFetchThreads=false&perPage=60&fromCreatedAt=0&direction=up"
|
||||
endpoint = "/api/v4/posts/$rootId/thread$queryParams$additionalParams"
|
||||
} else {
|
||||
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
|
||||
"/api/v4/channels/$channelId/posts$queryParams$additionalParams"
|
||||
var queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
|
||||
endpoint = "/api/v4/channels/$channelId/posts$queryParams$additionalParams"
|
||||
}
|
||||
|
||||
val postsResponse = fetch(serverUrl, endpoint)
|
||||
@@ -127,11 +124,11 @@ class PushNotificationDataRunnable {
|
||||
if (postsResponse != null) {
|
||||
val data = ReadableMapUtils.toMap(postsResponse)
|
||||
results.putMap("posts", postsResponse)
|
||||
val postsData = data["data"] as? Map<*, *>
|
||||
val postsData = data.get("data") as? Map<*, *>
|
||||
if (postsData != null) {
|
||||
val postsMap = postsData["posts"]
|
||||
val postsMap = postsData.get("posts")
|
||||
if (postsMap != null) {
|
||||
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Any>)
|
||||
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Object>)
|
||||
val iterator = posts.keySetIterator()
|
||||
val userIds = mutableListOf<String>()
|
||||
val usernames = mutableListOf<String>()
|
||||
@@ -161,8 +158,8 @@ class PushNotificationDataRunnable {
|
||||
|
||||
if (isCRTEnabled) {
|
||||
// Add root post as a thread
|
||||
val threadId = post?.getString("root_id")
|
||||
if (threadId.isNullOrEmpty()) {
|
||||
val rootId = post?.getString("root_id")
|
||||
if (rootId.isNullOrEmpty()) {
|
||||
threads.pushMap(post!!)
|
||||
}
|
||||
|
||||
@@ -172,14 +169,14 @@ class PushNotificationDataRunnable {
|
||||
for (i in 0 until participants.size()) {
|
||||
val participant = participants.getMap(i)
|
||||
|
||||
val participantId = participant.getString("id")
|
||||
if (participantId != currentUserId && participantId != null) {
|
||||
if (!threadParticipantUserIds.contains(participantId)) {
|
||||
threadParticipantUserIds.add(participantId)
|
||||
val userId = participant.getString("id")
|
||||
if (userId != currentUserId && userId != null) {
|
||||
if (!threadParticipantUserIds.contains(userId)) {
|
||||
threadParticipantUserIds.add(userId)
|
||||
}
|
||||
|
||||
if (!threadParticipantUsers.containsKey(participantId)) {
|
||||
threadParticipantUsers[participantId] = participant
|
||||
if (!threadParticipantUsers.containsKey(userId)) {
|
||||
threadParticipantUsers[userId!!] = participant
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,14 +236,14 @@ class PushNotificationDataRunnable {
|
||||
val endpoint = "api/v4/users/ids"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds)))
|
||||
return fetchWithPost(serverUrl, endpoint, options)
|
||||
return fetchWithPost(serverUrl, endpoint, options);
|
||||
}
|
||||
|
||||
private suspend fun fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableMap? {
|
||||
val endpoint = "api/v4/users/usernames"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames)))
|
||||
return fetchWithPost(serverUrl, endpoint, options)
|
||||
return fetchWithPost(serverUrl, endpoint, options);
|
||||
}
|
||||
|
||||
private suspend fun fetch(serverUrl: String, endpoint: String): ReadableMap? {
|
||||
|
||||
@@ -5,15 +5,15 @@ import kotlin.math.floor
|
||||
class RandomId {
|
||||
companion object {
|
||||
private const val alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
private const val alphabetLength = alphabet.length
|
||||
private const val idLength = 16
|
||||
private const val alphabetLenght = alphabet.length
|
||||
private const val idLenght = 16
|
||||
|
||||
fun generate(): String {
|
||||
var id = ""
|
||||
for (i in 1.rangeTo((idLength / 2))) {
|
||||
val random = floor(Math.random() * alphabetLength * alphabetLength)
|
||||
id += alphabet[floor(random / alphabetLength).toInt()]
|
||||
id += alphabet[(random % alphabetLength).toInt()]
|
||||
for (i in 1.rangeTo((idLenght / 2))) {
|
||||
val random = floor(Math.random() * alphabetLenght * alphabetLenght)
|
||||
id += alphabet[floor(random / alphabetLenght).toInt()]
|
||||
id += alphabet[(random % alphabetLenght).toInt()]
|
||||
}
|
||||
|
||||
return id
|
||||
|
||||
@@ -99,17 +99,23 @@ public class ReadableArrayUtils {
|
||||
for (Object value : array) {
|
||||
if (value == null) {
|
||||
writableArray.pushNull();
|
||||
} else if (value instanceof Boolean) {
|
||||
}
|
||||
if (value instanceof Boolean) {
|
||||
writableArray.pushBoolean((Boolean) value);
|
||||
} else if (value instanceof Double) {
|
||||
}
|
||||
if (value instanceof Double) {
|
||||
writableArray.pushDouble((Double) value);
|
||||
} else if (value instanceof Integer) {
|
||||
}
|
||||
if (value instanceof Integer) {
|
||||
writableArray.pushInt((Integer) value);
|
||||
} else if (value instanceof String) {
|
||||
}
|
||||
if (value instanceof String) {
|
||||
writableArray.pushString((String) value);
|
||||
} else if (value instanceof Map) {
|
||||
}
|
||||
if (value instanceof Map) {
|
||||
writableArray.pushMap(ReadableMapUtils.toWritableMap((Map<String, Object>) value));
|
||||
} else if (value.getClass().isArray()) {
|
||||
}
|
||||
if (value.getClass().isArray()) {
|
||||
writableArray.pushArray(ReadableArrayUtils.toWritableArray((Object[]) value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.mattermost.helpers;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
@@ -39,16 +38,10 @@ public class ReadableMapUtils {
|
||||
jsonObject.put(key, readableMap.getString(key));
|
||||
break;
|
||||
case Map:
|
||||
ReadableMap map = readableMap.getMap(key);
|
||||
if (map != null) {
|
||||
jsonObject.put(key, ReadableMapUtils.toJSONObject(map));
|
||||
}
|
||||
jsonObject.put(key, ReadableMapUtils.toJSONObject(readableMap.getMap(key)));
|
||||
break;
|
||||
case Array:
|
||||
ReadableArray array = readableMap.getArray(key);
|
||||
if (array != null) {
|
||||
jsonObject.put(key, ReadableArrayUtils.toJSONArray(array));
|
||||
}
|
||||
jsonObject.put(key, ReadableArrayUtils.toJSONArray(readableMap.getArray(key)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -99,16 +92,10 @@ public class ReadableMapUtils {
|
||||
map.put(key, readableMap.getString(key));
|
||||
break;
|
||||
case Map:
|
||||
ReadableMap obj = readableMap.getMap(key);
|
||||
if (obj != null) {
|
||||
map.put(key, ReadableMapUtils.toMap(obj));
|
||||
}
|
||||
map.put(key, ReadableMapUtils.toMap(readableMap.getMap(key)));
|
||||
break;
|
||||
case Array:
|
||||
ReadableArray array = readableMap.getArray(key);
|
||||
if (array != null) {
|
||||
map.put(key, ReadableArrayUtils.toArray(array));
|
||||
}
|
||||
map.put(key, ReadableArrayUtils.toArray(readableMap.getArray(key)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -118,26 +105,26 @@ public class ReadableMapUtils {
|
||||
|
||||
public static WritableMap toWritableMap(Map<String, Object> map) {
|
||||
WritableMap writableMap = Arguments.createMap();
|
||||
Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
|
||||
Iterator iterator = map.entrySet().iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, Object> pair = iterator.next();
|
||||
Map.Entry pair = (Map.Entry)iterator.next();
|
||||
Object value = pair.getValue();
|
||||
|
||||
if (value == null) {
|
||||
writableMap.putNull(pair.getKey());
|
||||
writableMap.putNull((String) pair.getKey());
|
||||
} else if (value instanceof Boolean) {
|
||||
writableMap.putBoolean(pair.getKey(), (Boolean) value);
|
||||
writableMap.putBoolean((String) pair.getKey(), (Boolean) value);
|
||||
} else if (value instanceof Double) {
|
||||
writableMap.putDouble(pair.getKey(), (Double) value);
|
||||
writableMap.putDouble((String) pair.getKey(), (Double) value);
|
||||
} else if (value instanceof Integer) {
|
||||
writableMap.putInt(pair.getKey(), (Integer) value);
|
||||
writableMap.putInt((String) pair.getKey(), (Integer) value);
|
||||
} else if (value instanceof String) {
|
||||
writableMap.putString(pair.getKey(), (String) value);
|
||||
} else if (value instanceof Map)
|
||||
writableMap.putMap(pair.getKey(), ReadableMapUtils.toWritableMap((Map<String, Object>) value));
|
||||
else if (value.getClass().isArray()) {
|
||||
writableMap.putArray(pair.getKey(), ReadableArrayUtils.toWritableArray((Object[]) value));
|
||||
writableMap.putString((String) pair.getKey(), (String) value);
|
||||
} else if (value instanceof Map) {
|
||||
writableMap.putMap((String) pair.getKey(), ReadableMapUtils.toWritableMap((Map<String, Object>) value));
|
||||
} else if (value.getClass() != null && value.getClass().isArray()) {
|
||||
writableMap.putArray((String) pair.getKey(), ReadableArrayUtils.toWritableArray((Object[]) value));
|
||||
}
|
||||
|
||||
iterator.remove();
|
||||
|
||||
@@ -3,9 +3,11 @@ package com.mattermost.helpers;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.content.ContentResolver;
|
||||
import android.os.Environment;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.util.Log;
|
||||
@@ -16,14 +18,16 @@ import android.os.ParcelFileDescriptor;
|
||||
import java.io.*;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
// Class based on DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
|
||||
// Class based on the steveevers DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
|
||||
|
||||
public class RealPathUtil {
|
||||
public static final String CACHE_DIR_NAME = "mmShare";
|
||||
public static String getRealPathFromURI(final Context context, final Uri uri) {
|
||||
|
||||
final boolean isKitKatOrNewer = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
// DocumentProvider
|
||||
if (DocumentsContract.isDocumentUri(context, uri)) {
|
||||
if (isKitKatOrNewer && DocumentsContract.isDocumentUri(context, uri)) {
|
||||
// ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
@@ -44,7 +48,7 @@ public class RealPathUtil {
|
||||
try {
|
||||
return getPathFromSavingTempFile(context, uri);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri);
|
||||
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -96,7 +100,7 @@ public class RealPathUtil {
|
||||
|
||||
public static String getPathFromSavingTempFile(Context context, final Uri uri) {
|
||||
File tmpFile;
|
||||
String fileName = "";
|
||||
String fileName = null;
|
||||
|
||||
if (uri == null || uri.isRelative()) {
|
||||
return null;
|
||||
@@ -109,14 +113,13 @@ public class RealPathUtil {
|
||||
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
returnCursor.moveToFirst();
|
||||
fileName = sanitizeFilename(returnCursor.getString(nameIndex));
|
||||
returnCursor.close();
|
||||
} catch (Exception e) {
|
||||
// just continue to get the filename with the last segment of the path
|
||||
}
|
||||
|
||||
try {
|
||||
if (TextUtils.isEmpty(fileName)) {
|
||||
fileName = sanitizeFilename(uri.getLastPathSegment().trim());
|
||||
fileName = sanitizeFilename(uri.getLastPathSegment().toString().trim());
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +128,7 @@ public class RealPathUtil {
|
||||
cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
String mimeType = getMimeType(uri.getPath());
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
tmpFile.createNewFile();
|
||||
|
||||
@@ -210,6 +214,15 @@ public class RealPathUtil {
|
||||
return getMimeType(file);
|
||||
}
|
||||
|
||||
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
|
||||
try {
|
||||
ContentResolver cR = context.getContentResolver();
|
||||
return cR.getType(uri);
|
||||
} catch (Exception e) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteTempFiles(final File dir) {
|
||||
try {
|
||||
if (dir.isDirectory()) {
|
||||
@@ -221,13 +234,9 @@ public class RealPathUtil {
|
||||
}
|
||||
|
||||
private static void deleteRecursive(File fileOrDirectory) {
|
||||
if (fileOrDirectory.isDirectory()) {
|
||||
File[] files = fileOrDirectory.listFiles();
|
||||
if (files != null) {
|
||||
for (File child : files)
|
||||
deleteRecursive(child);
|
||||
}
|
||||
}
|
||||
if (fileOrDirectory.isDirectory())
|
||||
for (File child : fileOrDirectory.listFiles())
|
||||
deleteRecursive(child);
|
||||
|
||||
fileOrDirectory.delete();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.mattermost.helpers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
@@ -20,7 +18,7 @@ public class ResolvePromise implements Promise {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, @NonNull WritableMap map) {
|
||||
public void reject(String code, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@@ -50,7 +48,7 @@ public class ResolvePromise implements Promise {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message, @NonNull WritableMap map) {
|
||||
public void reject(String code, String message, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.mattermost.helpers.CustomPushNotificationHelper;
|
||||
import com.mattermost.helpers.DatabaseHelper;
|
||||
import com.mattermost.helpers.Network;
|
||||
import com.mattermost.helpers.NotificationHelper;
|
||||
import com.mattermost.helpers.PushNotificationDataHelper;
|
||||
import com.mattermost.helpers.ResolvePromise;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
@@ -25,7 +34,15 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
private static final String PUSH_NOTIFICATIONS = "PUSH_NOTIFICATIONS";
|
||||
private static final String VERSION_PREFERENCE = "VERSION_PREFERENCE";
|
||||
private static final String PUSH_TYPE_MESSAGE = "message";
|
||||
private static final String PUSH_TYPE_CLEAR = "clear";
|
||||
private static final String PUSH_TYPE_SESSION = "session";
|
||||
private static final String NOTIFICATIONS_IN_CHANNEL = "notificationsInChannel";
|
||||
private final PushNotificationDataHelper dataHelper;
|
||||
|
||||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||
@@ -36,12 +53,101 @@ public class CustomPushNotification extends PushNotification {
|
||||
try {
|
||||
Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).init(context);
|
||||
Network.init(context);
|
||||
NotificationHelper.cleanNotificationPreferencesIfNeeded(context);
|
||||
} catch (Exception e) {
|
||||
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
||||
String version = String.valueOf(pInfo.versionCode);
|
||||
String storedVersion = null;
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(VERSION_PREFERENCE, Context.MODE_PRIVATE);
|
||||
if (pSharedPref != null) {
|
||||
storedVersion = pSharedPref.getString("Version", "");
|
||||
}
|
||||
|
||||
if (!version.equals(storedVersion)) {
|
||||
if (pSharedPref != null) {
|
||||
SharedPreferences.Editor editor = pSharedPref.edit();
|
||||
editor.putString("Version", version);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
Map<String, Map<String, JSONObject>> inputMap = new HashMap<>();
|
||||
saveNotificationsMap(context, inputMap);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void cancelNotification(Context context, String channelId, String rootId, Integer notificationId, Boolean isCRTEnabled) {
|
||||
if (!android.text.TextUtils.isEmpty(channelId)) {
|
||||
final String notificationIdStr = notificationId.toString();
|
||||
final Boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
final String groupId = isThreadNotification ? rootId : channelId;
|
||||
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
|
||||
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
|
||||
if (notifications == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
notificationManager.cancel(notificationId);
|
||||
notifications.remove(notificationIdStr);
|
||||
final StatusBarNotification[] statusNotifications = notificationManager.getActiveNotifications();
|
||||
boolean hasMore = false;
|
||||
for (final StatusBarNotification status : statusNotifications) {
|
||||
Bundle bundle = status.getNotification().extras;
|
||||
if (isThreadNotification) {
|
||||
hasMore = bundle.getString("root_id").equals(rootId);
|
||||
} else {
|
||||
hasMore = bundle.getString("channel_id").equals(channelId);
|
||||
}
|
||||
if (hasMore) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMore) {
|
||||
notificationsInChannel.remove(groupId);
|
||||
} else {
|
||||
notificationsInChannel.put(groupId, notifications);
|
||||
}
|
||||
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearChannelNotifications(Context context, String channelId, String rootId, Boolean isCRTEnabled) {
|
||||
if (!android.text.TextUtils.isEmpty(channelId)) {
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
|
||||
// rootId is available only when CRT is enabled & clearing the thread
|
||||
final boolean isClearThread = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
|
||||
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
|
||||
String groupId = isClearThread ? rootId : channelId;
|
||||
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
|
||||
|
||||
if (notifications == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
notificationsInChannel.remove(groupId);
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
|
||||
notifications.forEach(
|
||||
(notificationIdStr, post) -> notificationManager.cancel(Integer.valueOf(notificationIdStr))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearAllNotifications(Context context) {
|
||||
if (context != null) {
|
||||
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
|
||||
notificationsInChannel.clear();
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
notificationManager.cancelAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceived() {
|
||||
final Bundle initialData = mNotificationProps.asBundle();
|
||||
@@ -49,8 +155,15 @@ public class CustomPushNotification extends PushNotification {
|
||||
final String ackId = initialData.getString("ack_id");
|
||||
final String postId = initialData.getString("post_id");
|
||||
final String channelId = initialData.getString("channel_id");
|
||||
final String rootId = initialData.getString("root_id");
|
||||
final boolean isCRTEnabled = initialData.getString("is_crt_enabled") != null && initialData.getString("is_crt_enabled").equals("true");
|
||||
final boolean isIdLoaded = initialData.getString("id_loaded") != null && initialData.getString("id_loaded").equals("true");
|
||||
int notificationId = NotificationHelper.getNotificationId(initialData);
|
||||
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
if (postId != null) {
|
||||
notificationId = postId.hashCode();
|
||||
} else if (channelId != null) {
|
||||
notificationId = channelId.hashCode();
|
||||
}
|
||||
|
||||
String serverUrl = addServerUrlToBundle(initialData);
|
||||
boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
|
||||
@@ -63,9 +176,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
Bundle response = (Bundle) value;
|
||||
if (value != null) {
|
||||
response.putString("server_url", serverUrl);
|
||||
Bundle current = mNotificationProps.asBundle();
|
||||
current.putAll(response);
|
||||
mNotificationProps = createProps(current);
|
||||
mNotificationProps = createProps(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,34 +189,62 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_SESSION:
|
||||
case PUSH_TYPE_MESSAGE:
|
||||
case PUSH_TYPE_SESSION:
|
||||
boolean createSummary = type.equals(PUSH_TYPE_MESSAGE);
|
||||
if (!mAppLifecycleFacade.isAppVisible()) {
|
||||
boolean createSummary = type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE);
|
||||
if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) {
|
||||
if (type.equals(PUSH_TYPE_MESSAGE)) {
|
||||
if (channelId != null) {
|
||||
Bundle notificationBundle = mNotificationProps.asBundle();
|
||||
if (serverUrl != null && !isReactInit) {
|
||||
// We will only fetch the data related to the notification on the native side
|
||||
// as updating the data directly to the db removes the wal & shm files needed
|
||||
// by watermelonDB, if the DB is updated while WDB is running it causes WDB to
|
||||
// detect the database as malformed, thus the app stop working and a restart is required.
|
||||
// Data will be fetch from within the JS context instead.
|
||||
dataHelper.fetchAndStoreDataForPushNotification(notificationBundle);
|
||||
dataHelper.fetchAndStoreDataForPushNotification(mNotificationProps.asBundle());
|
||||
}
|
||||
try {
|
||||
JSONObject post = new JSONObject();
|
||||
if (!android.text.TextUtils.isEmpty(rootId)) {
|
||||
post.put("root_id", rootId);
|
||||
}
|
||||
if (!android.text.TextUtils.isEmpty(postId)) {
|
||||
post.put("post_id", postId);
|
||||
}
|
||||
|
||||
final Boolean isThreadNotification = isCRTEnabled && post.has("root_id");
|
||||
final String groupId = isThreadNotification ? rootId : channelId;
|
||||
|
||||
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(mContext);
|
||||
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
|
||||
if (notifications == null) {
|
||||
notifications = Collections.synchronizedMap(new HashMap<String, JSONObject>());
|
||||
}
|
||||
|
||||
if (notifications.size() > 0) {
|
||||
createSummary = false;
|
||||
}
|
||||
|
||||
notifications.put(String.valueOf(notificationId), post);
|
||||
|
||||
if (createSummary) {
|
||||
// Add the summary notification id as well
|
||||
notifications.put(String.valueOf(notificationId + 1), new JSONObject());
|
||||
}
|
||||
|
||||
notificationsInChannel.put(groupId, notifications);
|
||||
saveNotificationsMap(mContext, notificationsInChannel);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
createSummary = NotificationHelper.addNotificationToPreferences(
|
||||
mContext,
|
||||
notificationId,
|
||||
notificationBundle
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
buildNotification(notificationId, createSummary);
|
||||
}
|
||||
break;
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_CLEAR:
|
||||
NotificationHelper.clearChannelOrThreadNotifications(mContext, mNotificationProps.asBundle());
|
||||
case PUSH_TYPE_CLEAR:
|
||||
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -116,11 +255,15 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
@Override
|
||||
public void onOpened() {
|
||||
if (mNotificationProps != null) {
|
||||
digestNotification();
|
||||
digestNotification();
|
||||
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
final String channelId = data.getString("channel_id");
|
||||
final String rootId = data.getString("root_id");
|
||||
final Boolean isCRTEnabled = data.getBoolean("is_crt_enabled");
|
||||
|
||||
if (channelId != null) {
|
||||
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,4 +312,61 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
return serverUrl;
|
||||
}
|
||||
|
||||
private static void saveNotificationsMap(Context context, Map<String, Map<String, JSONObject>> inputMap) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
if (pSharedPref != null) {
|
||||
JSONObject json = new JSONObject(inputMap);
|
||||
String jsonString = json.toString();
|
||||
SharedPreferences.Editor editor = pSharedPref.edit();
|
||||
editor.remove(NOTIFICATIONS_IN_CHANNEL).apply();
|
||||
editor.putString(NOTIFICATIONS_IN_CHANNEL, jsonString);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Structure
|
||||
*
|
||||
* {
|
||||
* channel_id1 | thread_id1: {
|
||||
* notification_id1: {
|
||||
* post_id: 'p1',
|
||||
* root_id: 'r1',
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
private static Map<String, Map<String, JSONObject>> loadNotificationsMap(Context context) {
|
||||
Map<String, Map<String, JSONObject>> outputMap = new HashMap<>();
|
||||
if (context != null) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
try {
|
||||
if (pSharedPref != null) {
|
||||
String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_CHANNEL, (new JSONObject()).toString());
|
||||
JSONObject json = new JSONObject(jsonString);
|
||||
|
||||
// Can be a channel_id or thread_id
|
||||
Iterator<String> groupIdsItr = json.keys();
|
||||
while (groupIdsItr.hasNext()) {
|
||||
String groupId = groupIdsItr.next();
|
||||
JSONObject notificationsJSONObj = json.getJSONObject(groupId);
|
||||
Map<String, JSONObject> notifications = new HashMap<>();
|
||||
Iterator<String> notificationIdKeys = notificationsJSONObj.keys();
|
||||
while(notificationIdKeys.hasNext()) {
|
||||
String notificationId = notificationIdKeys.next();
|
||||
JSONObject post = notificationsJSONObj.getJSONObject(notificationId);
|
||||
notifications.put(notificationId, post);
|
||||
}
|
||||
outputMap.put(groupId, notifications);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return outputMap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer;
|
||||
|
||||
public class CustomPushNotificationDrawer extends PushNotificationsDrawer {
|
||||
final protected Context mContext;
|
||||
final protected AppLaunchHelper mAppLaunchHelper;
|
||||
|
||||
protected CustomPushNotificationDrawer(Context context, AppLaunchHelper appLaunchHelper) {
|
||||
super(context, appLaunchHelper);
|
||||
mContext = context;
|
||||
mAppLaunchHelper = appLaunchHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppInit() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppVisible() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationOpened() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelAllLocalNotifications() {
|
||||
CustomPushNotification.clearAllNotifications(mContext);
|
||||
cancelAllScheduledNotifications();
|
||||
}
|
||||
}
|
||||
@@ -57,12 +57,6 @@ public class MainActivity extends NavigationActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
getReactGateway().onWindowFocusChanged(hasFocus);
|
||||
}
|
||||
|
||||
/*
|
||||
https://mattermost.atlassian.net/browse/MM-10601
|
||||
Required by react-native-hw-keyboard-event
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.facebook.react.bridge.JSIModuleSpec;
|
||||
import com.facebook.react.bridge.JavaScriptContextHolder;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -39,9 +40,10 @@ import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.mattermost.networkclient.RCTOkHttpClientFactory;
|
||||
import com.mattermost.newarchitecture.MainApplicationReactNativeHost;
|
||||
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
||||
import com.nozbe.watermelondb.jsi.WatermelonDBJSIPackage;
|
||||
|
||||
public class MainApplication extends NavigationApplication implements INotificationsApplication {
|
||||
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
|
||||
public static MainApplication instance;
|
||||
|
||||
public Boolean sharedExtensionIsOpened = false;
|
||||
@@ -68,8 +70,8 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
switch (name) {
|
||||
case "MattermostManaged":
|
||||
return MattermostManagedModule.getInstance(reactContext);
|
||||
case "Notifications":
|
||||
return NotificationsModule.getInstance(instance, reactContext);
|
||||
case "NotificationPreferences":
|
||||
return NotificationPreferencesModule.getInstance(instance, reactContext);
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not find module " + name);
|
||||
}
|
||||
@@ -80,7 +82,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
return () -> {
|
||||
Map<String, ReactModuleInfo> map = new HashMap<>();
|
||||
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
|
||||
map.put("Notifications", new ReactModuleInfo("Notifications", "com.mattermost.rnbeta.NotificationsModule", false, false, false, false, false));
|
||||
map.put("NotificationPreferences", new ReactModuleInfo("NotificationPreferences", "com.mattermost.rnbeta.NotificationPreferencesModule", false, false, false, false, false));
|
||||
return map;
|
||||
};
|
||||
}
|
||||
@@ -92,11 +94,18 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
|
||||
@Override
|
||||
protected JSIModulePackage getJSIModulePackage() {
|
||||
return (reactApplicationContext, jsContext) -> {
|
||||
List<JSIModuleSpec> modules = Collections.emptyList();
|
||||
modules.addAll(new WatermelonDBJSIPackage().getJSIModules(reactApplicationContext, jsContext));
|
||||
return new JSIModulePackage() {
|
||||
@Override
|
||||
public List<JSIModuleSpec> getJSIModules(
|
||||
final ReactApplicationContext reactApplicationContext,
|
||||
final JavaScriptContextHolder jsContext
|
||||
) {
|
||||
List<JSIModuleSpec> modules = Arrays.asList();
|
||||
modules.addAll(new WatermelonDBJSIPackage().getJSIModules(reactApplicationContext, jsContext));
|
||||
modules.addAll(new ReanimatedJSIModulePackage().getJSIModules(reactApplicationContext, jsContext));
|
||||
|
||||
return modules;
|
||||
return modules;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -152,6 +161,11 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) {
|
||||
return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
|
||||
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.app.IntentService;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import com.mattermost.helpers.NotificationHelper;
|
||||
import com.mattermost.helpers.CustomPushNotificationHelper;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
|
||||
public class NotificationDismissService extends IntentService {
|
||||
@@ -18,8 +18,19 @@ public class NotificationDismissService extends IntentService {
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
final Context context = getApplicationContext();
|
||||
final Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
|
||||
final String channelId = bundle.getString("channel_id");
|
||||
final String postId = bundle.getString("post_id");
|
||||
final String rootId = bundle.getString("root_id");
|
||||
final Boolean isCRTEnabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true");
|
||||
|
||||
NotificationHelper.dismissNotification(context, bundle);
|
||||
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
if (postId != null) {
|
||||
notificationId = postId.hashCode();
|
||||
} else if (channelId != null) {
|
||||
notificationId = channelId.hashCode();
|
||||
}
|
||||
|
||||
CustomPushNotification.cancelNotification(context, channelId, rootId, notificationId, isCRTEnabled);
|
||||
Log.i("ReactNative", "Dismiss notification");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
|
||||
private static NotificationPreferencesModule instance;
|
||||
private final MainApplication mApplication;
|
||||
|
||||
private NotificationPreferencesModule(MainApplication application, ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mApplication = application;
|
||||
Context context = mApplication.getApplicationContext();
|
||||
}
|
||||
|
||||
public static NotificationPreferencesModule getInstance(MainApplication application, ReactApplicationContext reactContext) {
|
||||
if (instance == null) {
|
||||
instance = new NotificationPreferencesModule(application, reactContext);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static NotificationPreferencesModule getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "NotificationPreferences";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getDeliveredNotifications(final Promise promise) {
|
||||
final Context context = mApplication.getApplicationContext();
|
||||
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
StatusBarNotification[] statusBarNotifications = notificationManager.getActiveNotifications();
|
||||
WritableArray result = Arguments.createArray();
|
||||
for (StatusBarNotification sbn:statusBarNotifications) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String postId = bundle.getString("post_id");
|
||||
map.putString("post_id", postId);
|
||||
String rootId = bundle.getString("root_id");
|
||||
map.putString("root_id", rootId);
|
||||
String channelId = bundle.getString("channel_id");
|
||||
map.putString("channel_id", channelId);
|
||||
result.pushMap(map);
|
||||
}
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeDeliveredNotifications(String channelId, String rootId, Boolean isCRTEnabled) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
CustomPushNotification.clearChannelNotifications(context, channelId, rootId, isCRTEnabled);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.mattermost.helpers.NotificationHelper;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class NotificationsModule extends ReactContextBaseJavaModule {
|
||||
private static NotificationsModule instance;
|
||||
private final MainApplication mApplication;
|
||||
|
||||
private NotificationsModule(MainApplication application, ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mApplication = application;
|
||||
}
|
||||
|
||||
public static NotificationsModule getInstance(MainApplication application, ReactApplicationContext reactContext) {
|
||||
if (instance == null) {
|
||||
instance = new NotificationsModule(application, reactContext);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Notifications";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getDeliveredNotifications(final Promise promise) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
StatusBarNotification[] notifications = NotificationHelper.getDeliveredNotifications(context);
|
||||
WritableArray result = Arguments.createArray();
|
||||
for (StatusBarNotification sbn:notifications) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
Set<String> keys = bundle.keySet();
|
||||
for (String key: keys) {
|
||||
map.putString(key, bundle.getString(key));
|
||||
}
|
||||
result.pushMap(map);
|
||||
}
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeChannelNotifications(String serverUrl, String channelId) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
NotificationHelper.removeChannelNotifications(context, serverUrl, channelId);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeThreadNotifications(String serverUrl, String threadId) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
NotificationHelper.removeThreadNotifications(context, serverUrl, threadId);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeServerNotifications(String serverUrl) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
NotificationHelper.removeServerNotifications(context, serverUrl);
|
||||
}
|
||||
}
|
||||
48
android/app/src/main/jni/Android.mk
Normal file
48
android/app/src/main/jni/Android.mk
Normal file
@@ -0,0 +1,48 @@
|
||||
THIS_DIR := $(call my-dir)
|
||||
|
||||
include $(REACT_ANDROID_DIR)/Android-prebuilt.mk
|
||||
|
||||
# If you wish to add a custom TurboModule or Fabric component in your app you
|
||||
# will have to include the following autogenerated makefile.
|
||||
# include $(GENERATED_SRC_DIR)/codegen/jni/Android.mk
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_PATH := $(THIS_DIR)
|
||||
|
||||
# You can customize the name of your application .so file here.
|
||||
LOCAL_MODULE := mattermost_appmodules
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)
|
||||
LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp)
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
|
||||
|
||||
# If you wish to add a custom TurboModule or Fabric component in your app you
|
||||
# will have to uncomment those lines to include the generated source
|
||||
# files from the codegen (placed in $(GENERATED_SRC_DIR)/codegen/jni)
|
||||
#
|
||||
# LOCAL_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni
|
||||
# LOCAL_SRC_FILES += $(wildcard $(GENERATED_SRC_DIR)/codegen/jni/*.cpp)
|
||||
# LOCAL_EXPORT_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni
|
||||
|
||||
# Here you should add any native library you wish to depend on.
|
||||
LOCAL_SHARED_LIBRARIES := \
|
||||
libfabricjni \
|
||||
libfbjni \
|
||||
libfolly_runtime \
|
||||
libglog \
|
||||
libjsi \
|
||||
libreact_codegen_rncore \
|
||||
libreact_debug \
|
||||
libreact_nativemodule_core \
|
||||
libreact_render_componentregistry \
|
||||
libreact_render_core \
|
||||
libreact_render_debug \
|
||||
libreact_render_graphics \
|
||||
librrc_view \
|
||||
libruntimeexecutor \
|
||||
libturbomodulejsijni \
|
||||
libyoga
|
||||
|
||||
LOCAL_CFLAGS := -DLOG_TAG=\"ReactNative\" -fexceptions -frtti -std=c++17 -Wall
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
@@ -1,7 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
# Define the library name here.
|
||||
project(rndiffapp_appmodules)
|
||||
|
||||
# This file includes all the necessary to let you build your application with the New Architecture.
|
||||
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)
|
||||
@@ -1,13 +1,12 @@
|
||||
#include "MainApplicationModuleProvider.h"
|
||||
|
||||
#include <rncli.h>
|
||||
#include <rncore.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
const std::string &moduleName,
|
||||
const std::string moduleName,
|
||||
const JavaTurboModule::InitParams ¶ms) {
|
||||
// Here you can provide your own module provider for TurboModules coming from
|
||||
// either your application or from external libraries. The approach to follow
|
||||
@@ -18,13 +17,6 @@ std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
// return module;
|
||||
// }
|
||||
// return rncore_ModuleProvider(moduleName, params);
|
||||
|
||||
// Module providers autolinked by RN CLI
|
||||
auto rncli_module = rncli_ModuleProvider(moduleName, params);
|
||||
if (rncli_module != nullptr) {
|
||||
return rncli_module;
|
||||
}
|
||||
|
||||
return rncore_ModuleProvider(moduleName, params);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace facebook {
|
||||
namespace react {
|
||||
|
||||
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
const std::string &moduleName,
|
||||
const std::string moduleName,
|
||||
const JavaTurboModule::InitParams ¶ms);
|
||||
|
||||
} // namespace react
|
||||
|
||||
@@ -22,24 +22,24 @@ void MainApplicationTurboModuleManagerDelegate::registerNatives() {
|
||||
|
||||
std::shared_ptr<TurboModule>
|
||||
MainApplicationTurboModuleManagerDelegate::getTurboModule(
|
||||
const std::string &name,
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker) {
|
||||
const std::string name,
|
||||
const std::shared_ptr<CallInvoker> jsInvoker) {
|
||||
// Not implemented yet: provide pure-C++ NativeModules here.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule>
|
||||
MainApplicationTurboModuleManagerDelegate::getTurboModule(
|
||||
const std::string &name,
|
||||
const std::string name,
|
||||
const JavaTurboModule::InitParams ¶ms) {
|
||||
return MainApplicationModuleProvider(name, params);
|
||||
}
|
||||
|
||||
bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule(
|
||||
const std::string &name) {
|
||||
std::string name) {
|
||||
return getTurboModule(name, nullptr) != nullptr ||
|
||||
getTurboModule(name, {.moduleName = name}) != nullptr;
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
} // namespace facebook
|
||||
@@ -14,25 +14,25 @@ class MainApplicationTurboModuleManagerDelegate
|
||||
public:
|
||||
// Adapt it to the package you used for your Java class.
|
||||
static constexpr auto kJavaDescriptor =
|
||||
"Lcom/rndiffapp/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;";
|
||||
"Lcom/mattermost/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;";
|
||||
|
||||
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject>);
|
||||
|
||||
static void registerNatives();
|
||||
|
||||
std::shared_ptr<TurboModule> getTurboModule(
|
||||
const std::string &name,
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker) override;
|
||||
const std::string name,
|
||||
const std::shared_ptr<CallInvoker> jsInvoker) override;
|
||||
std::shared_ptr<TurboModule> getTurboModule(
|
||||
const std::string &name,
|
||||
const std::string name,
|
||||
const JavaTurboModule::InitParams ¶ms) override;
|
||||
|
||||
/**
|
||||
* Test-only method. Allows user to verify whether a TurboModule can be
|
||||
* created by instances of this class.
|
||||
*/
|
||||
bool canCreateTurboModule(const std::string &name);
|
||||
bool canCreateTurboModule(std::string name);
|
||||
};
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
} // namespace facebook
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
||||
#include <react/renderer/components/rncore/ComponentDescriptors.h>
|
||||
#include <rncli.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
@@ -15,9 +14,6 @@ std::shared_ptr<ComponentDescriptorProviderRegistry const>
|
||||
MainComponentsRegistry::sharedProviderRegistry() {
|
||||
auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry();
|
||||
|
||||
// Autolinked providers registered by RN CLI
|
||||
rncli_registerProviders(providerRegistry);
|
||||
|
||||
// Custom Fabric Components go here. You can register custom
|
||||
// components coming from your App or from 3rd party libraries here.
|
||||
//
|
||||
@@ -62,4 +58,4 @@ void MainComponentsRegistry::registerNatives() {
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
} // namespace facebook
|
||||
@@ -1,3 +1,5 @@
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
@@ -25,7 +27,7 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.2.1")
|
||||
classpath("com.android.tools.build:gradle:7.1.1")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("de.undercouch:gradle-download-task:5.0.1")
|
||||
classpath('com.google.gms:google-services:4.3.10')
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=1g
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
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-7.5.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, removeChannelFromTeamHistory} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {getCurrentUser, queryUsersById} from '@queries/servers/user';
|
||||
import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
@@ -175,7 +176,8 @@ export async function markChannelAsViewed(serverUrl: string, channelId: string,
|
||||
m.viewedAt = member.lastViewedAt;
|
||||
m.lastViewedAt = Date.now();
|
||||
});
|
||||
PushNotifications.removeChannelNotifications(serverUrl, channelId);
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
PushNotifications.cancelChannelNotifications(channelId, undefined, isCRTEnabled);
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([member]);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {ActionType, Post} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPostById, prepareDeletePost, queryPostsById} from '@queries/servers/post';
|
||||
import {getPostById, prepareDeletePost} from '@queries/servers/post';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers/thread';
|
||||
import {generateId} from '@utils/general';
|
||||
@@ -233,12 +233,3 @@ export async function storePostsForChannel(
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPosts(serverUrl: string, ids: string[]) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
return queryPostsById(database, ids).fetch();
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export async function storeConfigAndLicense(serverUrl: string, config: ClientConfig, license: ClientLicense) {
|
||||
try {
|
||||
// If we have credentials for this server then update the values in the database
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials) {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const current = await getCommonSystemValues(operator.database);
|
||||
const systems: IdValue[] = [];
|
||||
if (!deepEqual(config, current.config)) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.CONFIG,
|
||||
value: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
if (!deepEqual(license, current.license)) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.LICENSE,
|
||||
value: JSON.stringify(license),
|
||||
});
|
||||
}
|
||||
|
||||
if (systems.length) {
|
||||
await operator.handleSystem({systems, prepareRecordsOnly: false});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logError('An error occurred while saving config & license', error);
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,10 @@ import NetworkManager from '@managers/network_manager';
|
||||
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getCommonSystemValues, getConfig, getCurrentTeamId, getCurrentUserId, getLicense, setCurrentChannelId} from '@queries/servers/system';
|
||||
import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams} from '@queries/servers/team';
|
||||
import {prepareMyTeams, getNthLastChannelFromTeam, getMyTeamById, getTeamById, getTeamByName, queryMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from '@utils/channel';
|
||||
import {generateChannelNameFromDisplayName, getDirectChannelName, isArchived, isDMorGM} from '@utils/channel';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logError, logInfo} from '@utils/log';
|
||||
import {showMuteChannelSnackbar} from '@utils/snack_bar';
|
||||
@@ -32,11 +32,14 @@ import {fetchPostsForChannel} from './post';
|
||||
import {setDirectChannelVisible} from './preference';
|
||||
import {fetchRolesIfNeeded} from './role';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from './team';
|
||||
import {addUserToTeam, fetchTeamByName, removeUserFromTeam} from './team';
|
||||
import {fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
export type MyChannelsRequest = {
|
||||
categories?: CategoryWithChannels[];
|
||||
@@ -528,12 +531,11 @@ export async function fetchDirectChannelsInfo(serverUrl: string, directChannels:
|
||||
return fetchMissingDirectChannelsInfo(serverUrl, channels, currentUser?.locale, teammateDisplayNameSetting, currentUser?.id);
|
||||
}
|
||||
|
||||
export async function joinChannel(serverUrl: string, teamId: string, channelId?: string, channelName?: string, fetchOnly = false) {
|
||||
export async function joinChannel(serverUrl: string, userId: string, teamId: string, channelId?: string, channelName?: string, fetchOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const database = operator.database;
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
@@ -542,8 +544,6 @@ export async function joinChannel(serverUrl: string, teamId: string, channelId?:
|
||||
return {error};
|
||||
}
|
||||
|
||||
const userId = await getCurrentUserId(database);
|
||||
|
||||
let member: ChannelMembership | undefined;
|
||||
let channel: Channel | undefined;
|
||||
try {
|
||||
@@ -614,7 +614,8 @@ export async function joinChannelIfNeeded(serverUrl: string, channelId: string)
|
||||
return {error: undefined};
|
||||
}
|
||||
|
||||
return joinChannel(serverUrl, '', channelId);
|
||||
const userId = await getCurrentUserId(database);
|
||||
return joinChannel(serverUrl, userId, '', channelId);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
@@ -633,77 +634,167 @@ export async function markChannelAsRead(serverUrl: string, channelId: string) {
|
||||
|
||||
export async function switchToChannelByName(serverUrl: string, channelName: string, teamName: string, errorHandler: (intl: IntlShape) => void, intl: IntlShape) {
|
||||
let database;
|
||||
let operator;
|
||||
try {
|
||||
const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
database = result.database;
|
||||
operator = result.operator;
|
||||
} catch (e) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const onError = (joinedTeam: boolean, teamId?: string) => {
|
||||
errorHandler(intl);
|
||||
if (joinedTeam && teamId) {
|
||||
removeCurrentUserFromTeam(serverUrl, teamId, false);
|
||||
}
|
||||
};
|
||||
|
||||
let joinedTeam = false;
|
||||
let teamId = '';
|
||||
try {
|
||||
if (teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
|
||||
teamId = await getCurrentTeamId(database);
|
||||
} else {
|
||||
const team = await getTeamByName(database, teamName);
|
||||
const isTeamMember = team ? await getMyTeamById(database, team.id) : false;
|
||||
teamId = team?.id || '';
|
||||
let myChannel: MyChannelModel | ChannelMembership | undefined;
|
||||
let team: TeamModel | Team | undefined;
|
||||
let myTeam: MyTeamModel | TeamMembership | undefined;
|
||||
let name = teamName;
|
||||
const roles: string [] = [];
|
||||
const system = await getCommonSystemValues(database);
|
||||
const currentTeam = await getTeamById(database, system.currentTeamId);
|
||||
|
||||
if (!isTeamMember) {
|
||||
const fetchRequest = await fetchTeamByName(serverUrl, teamName);
|
||||
if (!fetchRequest.team) {
|
||||
onError(joinedTeam);
|
||||
return {error: fetchRequest.error || 'no team received'};
|
||||
}
|
||||
const {error} = await addCurrentUserToTeam(serverUrl, fetchRequest.team.id);
|
||||
if (error) {
|
||||
onError(joinedTeam);
|
||||
return {error};
|
||||
}
|
||||
teamId = fetchRequest.team.id;
|
||||
joinedTeam = true;
|
||||
}
|
||||
if (name === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
|
||||
name = currentTeam!.name;
|
||||
} else {
|
||||
team = await getTeamByName(database, teamName);
|
||||
}
|
||||
|
||||
const channel = await getChannelByName(database, teamId, channelName);
|
||||
const isChannelMember = channel ? await getMyChannel(database, channel.id) : false;
|
||||
let channelId = channel?.id || '';
|
||||
if (!isChannelMember) {
|
||||
const fetchRequest = await fetchChannelByName(serverUrl, teamId, channelName, true);
|
||||
if (!fetchRequest.channel) {
|
||||
onError(joinedTeam, teamId);
|
||||
return {error: fetchRequest.error || 'cannot fetch channel'};
|
||||
if (!team) {
|
||||
const fetchTeam = await fetchTeamByName(serverUrl, name, true);
|
||||
if (fetchTeam.error) {
|
||||
errorHandler(intl);
|
||||
return {error: fetchTeam.error};
|
||||
}
|
||||
if (fetchRequest.channel.type === General.PRIVATE_CHANNEL) {
|
||||
const {join} = await privateChannelJoinPrompt(fetchRequest.channel.display_name, intl);
|
||||
|
||||
team = fetchTeam.team!;
|
||||
}
|
||||
|
||||
let joinedNewTeam = false;
|
||||
myTeam = await getMyTeamById(database, team.id);
|
||||
if (!myTeam) {
|
||||
const added = await addUserToTeam(serverUrl, team.id, system.currentUserId, true);
|
||||
if (added.error) {
|
||||
errorHandler(intl);
|
||||
return {error: added.error};
|
||||
}
|
||||
myTeam = added.member!;
|
||||
roles.push(...myTeam.roles.split(' '));
|
||||
joinedNewTeam = true;
|
||||
}
|
||||
|
||||
if (!myTeam) {
|
||||
errorHandler(intl);
|
||||
return {error: 'Could not fetch team member'};
|
||||
}
|
||||
|
||||
let channel: Channel | ChannelModel | undefined = await getChannelByName(database, team.id, channelName);
|
||||
if (!channel) {
|
||||
const chReq = await fetchChannelByName(serverUrl, team.id, channelName, true);
|
||||
if (chReq.error) {
|
||||
errorHandler(intl);
|
||||
return {error: chReq.error};
|
||||
}
|
||||
channel = chReq.channel;
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
errorHandler(intl);
|
||||
return {error: 'Could not fetch channel'};
|
||||
}
|
||||
|
||||
if (isArchived(channel) && system.config.ExperimentalViewArchivedChannels !== 'true') {
|
||||
errorHandler(intl);
|
||||
return {error: 'Channel is archived'};
|
||||
}
|
||||
|
||||
myChannel = await getMyChannel(database, channel.id);
|
||||
|
||||
if (!myChannel) {
|
||||
const channelTeamId = 'team_id' in channel ? channel.team_id : channel.teamId;
|
||||
const req = await fetchMyChannel(serverUrl, channelTeamId || team.id, channel.id, true);
|
||||
myChannel = req.memberships?.[0];
|
||||
}
|
||||
|
||||
if (!myChannel) {
|
||||
if (channel.type === General.PRIVATE_CHANNEL) {
|
||||
const displayName = 'display_name' in channel ? channel.display_name : channel.displayName;
|
||||
const {join} = await privateChannelJoinPrompt(displayName, intl);
|
||||
if (!join) {
|
||||
onError(joinedTeam, teamId);
|
||||
if (joinedNewTeam) {
|
||||
await removeUserFromTeam(serverUrl, team.id, system.currentUserId, true);
|
||||
}
|
||||
errorHandler(intl);
|
||||
return {error: 'Refused to join Private channel'};
|
||||
}
|
||||
}
|
||||
logInfo('joining channel', displayName, channel.id);
|
||||
const result = await joinChannel(serverUrl, system.currentUserId, team.id, channel.id, undefined, true);
|
||||
if (result.error || !result.channel) {
|
||||
if (joinedNewTeam) {
|
||||
await removeUserFromTeam(serverUrl, team.id, system.currentUserId, true);
|
||||
}
|
||||
|
||||
logInfo('joining channel', fetchRequest.channel.display_name, fetchRequest.channel.id);
|
||||
const joinRequest = await joinChannel(serverUrl, teamId, undefined, channelName, false);
|
||||
if (!joinRequest.channel) {
|
||||
onError(joinedTeam, teamId);
|
||||
return {error: joinRequest.error || 'no channel returned from join'};
|
||||
}
|
||||
errorHandler(intl);
|
||||
return {error: result.error};
|
||||
}
|
||||
|
||||
channelId = fetchRequest.channel.id;
|
||||
myChannel = result.member!;
|
||||
roles.push(...myChannel.roles.split(' '));
|
||||
}
|
||||
}
|
||||
|
||||
if (!myChannel) {
|
||||
errorHandler(intl);
|
||||
return {error: 'could not fetch channel member'};
|
||||
}
|
||||
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
if (!(team instanceof Model)) {
|
||||
modelPromises.push(...prepareMyTeams(operator, [team], [(myTeam as TeamMembership)]));
|
||||
} else if (!(myTeam instanceof Model)) {
|
||||
const mt: MyTeam[] = [{
|
||||
id: myTeam.team_id,
|
||||
roles: myTeam.roles,
|
||||
}];
|
||||
modelPromises.push(
|
||||
operator.handleMyTeam({myTeams: mt, prepareRecordsOnly: true}),
|
||||
operator.handleTeamMemberships({teamMemberships: [myTeam], prepareRecordsOnly: true}),
|
||||
);
|
||||
}
|
||||
|
||||
// We are checking both, so this may become an issue
|
||||
if (!(myChannel instanceof Model) && !(channel instanceof Model)) {
|
||||
modelPromises.push(...await prepareMyChannelsForTeam(operator, team.id, [channel], [myChannel]));
|
||||
}
|
||||
|
||||
let teamId;
|
||||
if (team.id !== system.currentTeamId) {
|
||||
teamId = team.id;
|
||||
}
|
||||
|
||||
let channelId;
|
||||
if (channel.id !== system.currentChannelId) {
|
||||
channelId = channel.id;
|
||||
}
|
||||
|
||||
if (modelPromises.length) {
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
fetchMyChannelsForTeam(serverUrl, teamId, true, 0, false, true);
|
||||
}
|
||||
|
||||
if (teamId || channelId) {
|
||||
await switchToChannelById(serverUrl, channel.id, team.id);
|
||||
}
|
||||
|
||||
if (roles.length) {
|
||||
fetchRolesIfNeeded(serverUrl, roles);
|
||||
}
|
||||
|
||||
switchToChannelById(serverUrl, channelId, teamId);
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
onError(joinedTeam, teamId);
|
||||
errorHandler(intl);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
@@ -1056,7 +1147,7 @@ export async function switchToLastChannel(serverUrl: string, teamId?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchChannels(serverUrl: string, term: string, teamId: string, isSearch = false) {
|
||||
export async function searchChannels(serverUrl: string, term: string, isSearch = false) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -1070,8 +1161,9 @@ export async function searchChannels(serverUrl: string, term: string, teamId: st
|
||||
}
|
||||
|
||||
try {
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const autoCompleteFunc = isSearch ? client.autocompleteChannelsForSearch : client.autocompleteChannels;
|
||||
const channels = await autoCompleteFunc(teamId, term);
|
||||
const channels = await autoCompleteFunc(currentTeamId, term);
|
||||
return {channels};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getCurrentChannelId} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getConfig, getCurrentChannelId} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {deleteV1Data} from '@utils/file';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logInfo} from '@utils/log';
|
||||
import {logDebug, logInfo} from '@utils/log';
|
||||
|
||||
import {registerDeviceToken, syncOtherServers, verifyPushProxy} from './common';
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
import {deferredAppEntryActions, entry, registerDeviceToken, syncOtherServers, verifyPushProxy} from './common';
|
||||
import {graphQLCommon} from './gql_common';
|
||||
|
||||
export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -19,6 +19,8 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
if (!since) {
|
||||
registerDeviceToken(serverUrl);
|
||||
}
|
||||
@@ -29,6 +31,31 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
||||
operator.batchRecords(removeLastUnreadChannelId);
|
||||
}
|
||||
|
||||
const config = await getConfig(database);
|
||||
let result;
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
const {currentTeamId, currentChannelId} = await getCommonSystemValues(database);
|
||||
result = await graphQLCommon(serverUrl, true, currentTeamId, currentChannelId, isUpgrade);
|
||||
if (result.error) {
|
||||
logDebug('Error using GraphQL, trying REST', result.error);
|
||||
result = restAppEntry(serverUrl, since, isUpgrade);
|
||||
}
|
||||
} else {
|
||||
result = restAppEntry(serverUrl, since, isUpgrade);
|
||||
}
|
||||
|
||||
if (!since) {
|
||||
// Load data from other servers
|
||||
syncOtherServers(serverUrl);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function restAppEntry(serverUrl: string, since = 0, isUpgrade = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const {database} = operator;
|
||||
|
||||
const tabletDevice = await isTablet();
|
||||
@@ -68,11 +95,6 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
|
||||
|
||||
if (!since) {
|
||||
// Load data from other servers
|
||||
syncOtherServers(serverUrl);
|
||||
}
|
||||
|
||||
verifyPushProxy(serverUrl);
|
||||
|
||||
return {userId: currentUserId};
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Model} from '@nozbe/watermelondb';
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {fetchAllTeams, fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, MyTeamsRequest} from '@actions/remote/team';
|
||||
import {fetchNewThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlAllChannels} from '@client/graphQL/entry';
|
||||
import {General, Preferences, Screens} from '@constants';
|
||||
import {Preferences} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||
import DatabaseManager from '@database/manager';
|
||||
@@ -27,12 +26,14 @@ import {prepareMyChannelsForTeam, queryAllChannelsForTeam, queryChannelsById} fr
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
|
||||
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
|
||||
import {deleteMyTeams, getAvailableTeamIds, getNthLastChannelFromTeam, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {getMemberChannelsFromGQLQuery, gqlToClientChannelMembership} from '@utils/graphql';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
import {fetchGroupsForMember} from '../groups';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
|
||||
export type AppEntryData = {
|
||||
@@ -65,12 +66,6 @@ export type EntryResponse = {
|
||||
const FETCH_MISSING_DM_TIMEOUT = 2500;
|
||||
export const FETCH_UNREADS_TIMEOUT = 2500;
|
||||
|
||||
export const getRemoveTeamIds = async (database: Database, teamData: MyTeamsRequest) => {
|
||||
const myTeams = await queryMyTeams(database).fetch();
|
||||
const joinedTeams = new Set(teamData.memberships?.filter((m) => m.delete_at === 0).map((m) => m.team_id));
|
||||
return myTeams.filter((m) => !joinedTeams.has(m.id)).map((m) => m.id);
|
||||
};
|
||||
|
||||
export const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[]) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
@@ -92,7 +87,7 @@ export const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[])
|
||||
return [];
|
||||
};
|
||||
|
||||
export const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
|
||||
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -115,7 +110,13 @@ export const entryRest = async (serverUrl: string, teamId?: string, channelId?:
|
||||
|
||||
const rolesData = await fetchRoles(serverUrl, teamData.memberships, chData?.memberships, meData.user, true);
|
||||
|
||||
const initialChannelId = await entryInitialChannelId(database, channelId, teamId, initialTeamId, meData?.user?.locale || '', chData?.channels, chData?.memberships);
|
||||
let initialChannelId = channelId;
|
||||
if (!chData?.channels?.find((c) => c.id === channelId)) {
|
||||
initialChannelId = '';
|
||||
}
|
||||
if (initialTeamId !== teamId || !initialChannelId) {
|
||||
initialChannelId = await getNthLastChannelFromTeam(database, initialTeamId);
|
||||
}
|
||||
|
||||
const removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
|
||||
|
||||
@@ -168,6 +169,7 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
fetchMe(serverUrl, fetchOnly),
|
||||
];
|
||||
|
||||
const removeTeamIds: string[] = [];
|
||||
const resolution = await Promise.all(promises);
|
||||
const [teamData, , meData] = resolution;
|
||||
let [, chData] = resolution;
|
||||
@@ -184,7 +186,10 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
}
|
||||
}
|
||||
|
||||
const removeTeamIds = await getRemoveTeamIds(database, teamData);
|
||||
const removedFromTeam = teamData.memberships?.filter((m) => m.delete_at > 0);
|
||||
if (removedFromTeam?.length) {
|
||||
removeTeamIds.push(...removedFromTeam.map((m) => m.team_id));
|
||||
}
|
||||
|
||||
let data: AppEntryData = {
|
||||
initialTeamId,
|
||||
@@ -197,6 +202,10 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
};
|
||||
|
||||
if (teamData.teams?.length === 0 && !teamData.error) {
|
||||
// User is no longer a member of any team
|
||||
const myTeams = await queryMyTeams(database).fetch();
|
||||
removeTeamIds.push(...(myTeams.map((myTeam) => myTeam.id) || []));
|
||||
|
||||
return {
|
||||
...data,
|
||||
initialTeamId: '',
|
||||
@@ -266,45 +275,7 @@ export const fetchAlternateTeamData = async (
|
||||
return {initialTeamId, removeTeamIds};
|
||||
};
|
||||
|
||||
export async function entryInitialChannelId(database: Database, requestedChannelId = '', requestedTeamId = '', initialTeamId: string, locale: string, channels?: Channel[], memberships?: ChannelMember[]) {
|
||||
const membershipIds = new Set(memberships?.map((m) => m.channel_id));
|
||||
const requestedChannel = channels?.find((c) => (c.id === requestedChannelId) && membershipIds.has(c.id));
|
||||
|
||||
// If team and channel are the requested, return the channel
|
||||
if (initialTeamId === requestedTeamId && requestedChannel) {
|
||||
return requestedChannelId;
|
||||
}
|
||||
|
||||
// DM or GMs don't care about changes in teams, so return directly
|
||||
if (requestedChannel && isDMorGM(requestedChannel)) {
|
||||
return requestedChannelId;
|
||||
}
|
||||
|
||||
// Check if we are still members of any channel on the history
|
||||
const teamChannelHistory = await getTeamChannelHistory(database, initialTeamId);
|
||||
for (const c of teamChannelHistory) {
|
||||
if (membershipIds.has(c) || c === Screens.GLOBAL_THREADS) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we are member of the default channel.
|
||||
const defaultChannel = channels?.find((c) => c.name === General.DEFAULT_CHANNEL && c.team_id === initialTeamId);
|
||||
const iAmMemberOfTheTeamDefaultChannel = Boolean(defaultChannel && membershipIds.has(defaultChannel.id));
|
||||
if (iAmMemberOfTheTeamDefaultChannel) {
|
||||
return defaultChannel!.id;
|
||||
}
|
||||
|
||||
// Get the first channel of the list, based on the locale.
|
||||
const myFirstTeamChannel = channels?.filter((c) =>
|
||||
c.team_id === requestedTeamId &&
|
||||
c.type === General.OPEN_CHANNEL &&
|
||||
membershipIds.has(c.id),
|
||||
).sort(sortChannelsByDisplayName.bind(null, locale))[0];
|
||||
return myFirstTeamChannel?.id || '';
|
||||
}
|
||||
|
||||
export async function restDeferredAppEntryActions(
|
||||
export async function deferredAppEntryActions(
|
||||
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
|
||||
config: ClientConfig, license: ClientLicense, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
initialTeamId?: string, initialChannelId?: string) {
|
||||
@@ -381,7 +352,6 @@ export const syncOtherServers = async (serverUrl: string) => {
|
||||
if (server.url !== serverUrl && server.lastActiveAt > 0) {
|
||||
registerDeviceToken(server.url);
|
||||
syncAllChannelMembersAndThreads(server.url);
|
||||
autoUpdateTimezone(server.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
|
||||
import {storeConfigAndLicense} from '@actions/local/systems';
|
||||
import {MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {markChannelAsRead, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {fetchPostsForChannel, fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {MyTeamsRequest} from '@actions/remote/team';
|
||||
import {fetchNewThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry';
|
||||
import {Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
@@ -18,26 +17,30 @@ import {selectDefaultTeam} from '@helpers/api/team';
|
||||
import {queryAllChannels, queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory, queryMyTeams} from '@queries/servers/team';
|
||||
import {selectDefaultChannelForTeam} from '@utils/channel';
|
||||
import {filterAndTransformRoles, getMemberChannelsFromGQLQuery, getMemberTeamsFromGQLQuery, gqlToClientChannelMembership, gqlToClientPreference, gqlToClientSidebarCategory, gqlToClientTeamMembership, gqlToClientUser} from '@utils/graphql';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
import {teamsToRemove, FETCH_UNREADS_TIMEOUT, entryRest, EntryResponse, entryInitialChannelId, restDeferredAppEntryActions, getRemoveTeamIds} from './common';
|
||||
import {teamsToRemove, FETCH_UNREADS_TIMEOUT} from './common';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
export async function deferredAppEntryGraphQLActions(
|
||||
serverUrl: string,
|
||||
since: number,
|
||||
currentUserId: string,
|
||||
meData: MyUserRequest,
|
||||
teamData: MyTeamsRequest,
|
||||
chData: MyChannelsRequest | undefined,
|
||||
preferences: PreferenceType[] | undefined,
|
||||
config: ClientConfig,
|
||||
isTabletDevice: boolean,
|
||||
initialTeamId?: string,
|
||||
initialChannelId?: string,
|
||||
isCRTEnabled = false,
|
||||
syncDatabase?: boolean,
|
||||
) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
@@ -45,14 +48,20 @@ export async function deferredAppEntryGraphQLActions(
|
||||
}
|
||||
const {database} = operator;
|
||||
|
||||
// defer fetching posts for initial channel
|
||||
if (initialChannelId && isTabletDevice) {
|
||||
fetchPostsForChannel(serverUrl, initialChannelId);
|
||||
markChannelAsRead(serverUrl, initialChannelId);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, true);
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
|
||||
if (preferences && processIsCRTEnabled(preferences, config)) {
|
||||
if (isCRTEnabled) {
|
||||
if (initialTeamId) {
|
||||
await fetchNewThreads(serverUrl, initialTeamId, false);
|
||||
}
|
||||
@@ -68,19 +77,16 @@ export async function deferredAppEntryGraphQLActions(
|
||||
}
|
||||
|
||||
if (initialTeamId) {
|
||||
const result = await getChannelData(serverUrl, initialTeamId, currentUserId, true);
|
||||
const result = await getChannelData(serverUrl, initialTeamId, meData.user!.id, true);
|
||||
if ('error' in result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const removeChannels = await getRemoveChannels(database, result.chData, initialTeamId, false);
|
||||
const removeChannels = await getRemoveChannels(database, result.chData, initialTeamId, false, syncDatabase);
|
||||
|
||||
const modelPromises = await prepareModels({operator, removeChannels, chData: result.chData}, true);
|
||||
|
||||
const roles = filterAndTransformRoles(result.roles);
|
||||
if (roles.length) {
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
modelPromises.push(operator.handleRole({roles: filterAndTransformRoles(result.roles), prepareRecordsOnly: true}));
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
operator.batchRecords(models);
|
||||
|
||||
@@ -92,26 +98,30 @@ export async function deferredAppEntryGraphQLActions(
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
}
|
||||
|
||||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, currentUserId);
|
||||
if (meData.user?.id) {
|
||||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, meData.user?.id);
|
||||
}
|
||||
|
||||
updateAllUsersSince(serverUrl, since);
|
||||
|
||||
return {error: undefined};
|
||||
return {};
|
||||
}
|
||||
|
||||
const getRemoveChannels = async (database: Database, chData: MyChannelsRequest | undefined, initialTeamId: string, singleTeam: boolean) => {
|
||||
const getRemoveChannels = async (database: Database, chData: MyChannelsRequest | undefined, initialTeamId: string, singleTeam: boolean, syncDatabase?: boolean) => {
|
||||
const removeChannels: ChannelModel[] = [];
|
||||
if (chData?.channels) {
|
||||
const fetchedChannelIds = chData.channels?.map((channel) => channel.id);
|
||||
if (syncDatabase) {
|
||||
if (chData?.channels) {
|
||||
const fetchedChannelIds = chData.channels?.map((channel) => channel.id);
|
||||
|
||||
const query = singleTeam ? queryAllChannelsForTeam(database, initialTeamId) : queryAllChannels(database);
|
||||
const channels = await query.fetch();
|
||||
const query = singleTeam ? queryAllChannelsForTeam(database, initialTeamId) : queryAllChannels(database);
|
||||
const channels = await query.fetch();
|
||||
|
||||
for (const channel of channels) {
|
||||
const excludeCondition = singleTeam ? true : channel.teamId !== initialTeamId && channel.teamId !== '';
|
||||
if (excludeCondition && !fetchedChannelIds?.includes(channel.id)) {
|
||||
removeChannels.push(channel);
|
||||
for (const channel of channels) {
|
||||
const excludeCondition = singleTeam ? true : channel.teamId !== initialTeamId && channel.teamId !== '';
|
||||
if (excludeCondition && !fetchedChannelIds?.includes(channel.id)) {
|
||||
removeChannels.push(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,13 +158,17 @@ const getChannelData = async (serverUrl: string, initialTeamId: string, userId:
|
||||
return {chData, roles};
|
||||
};
|
||||
|
||||
export const entryGQL = async (serverUrl: string, currentTeamId?: string, currentChannelId?: string): Promise<EntryResponse> => {
|
||||
export const graphQLCommon = async (serverUrl: string, syncDatabase: boolean, currentTeamId: string, currentChannelId: string, isUpgrade = false) => {
|
||||
const dt = Date.now();
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const {database} = operator;
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await gqlEntry(serverUrl);
|
||||
@@ -174,7 +188,6 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
|
||||
|
||||
const config = fetchedData.config || {} as ClientConfig;
|
||||
const license = fetchedData.license || {} as ClientLicense;
|
||||
await storeConfigAndLicense(serverUrl, config, license);
|
||||
|
||||
const meData = {
|
||||
user: gqlToClientUser(fetchedData.user!),
|
||||
@@ -199,6 +212,13 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpgrade && meData?.user) {
|
||||
const me = await prepareCommonSystemValues(operator, {currentUserId: meData.user.id});
|
||||
if (me?.length) {
|
||||
await operator.batchRecords(me);
|
||||
}
|
||||
}
|
||||
|
||||
let initialTeamId = currentTeamId;
|
||||
if (!teamData.teams.length) {
|
||||
initialTeamId = '';
|
||||
@@ -224,52 +244,68 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
|
||||
|
||||
const roles = filterAndTransformRoles(gqlRoles);
|
||||
|
||||
const initialChannelId = await entryInitialChannelId(database, currentChannelId, currentTeamId, initialTeamId, meData.user.id, chData?.channels, chData?.memberships);
|
||||
const removeChannels = await getRemoveChannels(database, chData, initialTeamId, true);
|
||||
const removeTeamIds = await getRemoveTeamIds(database, teamData);
|
||||
const removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
|
||||
let initialChannelId = currentChannelId;
|
||||
if (initialTeamId !== currentTeamId || !chData?.channels?.find((c) => c.id === currentChannelId)) {
|
||||
initialChannelId = '';
|
||||
if (isTabletDevice && chData?.channels && chData.memberships) {
|
||||
initialChannelId = selectDefaultChannelForTeam(chData.channels, chData.memberships, initialTeamId, roles, meData.user.locale)?.id || '';
|
||||
}
|
||||
}
|
||||
|
||||
let removeTeams: TeamModel[] = [];
|
||||
const removeChannels = await getRemoveChannels(database, chData, initialTeamId, true, syncDatabase);
|
||||
|
||||
if (syncDatabase) {
|
||||
const removeTeamIds = [];
|
||||
|
||||
const removedFromTeam = teamData.memberships?.filter((m) => m.delete_at > 0);
|
||||
if (removedFromTeam?.length) {
|
||||
removeTeamIds.push(...removedFromTeam.map((m) => m.team_id));
|
||||
}
|
||||
|
||||
if (teamData.teams?.length === 0) {
|
||||
// User is no longer a member of any team
|
||||
const myTeams = await queryMyTeams(database).fetch();
|
||||
removeTeamIds.push(...(myTeams?.map((myTeam) => myTeam.id) || []));
|
||||
}
|
||||
|
||||
removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
|
||||
}
|
||||
|
||||
const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData}, true);
|
||||
if (roles.length) {
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
return {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData};
|
||||
};
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
modelPromises.push(prepareCommonSystemValues(
|
||||
operator,
|
||||
{
|
||||
config,
|
||||
license,
|
||||
currentTeamId: initialTeamId,
|
||||
currentChannelId: initialChannelId,
|
||||
},
|
||||
));
|
||||
|
||||
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const config = await getConfig(database);
|
||||
let result;
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
result = await entryGQL(serverUrl, teamId, channelId);
|
||||
if ('error' in result) {
|
||||
logDebug('Error using GraphQL, trying REST', result.error);
|
||||
result = entryRest(serverUrl, teamId, channelId, since);
|
||||
if (initialTeamId && initialTeamId !== currentTeamId) {
|
||||
const th = addTeamToTeamHistory(operator, initialTeamId, true);
|
||||
modelPromises.push(th);
|
||||
}
|
||||
|
||||
if (initialTeamId !== currentTeamId && initialChannelId) {
|
||||
try {
|
||||
const tch = addChannelToTeamHistory(operator, initialTeamId, initialChannelId, true);
|
||||
modelPromises.push(tch);
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
} else {
|
||||
result = entryRest(serverUrl, teamId, channelId, since);
|
||||
}
|
||||
|
||||
return result;
|
||||
const models = await Promise.all(modelPromises);
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
|
||||
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, config));
|
||||
deferredAppEntryGraphQLActions(serverUrl, 0, meData, teamData, chData, isTabletDevice, initialTeamId, initialChannelId, isCRTEnabled, syncDatabase);
|
||||
|
||||
const timeElapsed = Date.now() - dt;
|
||||
return {time: timeElapsed, hasTeams: Boolean(teamData.teams.length), userId: meData.user.id, error: undefined};
|
||||
};
|
||||
|
||||
export async function deferredAppEntryActions(
|
||||
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
|
||||
config: ClientConfig, license: ClientLicense, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
initialTeamId?: string, initialChannelId?: string) {
|
||||
let result;
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
result = await deferredAppEntryGraphQLActions(serverUrl, since, currentUserId, teamData, chData, preferences, config, initialTeamId, initialChannelId);
|
||||
if (result.error) {
|
||||
logDebug('Error using GraphQL, trying REST', result.error);
|
||||
result = restDeferredAppEntryActions(serverUrl, since, currentUserId, currentUserLocale, preferences, config, license, teamData, chData, initialTeamId, initialChannelId);
|
||||
}
|
||||
} else {
|
||||
result = restDeferredAppEntryActions(serverUrl, since, currentUserId, currentUserLocale, preferences, config, license, teamData, chData, initialTeamId, initialChannelId);
|
||||
}
|
||||
|
||||
autoUpdateTimezone(serverUrl);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {getSessions} from '@actions/remote/session';
|
||||
import {ConfigAndLicenseRequest, fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug, logWarning} from '@utils/log';
|
||||
import {scheduleExpiredNotification} from '@utils/notification';
|
||||
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
import {deferredAppEntryActions, entry} from './common';
|
||||
import {graphQLCommon} from './gql_common';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
|
||||
@@ -18,9 +22,13 @@ type AfterLoginArgs = {
|
||||
deviceToken?: string;
|
||||
}
|
||||
|
||||
export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs): Promise<{error?: any; hasTeams?: boolean; time?: number}> {
|
||||
const dt = Date.now();
|
||||
type SpecificAfterLoginArgs = {
|
||||
serverUrl: string;
|
||||
user: UserProfile;
|
||||
clData: ConfigAndLicenseRequest;
|
||||
}
|
||||
|
||||
export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs): Promise<{error?: any; hasTeams?: boolean; time?: number}> {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -42,37 +50,77 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs)
|
||||
}
|
||||
|
||||
try {
|
||||
const clData = await fetchConfigAndLicense(serverUrl, false);
|
||||
const clData = await fetchConfigAndLicense(serverUrl, true);
|
||||
if (clData.error) {
|
||||
return {error: clData.error};
|
||||
}
|
||||
|
||||
const entryData = await entry(serverUrl, '', '');
|
||||
// schedule local push notification if needed
|
||||
if (clData.config) {
|
||||
if (clData.config.ExtendSessionLengthWithActivity !== 'true') {
|
||||
const timeOut = setTimeout(async () => {
|
||||
clearTimeout(timeOut);
|
||||
let sessions: Session[]|undefined;
|
||||
|
||||
if ('error' in entryData) {
|
||||
return {error: entryData.error};
|
||||
try {
|
||||
sessions = await getSessions(serverUrl, 'me');
|
||||
} catch (e) {
|
||||
logWarning('Failed to get user sessions', e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessions && Array.isArray(sessions)) {
|
||||
scheduleExpiredNotification(sessions, clData.config?.SiteName || serverUrl, user.locale);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
|
||||
let switchToChannel = false;
|
||||
if (initialChannelId && isTabletDevice) {
|
||||
switchToChannel = true;
|
||||
switchToChannelById(serverUrl, initialChannelId, initialTeamId);
|
||||
} else {
|
||||
setCurrentTeamAndChannelId(operator, initialTeamId, '');
|
||||
if (clData.config?.FeatureFlagGraphQL === 'true') {
|
||||
const result = await graphQLCommon(serverUrl, false, '', '');
|
||||
if (!result.error) {
|
||||
return result;
|
||||
}
|
||||
logDebug('Error using GraphQL, trying REST', result.error);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
|
||||
const config = clData.config || {} as ClientConfig;
|
||||
const license = clData.license || {} as ClientLicense;
|
||||
deferredAppEntryActions(serverUrl, 0, user.id, user.locale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
|
||||
|
||||
return {time: Date.now() - dt, hasTeams: Boolean(teamData.teams?.length)};
|
||||
return restLoginEntry({serverUrl, user, clData});
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
const restLoginEntry = async ({serverUrl, user, clData}: SpecificAfterLoginArgs) => {
|
||||
const dt = Date.now();
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const entryData = await entry(serverUrl, '', '');
|
||||
|
||||
if ('error' in entryData) {
|
||||
return {error: entryData.error};
|
||||
}
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
|
||||
let switchToChannel = false;
|
||||
if (initialChannelId && isTabletDevice) {
|
||||
switchToChannel = true;
|
||||
switchToChannelById(serverUrl, initialChannelId, initialTeamId);
|
||||
} else {
|
||||
setCurrentTeamAndChannelId(operator, initialTeamId, '');
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
|
||||
const config = clData.config || {} as ClientConfig;
|
||||
const license = clData.license || {} as ClientLicense;
|
||||
deferredAppEntryActions(serverUrl, 0, user.id, user.locale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
|
||||
|
||||
return {time: Date.now() - dt, hasTeams: Boolean(teamData.teams?.length)};
|
||||
};
|
||||
|
||||
@@ -8,18 +8,19 @@ import {getDefaultThemeByAppearance} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getMyChannel} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getCommonSystemValues, getConfig, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme';
|
||||
|
||||
import {syncOtherServers} from './common';
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
import {deferredAppEntryActions, entry, syncOtherServers} from './common';
|
||||
import {graphQLCommon} from './gql_common';
|
||||
|
||||
export async function pushNotificationEntry(serverUrl: string, notification: NotificationWithData) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -33,8 +34,6 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
const {database} = operator;
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
|
||||
let isDirectChannel = false;
|
||||
|
||||
let teamId = notification.payload?.team_id;
|
||||
@@ -62,25 +61,30 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
|
||||
await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME);
|
||||
|
||||
// To make the switch faster we determine if we already have the team & channel
|
||||
const myChannel = await getMyChannel(database, channelId);
|
||||
const myTeam = await getMyTeamById(database, teamId);
|
||||
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(rootId);
|
||||
|
||||
let switchedToScreen = false;
|
||||
let switchedToChannel = false;
|
||||
if (myChannel && myTeam) {
|
||||
if (isThreadNotification) {
|
||||
await fetchAndSwitchToThread(serverUrl, rootId, true);
|
||||
} else {
|
||||
switchedToChannel = true;
|
||||
await switchToChannelById(serverUrl, channelId, teamId);
|
||||
const config = await getConfig(database);
|
||||
let result;
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
result = await graphQLCommon(serverUrl, true, teamId, channelId);
|
||||
if (result.error) {
|
||||
logDebug('Error using GraphQL, trying REST', result.error);
|
||||
result = restNotificationEntry(serverUrl, teamId, channelId, rootId, isDirectChannel);
|
||||
}
|
||||
switchedToScreen = true;
|
||||
} else {
|
||||
result = restNotificationEntry(serverUrl, teamId, channelId, rootId, isDirectChannel);
|
||||
}
|
||||
|
||||
syncOtherServers(serverUrl);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const restNotificationEntry = async (serverUrl: string, teamId: string, channelId: string, rootId: string, isDirectChannel: boolean) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const {database} = operator;
|
||||
|
||||
const entryData = await entry(serverUrl, teamId, channelId);
|
||||
if ('error' in entryData) {
|
||||
return {error: entryData.error};
|
||||
@@ -102,45 +106,54 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
}
|
||||
}
|
||||
|
||||
const myChannel = await getMyChannel(database, channelId);
|
||||
const myTeam = await getMyTeamById(database, teamId);
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(rootId);
|
||||
|
||||
let switchedToScreen = false;
|
||||
let switchedToChannel = false;
|
||||
if (myChannel && myTeam) {
|
||||
if (isThreadNotification) {
|
||||
await fetchAndSwitchToThread(serverUrl, rootId, true);
|
||||
} else {
|
||||
switchedToChannel = true;
|
||||
await switchToChannelById(serverUrl, channelId, teamId);
|
||||
}
|
||||
switchedToScreen = true;
|
||||
}
|
||||
|
||||
if (!switchedToScreen) {
|
||||
const isTabletDevice = await isTablet();
|
||||
if (isTabletDevice || (channelId === selectedChannelId)) {
|
||||
if (isTabletDevice || (selectedChannelId === channelId)) {
|
||||
// Make switch again to get the missing data and make sure the team is the correct one
|
||||
switchedToScreen = true;
|
||||
if (isThreadNotification) {
|
||||
await fetchAndSwitchToThread(serverUrl, rootId, true);
|
||||
fetchAndSwitchToThread(serverUrl, rootId, true);
|
||||
} else {
|
||||
switchedToChannel = true;
|
||||
await switchToChannelById(serverUrl, channelId, teamId);
|
||||
switchToChannelById(serverUrl, selectedChannelId, selectedTeamId);
|
||||
}
|
||||
} else if (teamId !== selectedTeamId || channelId !== selectedChannelId) {
|
||||
} else if (selectedTeamId !== teamId || selectedChannelId !== channelId) {
|
||||
// If in the end the selected team or channel is different than the one from the notification
|
||||
// we switch again
|
||||
await setCurrentTeamAndChannelId(operator, selectedTeamId, selectedChannelId);
|
||||
setCurrentTeamAndChannelId(operator, selectedTeamId, selectedChannelId);
|
||||
}
|
||||
}
|
||||
|
||||
if (teamId !== selectedTeamId) {
|
||||
if (selectedTeamId !== teamId) {
|
||||
emitNotificationError('Team');
|
||||
} else if (channelId !== selectedChannelId) {
|
||||
} else if (selectedChannelId !== channelId) {
|
||||
emitNotificationError('Channel');
|
||||
}
|
||||
|
||||
// Waiting for the screen to display fixes a race condition when fetching and storing data
|
||||
if (switchedToChannel) {
|
||||
await NavigationStore.waitUntilScreenHasLoaded(Screens.CHANNEL);
|
||||
} else if (switchedToScreen && isThreadNotification) {
|
||||
await NavigationStore.waitUntilScreenHasLoaded(Screens.THREAD);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;
|
||||
const {config, license} = await getCommonSystemValues(operator.database);
|
||||
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId);
|
||||
|
||||
syncOtherServers(serverUrl);
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, switchedToChannel ? selectedChannelId : undefined);
|
||||
|
||||
return {userId: currentUserId};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,10 +30,6 @@ export const fetchGroupsForAutocomplete = async (serverUrl: string, query: strin
|
||||
const client: Client = NetworkManager.getClient(serverUrl);
|
||||
const response = await client.getGroups({query, includeMemberCount: true});
|
||||
|
||||
if (!response.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return operator.handleGroups({groups: response, prepareRecordsOnly: fetchOnly});
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
@@ -55,10 +51,6 @@ export const fetchGroupsByNames = async (serverUrl: string, names: string[], fet
|
||||
const groups = (await Promise.all(promises)).flat();
|
||||
|
||||
// Save locally
|
||||
if (!groups.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return operator.handleGroups({groups, prepareRecordsOnly: fetchOnly});
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
@@ -72,10 +64,6 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const response = await client.getAllGroupsAssociatedToChannel(channelId);
|
||||
|
||||
if (!response.groups.length) {
|
||||
return {groups: [], groupChannels: []};
|
||||
}
|
||||
|
||||
const [groups, groupChannels] = await Promise.all([
|
||||
operator.handleGroups({groups: response.groups, prepareRecordsOnly: true}),
|
||||
operator.handleGroupChannelsForChannel({groups: response.groups, channelId, prepareRecordsOnly: true}),
|
||||
@@ -99,10 +87,6 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc
|
||||
const client: Client = NetworkManager.getClient(serverUrl);
|
||||
const response = await client.getAllGroupsAssociatedToTeam(teamId);
|
||||
|
||||
if (!response.groups.length) {
|
||||
return {groups: [], groupTeams: []};
|
||||
}
|
||||
|
||||
const [groups, groupTeams] = await Promise.all([
|
||||
operator.handleGroups({groups: response.groups, prepareRecordsOnly: true}),
|
||||
operator.handleGroupTeamsForTeam({groups: response.groups, teamId, prepareRecordsOnly: true}),
|
||||
@@ -125,10 +109,6 @@ export const fetchGroupsForMember = async (serverUrl: string, userId: string, fe
|
||||
const client: Client = NetworkManager.getClient(serverUrl);
|
||||
const response = await client.getAllGroupsAssociatedToMembership(userId);
|
||||
|
||||
if (!response.length) {
|
||||
return {groups: [], groupMemberships: []};
|
||||
}
|
||||
|
||||
const [groups, groupMemberships] = await Promise.all([
|
||||
operator.handleGroups({groups: response, prepareRecordsOnly: true}),
|
||||
operator.handleGroupMembershipsForMember({groups: response, userId, prepareRecordsOnly: true}),
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
// import {updatePostSinceCache, updatePostsInThreadsSinceCache} from '@actions/local/notification';
|
||||
import {updatePostSinceCache, updatePostsInThreadsSinceCache} from '@actions/local/notification';
|
||||
import {fetchDirectChannelsInfo, fetchMyChannel, switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {fetchMyTeam} from '@actions/remote/team';
|
||||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||
@@ -74,18 +73,6 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
// on Android we only fetched the post data on the native side
|
||||
// when the RN context is not running, thus we need to fetch the
|
||||
// data here as well
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(notification.payload?.root_id);
|
||||
if (isThreadNotification) {
|
||||
fetchPostThread(serverUrl, notification.payload!.root_id!);
|
||||
} else {
|
||||
fetchPostsForChannel(serverUrl, channelId);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
@@ -101,15 +88,15 @@ export const backgroundNotification = async (serverUrl: string, notification: No
|
||||
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
if (lastDisconnectedAt) {
|
||||
// if (Platform.OS === 'ios') {
|
||||
// const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
// const isThreadNotification = isCRTEnabled && Boolean(notification.payload?.root_id);
|
||||
// if (isThreadNotification) {
|
||||
// updatePostsInThreadsSinceCache(serverUrl, notification);
|
||||
// } else {
|
||||
// updatePostSinceCache(serverUrl, notification);
|
||||
// }
|
||||
// }
|
||||
if (Platform.OS === 'ios') {
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(notification.payload?.root_id);
|
||||
if (isThreadNotification) {
|
||||
updatePostsInThreadsSinceCache(serverUrl, notification);
|
||||
} else {
|
||||
updatePostSinceCache(serverUrl, notification);
|
||||
}
|
||||
}
|
||||
|
||||
await fetchNotificationData(serverUrl, notification, true);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import NetInfo from '@react-native-community/netinfo';
|
||||
import {DeviceEventEmitter, Platform} from 'react-native';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {Database, Events} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {queryServerName} from '@queries/app/servers';
|
||||
import {getCurrentUserId, getCommonSystemValues, getExpiredSession} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {getCurrentUserId, getCommonSystemValues} from '@queries/servers/system';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {logWarning, logError} from '@utils/log';
|
||||
import {scheduleExpiredNotification} from '@utils/notification';
|
||||
import {logWarning} from '@utils/log';
|
||||
import {getCSRFFromCookie} from '@utils/security';
|
||||
import {getDeviceTimezone, isTimezoneEnabled} from '@utils/timezone';
|
||||
|
||||
import {loginEntry} from './entry';
|
||||
import {fetchDataRetentionPolicy} from './systems';
|
||||
import {autoUpdateTimezone} from './user';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type {LoginArgs} from '@typings/database/database';
|
||||
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
export const completeLogin = async (serverUrl: string) => {
|
||||
export const completeLogin = async (serverUrl: string, user: UserProfile) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -41,6 +38,12 @@ export const completeLogin = async (serverUrl: string) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set timezone
|
||||
if (isTimezoneEnabled(config)) {
|
||||
const timezone = getDeviceTimezone();
|
||||
await autoUpdateTimezone(serverUrl, {deviceTimezone: timezone, userId: user.id});
|
||||
}
|
||||
|
||||
// Data retention
|
||||
if (config?.DataRetentionEnableMessageDeletion === 'true' && license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
fetchDataRetentionPolicy(serverUrl);
|
||||
@@ -88,7 +91,7 @@ export const forceLogoutIfNecessary = async (serverUrl: string, err: ClientError
|
||||
return {error: null};
|
||||
};
|
||||
|
||||
export const fetchSessions = async (serverUrl: string, currentUserId: string) => {
|
||||
export const getSessions = async (serverUrl: string, currentUserId: string) => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
@@ -99,7 +102,6 @@ export const fetchSessions = async (serverUrl: string, currentUserId: string) =>
|
||||
try {
|
||||
return await client.getSessions(currentUserId);
|
||||
} catch (e) {
|
||||
logError('fetchSessions', e);
|
||||
await forceLogoutIfNecessary(serverUrl, e as ClientError);
|
||||
}
|
||||
|
||||
@@ -157,14 +159,14 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
|
||||
|
||||
try {
|
||||
const {error, hasTeams, time} = await loginEntry({serverUrl, user});
|
||||
completeLogin(serverUrl);
|
||||
completeLogin(serverUrl, user);
|
||||
return {error: error as ClientError, failed: false, hasTeams, time};
|
||||
} catch (error) {
|
||||
return {error: error as ClientError, failed: false, time: 0};
|
||||
}
|
||||
};
|
||||
|
||||
export const logout = async (serverUrl: string, skipServerLogout = false, removeServer = false, skipEvents = false) => {
|
||||
export const logout = async (serverUrl: string, skipServerLogout = false, removeServer = false) => {
|
||||
if (!skipServerLogout) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
@@ -175,65 +177,7 @@ export const logout = async (serverUrl: string, skipServerLogout = false, remove
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipEvents) {
|
||||
DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl, removeServer});
|
||||
}
|
||||
};
|
||||
|
||||
export const cancelSessionNotification = async (serverUrl: string) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const expiredSession = await getExpiredSession(database);
|
||||
const rechable = (await NetInfo.fetch()).isInternetReachable;
|
||||
|
||||
if (expiredSession?.notificationId && rechable) {
|
||||
PushNotifications.cancelScheduleNotification(parseInt(expiredSession.notificationId, 10));
|
||||
operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION,
|
||||
value: '',
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logError('cancelSessionNotification', e);
|
||||
}
|
||||
};
|
||||
|
||||
export const scheduleSessionNotification = async (serverUrl: string) => {
|
||||
try {
|
||||
const {database: appDatabase} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const sessions = await fetchSessions(serverUrl, 'me');
|
||||
const user = await getCurrentUser(database);
|
||||
const serverName = await queryServerName(appDatabase, serverUrl);
|
||||
|
||||
await cancelSessionNotification(serverUrl);
|
||||
|
||||
if (sessions) {
|
||||
const session = await findSession(serverUrl, sessions);
|
||||
|
||||
if (session) {
|
||||
const sessionId = session.id;
|
||||
const notificationId = scheduleExpiredNotification(serverUrl, session, serverName, user?.locale);
|
||||
operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION,
|
||||
value: {
|
||||
id: sessionId,
|
||||
notificationId,
|
||||
expiresAt: session.expires_at,
|
||||
},
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logError('scheduleExpiredNotification', e);
|
||||
await forceLogoutIfNecessary(serverUrl, e as ClientError);
|
||||
}
|
||||
DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl, removeServer});
|
||||
};
|
||||
|
||||
export const sendPasswordResetEmail = async (serverUrl: string, email: string) => {
|
||||
@@ -301,53 +245,9 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
|
||||
|
||||
try {
|
||||
const {error, hasTeams, time} = await loginEntry({serverUrl, user, deviceToken});
|
||||
completeLogin(serverUrl);
|
||||
completeLogin(serverUrl, user);
|
||||
return {error: error as ClientError, failed: false, hasTeams, time};
|
||||
} catch (error) {
|
||||
return {error: error as ClientError, failed: false, time: 0};
|
||||
}
|
||||
};
|
||||
|
||||
async function findSession(serverUrl: string, sessions: Session[]) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {database: appDatabase} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
const expiredSession = await getExpiredSession(database);
|
||||
const deviceToken = await getDeviceToken(appDatabase);
|
||||
|
||||
// First try and find the session by the given identifier hyqddef7jjdktqiyy36gxa8sqy
|
||||
let session = sessions.find((s) => s.id === expiredSession?.id);
|
||||
if (session) {
|
||||
return session;
|
||||
}
|
||||
|
||||
// Next try and find the session by deviceId
|
||||
if (deviceToken) {
|
||||
session = sessions.find((s) => s.device_id === deviceToken);
|
||||
if (session) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
// Next try and find the session by the CSRF token
|
||||
const csrfToken = await getCSRFFromCookie(serverUrl);
|
||||
if (csrfToken) {
|
||||
session = sessions.find((s) => s.props?.csrf === csrfToken);
|
||||
if (session) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
// Next try and find the session based on the OS
|
||||
// if multiple sessions exists with the same os type this can be inaccurate
|
||||
session = sessions.find((s) => s.props?.os.toLowerCase() === Platform.OS);
|
||||
if (session) {
|
||||
return session;
|
||||
}
|
||||
} catch (e) {
|
||||
logError('findSession', e);
|
||||
}
|
||||
|
||||
// At this point we did not find the session
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {storeConfigAndLicense} from '@actions/local/systems';
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
@@ -62,8 +65,34 @@ export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false
|
||||
client.getClientLicenseOld(),
|
||||
]);
|
||||
|
||||
// If we have credentials for this server then update the values in the database
|
||||
if (!fetchOnly) {
|
||||
await storeConfigAndLicense(serverUrl, config, license);
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (credentials && operator) {
|
||||
const current = await getCommonSystemValues(operator.database);
|
||||
const systems: IdValue[] = [];
|
||||
if (!deepEqual(config, current.config)) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.CONFIG,
|
||||
value: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
if (!deepEqual(license, current.license)) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.LICENSE,
|
||||
value: JSON.stringify(license),
|
||||
});
|
||||
}
|
||||
|
||||
if (systems.length) {
|
||||
await operator.handleSystem({systems, prepareRecordsOnly: false}).
|
||||
catch((error) => {
|
||||
logError('An error occurred while saving config & license', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {config, license};
|
||||
|
||||
@@ -55,7 +55,6 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
||||
|
||||
try {
|
||||
EphemeralStore.startAddingToTeam(teamId);
|
||||
const team = await client.getTeam(teamId);
|
||||
const member = await client.addToTeam(teamId, userId);
|
||||
|
||||
if (!fetchOnly) {
|
||||
@@ -69,7 +68,6 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
||||
}];
|
||||
|
||||
const models: Model[] = (await Promise.all([
|
||||
operator.handleTeam({teams: [team], prepareRecordsOnly: true}),
|
||||
operator.handleMyTeam({myTeams, prepareRecordsOnly: true}),
|
||||
operator.handleTeamMemberships({teamMemberships: [member], prepareRecordsOnly: true}),
|
||||
...await prepareMyChannelsForTeam(operator, teamId, channels || [], channelMembers || []),
|
||||
@@ -250,16 +248,6 @@ export async function fetchTeamByName(serverUrl: string, teamName: string, fetch
|
||||
}
|
||||
}
|
||||
|
||||
export const removeCurrentUserFromTeam = async (serverUrl: string, teamId: string, fetchOnly = false) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const userId = await getCurrentUserId(database);
|
||||
return removeUserFromTeam(serverUrl, teamId, userId, fetchOnly);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const removeUserFromTeam = async (serverUrl: string, teamId: string, userId: string, fetchOnly = false) => {
|
||||
let client;
|
||||
try {
|
||||
|
||||
@@ -54,7 +54,7 @@ export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string,
|
||||
}
|
||||
}
|
||||
|
||||
await switchToThread(serverUrl, rootId, isFromNotification);
|
||||
switchToThread(serverUrl, rootId, isFromNotification);
|
||||
|
||||
return {};
|
||||
};
|
||||
@@ -186,11 +186,7 @@ export const markThreadAsRead = async (serverUrl: string, teamId: string | undef
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const post = await getPostById(database, threadId);
|
||||
if (post) {
|
||||
if (isCRTEnabled) {
|
||||
PushNotifications.removeThreadNotifications(serverUrl, threadId);
|
||||
} else {
|
||||
PushNotifications.removeChannelNotifications(serverUrl, post.channelId);
|
||||
}
|
||||
PushNotifications.cancelChannelNotifications(post.channelId, threadId, isCRTEnabled);
|
||||
}
|
||||
|
||||
return {data};
|
||||
|
||||
@@ -15,10 +15,9 @@ import {debounce} from '@helpers/api/general';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getMembersCountByChannelsId, queryChannelsByTypes} from '@queries/servers/channel';
|
||||
import {queryGroupsByNames} from '@queries/servers/group';
|
||||
import {getConfig, getCurrentUserId} from '@queries/servers/system';
|
||||
import {getCurrentUser, prepareUsers, queryAllUsers, queryUsersById, queryUsersByIdsOrUsernames, queryUsersByUsername} from '@queries/servers/user';
|
||||
import {getCurrentTeamId, getCurrentUserId} from '@queries/servers/system';
|
||||
import {getCurrentUser, getUserById, prepareUsers, queryAllUsers, queryUsersById, queryUsersByIdsOrUsernames, queryUsersByUsername} from '@queries/servers/user';
|
||||
import {logError} from '@utils/log';
|
||||
import {getDeviceTimezone, isTimezoneEnabled} from '@utils/timezone';
|
||||
import {getUserTimezoneProps, removeUserFromList} from '@utils/user';
|
||||
|
||||
import {fetchGroupsByNames} from './groups';
|
||||
@@ -818,7 +817,7 @@ export const uploadUserProfileImage = async (serverUrl: string, localPath: strin
|
||||
return {error: undefined};
|
||||
};
|
||||
|
||||
export const searchUsers = async (serverUrl: string, term: string, teamId: string, channelId?: string) => {
|
||||
export const searchUsers = async (serverUrl: string, term: string, channelId?: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -832,7 +831,8 @@ export const searchUsers = async (serverUrl: string, term: string, teamId: strin
|
||||
}
|
||||
|
||||
try {
|
||||
const users = await client.autocompleteUsers(term, teamId, channelId);
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const users = await client.autocompleteUsers(term, currentTeamId, channelId);
|
||||
return {users};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
@@ -850,7 +850,7 @@ export const buildProfileImageUrl = (serverUrl: string, userId: string, timestam
|
||||
return client.getProfilePictureUrl(userId, timestamp);
|
||||
};
|
||||
|
||||
export const autoUpdateTimezone = async (serverUrl: string) => {
|
||||
export const autoUpdateTimezone = async (serverUrl: string, {deviceTimezone, userId}: {deviceTimezone: string; userId: string}) => {
|
||||
let database;
|
||||
try {
|
||||
const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
@@ -859,16 +859,12 @@ export const autoUpdateTimezone = async (serverUrl: string) => {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const config = await getConfig(database);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const currentUser = await getUserById(database, userId);
|
||||
|
||||
if (!currentUser || !config || !isTimezoneEnabled(config)) {
|
||||
if (!currentUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set timezone
|
||||
const deviceTimezone = getDeviceTimezone();
|
||||
|
||||
const currentTimezone = getUserTimezoneProps(currentUser);
|
||||
const newTimezoneExists = currentTimezone.automaticTimezone !== deviceTimezone;
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import {fetchMissingDirectChannelsInfo, fetchMyChannel, fetchChannelStats, fetch
|
||||
import {fetchPostsForChannel} from '@actions/remote/post';
|
||||
import {fetchRolesIfNeeded} from '@actions/remote/role';
|
||||
import {fetchUsersByIds, updateUsersNoLongerVisible} from '@actions/remote/user';
|
||||
import {loadCallForChannel} from '@calls/actions/calls';
|
||||
import {Events, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryActiveServer} from '@queries/app/servers';
|
||||
@@ -294,8 +293,6 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
|
||||
models.push(...prepared);
|
||||
}
|
||||
}
|
||||
|
||||
loadCallForChannel(serverUrl, channelId);
|
||||
} else {
|
||||
const addedUser = getUserById(database, userId);
|
||||
if (!addedUser) {
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {deferredAppEntryActions, entry} from '@actions/remote/entry/gql_common';
|
||||
import {deferredAppEntryActions, entry} from '@actions/remote/entry/common';
|
||||
import {graphQLCommon} from '@actions/remote/entry/gql_common';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {fetchStatusByIds} from '@actions/remote/user';
|
||||
import {loadConfigAndCalls} from '@calls/actions/calls';
|
||||
import {
|
||||
handleCallChannelDisabled,
|
||||
handleCallChannelEnabled,
|
||||
handleCallEnded,
|
||||
handleCallChannelEnabled, handleCallEnded,
|
||||
handleCallScreenOff,
|
||||
handleCallScreenOn,
|
||||
handleCallStarted,
|
||||
@@ -27,6 +28,7 @@ import {isSupportedServerCalls} from '@calls/utils';
|
||||
import {Events, Screens, WebsocketEvents} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import {getActiveServerUrl, queryActiveServer} from '@queries/app/servers';
|
||||
import {getCurrentChannel} from '@queries/servers/channel';
|
||||
import {
|
||||
@@ -113,26 +115,20 @@ export async function handleClose(serverUrl: string, lastDisconnect: number) {
|
||||
});
|
||||
}
|
||||
|
||||
async function doReconnect(serverUrl: string) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
|
||||
async function doReconnectRest(serverUrl: string, operator: ServerDataOperator, currentTeamId: string, currentUserId: string, config: ClientConfig, license: ClientLicense, lastDisconnectedAt: number) {
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
resetWebSocketLastDisconnected(operator);
|
||||
|
||||
const currentTeam = await getCurrentTeam(database);
|
||||
const currentChannel = await getCurrentChannel(database);
|
||||
const currentActiveServerUrl = await getActiveServerUrl(DatabaseManager.appDatabase!.database);
|
||||
|
||||
if (serverUrl === currentActiveServerUrl) {
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, true);
|
||||
}
|
||||
const entryData = await entry(serverUrl, currentTeam?.id, currentChannel?.id, lastDisconnectedAt);
|
||||
if ('error' in entryData) {
|
||||
if (serverUrl === currentActiveServerUrl) {
|
||||
@@ -176,8 +172,7 @@ async function doReconnect(serverUrl: string) {
|
||||
await operator.batchRecords(models);
|
||||
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(database))!;
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
const {locale: currentUserLocale} = (await getCurrentUser(database))!;
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchedToChannel ? initialChannelId : undefined);
|
||||
|
||||
if (isSupportedServerCalls(config?.Version)) {
|
||||
@@ -187,6 +182,33 @@ async function doReconnect(serverUrl: string) {
|
||||
// https://mattermost.atlassian.net/browse/MM-41520
|
||||
}
|
||||
|
||||
async function doReconnect(serverUrl: string) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const system = await getCommonSystemValues(database);
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
|
||||
resetWebSocketLastDisconnected(operator);
|
||||
let {config, license} = await fetchConfigAndLicense(serverUrl);
|
||||
if (!config) {
|
||||
config = system.config;
|
||||
}
|
||||
|
||||
if (!license) {
|
||||
license = system.license;
|
||||
}
|
||||
|
||||
if (config.FeatureFlagGraphQL === 'true') {
|
||||
await graphQLCommon(serverUrl, true, system.currentTeamId, system.currentChannelId);
|
||||
} else {
|
||||
await doReconnectRest(serverUrl, operator, system.currentTeamId, system.currentUserId, config, license, lastDisconnectedAt);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.POSTED:
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function handleThreadUpdatedEvent(serverUrl: string, msg: WebSocket
|
||||
|
||||
// Mark it as following
|
||||
thread.is_following = true;
|
||||
processReceivedThreads(serverUrl, [thread], teamId);
|
||||
processReceivedThreads(serverUrl, [thread], teamId, true);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {Client} from '@client/rest';
|
||||
import {MEMBERS_PER_PAGE} from '@constants/graphql';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
import {Client} from '../rest';
|
||||
|
||||
import QueryNames from './constants';
|
||||
|
||||
const doGQLQuery = async (serverUrl: string, query: string, variables: {[name: string]: any}, operationName: string) => {
|
||||
@@ -15,7 +16,7 @@ const doGQLQuery = async (serverUrl: string, query: string, variables: {[name: s
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await client.doFetch('/api/v5/graphql', {method: 'post', body: {query, variables, operationName}}) as GQLResponse;
|
||||
const response = await client.doFetch('/api/v5/graphql', {method: 'post', body: JSON.stringify({query, variables, operationName})}) as GQLResponse;
|
||||
return response;
|
||||
} catch (error) {
|
||||
return {error};
|
||||
@@ -187,7 +188,6 @@ query ${QueryNames.QUERY_ENTRY} {
|
||||
}
|
||||
teamMembers(userId:"me") {
|
||||
deleteAt
|
||||
schemeAdmin
|
||||
roles {
|
||||
id
|
||||
name
|
||||
@@ -221,8 +221,6 @@ query ${QueryNames.QUERY_CHANNELS}($teamId: String!, $perPage: Int!, $exclude: B
|
||||
msgCount
|
||||
msgCountRoot
|
||||
mentionCount
|
||||
mentionCountRoot
|
||||
schemeAdmin
|
||||
lastViewedAt
|
||||
notifyProps
|
||||
roles {
|
||||
@@ -236,7 +234,6 @@ query ${QueryNames.QUERY_CHANNELS}($teamId: String!, $perPage: Int!, $exclude: B
|
||||
purpose
|
||||
type
|
||||
createAt
|
||||
updateAt
|
||||
creatorId
|
||||
deleteAt
|
||||
displayName
|
||||
@@ -256,7 +253,6 @@ query ${QueryNames.QUERY_CHANNELS}($teamId: String!, $perPage: Int!, $exclude: B
|
||||
sidebarCategories(userId:"me", teamId:$teamId, excludeTeam:$exclude) {
|
||||
displayName
|
||||
id
|
||||
sortOrder
|
||||
sorting
|
||||
type
|
||||
muted
|
||||
@@ -274,8 +270,6 @@ query ${QueryNames.QUERY_CHANNELS_NEXT}($teamId: String!, $perPage: Int!, $exclu
|
||||
msgCount
|
||||
msgCountRoot
|
||||
mentionCount
|
||||
mentionCountRoot
|
||||
schemeAdmin
|
||||
lastViewedAt
|
||||
notifyProps
|
||||
roles {
|
||||
@@ -289,7 +283,6 @@ query ${QueryNames.QUERY_CHANNELS_NEXT}($teamId: String!, $perPage: Int!, $exclu
|
||||
purpose
|
||||
type
|
||||
createAt
|
||||
updateAt
|
||||
creatorId
|
||||
deleteAt
|
||||
displayName
|
||||
@@ -316,22 +309,14 @@ query ${QueryNames.QUERY_ALL_CHANNELS}($perPage: Int!){
|
||||
msgCount
|
||||
msgCountRoot
|
||||
mentionCount
|
||||
mentionCountRoot
|
||||
schemeAdmin
|
||||
lastViewedAt
|
||||
notifyProps
|
||||
roles {
|
||||
id
|
||||
name
|
||||
permissions
|
||||
}
|
||||
channel {
|
||||
id
|
||||
header
|
||||
purpose
|
||||
type
|
||||
createAt
|
||||
updateAt
|
||||
creatorId
|
||||
deleteAt
|
||||
displayName
|
||||
@@ -358,22 +343,14 @@ query ${QueryNames.QUERY_ALL_CHANNELS_NEXT}($perPage: Int!, $cursor: String!) {
|
||||
msgCount
|
||||
msgCountRoot
|
||||
mentionCount
|
||||
mentionCountRoot
|
||||
schemeAdmin
|
||||
lastViewedAt
|
||||
notifyProps
|
||||
roles {
|
||||
id
|
||||
name
|
||||
permissions
|
||||
}
|
||||
channel {
|
||||
id
|
||||
header
|
||||
purpose
|
||||
type
|
||||
createAt
|
||||
updateAt
|
||||
creatorId
|
||||
deleteAt
|
||||
displayName
|
||||
|
||||
@@ -112,7 +112,12 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
convertChannelToPrivate = async (channelId: string) => {
|
||||
this.updateChannelPrivacy(channelId, 'P');
|
||||
this.analytics.trackAPI('api_channels_convert_to_private', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/convert`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
updateChannelPrivacy = async (channelId: string, privacy: any) => {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
exports[`@components/app_version should match snapshot 1`] = `
|
||||
<View
|
||||
animatedStyle={
|
||||
{
|
||||
"value": {
|
||||
Object {
|
||||
"value": Object {
|
||||
"opacity": 1,
|
||||
},
|
||||
}
|
||||
@@ -12,14 +12,14 @@ exports[`@components/app_version should match snapshot 1`] = `
|
||||
collapsable={false}
|
||||
pointerEvents="none"
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"marginBottom": 12,
|
||||
"marginLeft": 20,
|
||||
@@ -29,7 +29,7 @@ exports[`@components/app_version should match snapshot 1`] = `
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {debounce} from 'lodash';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {Platform, SectionList, SectionListData, SectionListRenderItemInfo, StyleProp, ViewStyle} from 'react-native';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {Platform, SectionList, SectionListData, SectionListRenderItemInfo} from 'react-native';
|
||||
|
||||
import {searchGroupsByName, searchGroupsByNameInChannel, searchGroupsByNameInTeam} from '@actions/local/group';
|
||||
import {searchUsers} from '@actions/remote/user';
|
||||
@@ -13,10 +13,11 @@ import AutocompleteSectionHeader from '@components/autocomplete/autocomplete_sec
|
||||
import SpecialMentionItem from '@components/autocomplete/special_mention_item';
|
||||
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {t} from '@i18n';
|
||||
import {queryAllUsers} from '@queries/servers/user';
|
||||
import {hasTrailingSpaces} from '@utils/helpers';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type GroupModel from '@typings/database/models/servers/group';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
@@ -84,16 +85,13 @@ const keyExtractor = (item: UserProfile) => {
|
||||
return item.id;
|
||||
};
|
||||
|
||||
const filterResults = (users: Array<UserModel | UserProfile>, term: string) => {
|
||||
return users.filter((u) => {
|
||||
const firstName = ('firstName' in u ? u.firstName : u.first_name).toLowerCase();
|
||||
const lastName = ('lastName' in u ? u.lastName : u.last_name).toLowerCase();
|
||||
const fullName = `${firstName} ${lastName}`;
|
||||
return u.username.toLowerCase().includes(term) ||
|
||||
u.nickname.toLowerCase().includes(term) ||
|
||||
fullName.includes(term) ||
|
||||
u.email.toLowerCase().includes(term);
|
||||
});
|
||||
const filterLocalResults = (users: UserModel[], term: string) => {
|
||||
return users.filter((u) =>
|
||||
u.username.toLowerCase().startsWith(term) ||
|
||||
u.nickname.toLowerCase().startsWith(term) ||
|
||||
u.firstName.toLowerCase().startsWith(term) ||
|
||||
u.lastName.toLowerCase().startsWith(term),
|
||||
);
|
||||
};
|
||||
|
||||
const makeSections = (teamMembers: Array<UserProfile | UserModel>, usersInChannel: Array<UserProfile | UserModel>, usersOutOfChannel: Array<UserProfile | UserModel>, groups: GroupModel[], showSpecialMentions: boolean, isLocal = false, isSearch = false) => {
|
||||
@@ -175,37 +173,12 @@ const makeSections = (teamMembers: Array<UserProfile | UserModel>, usersInChanne
|
||||
return newSections;
|
||||
};
|
||||
|
||||
const searchGroups = async (serverUrl: string, matchTerm: string, useGroupMentions: boolean, isChannelConstrained: boolean, isTeamConstrained: boolean, channelId?: string, teamId?: string) => {
|
||||
try {
|
||||
if (useGroupMentions && matchTerm && matchTerm !== '') {
|
||||
let g = emptyGroupList;
|
||||
|
||||
if (isChannelConstrained) {
|
||||
// If the channel is constrained, we only show groups for that channel
|
||||
if (channelId) {
|
||||
g = await searchGroupsByNameInChannel(serverUrl, matchTerm, channelId);
|
||||
}
|
||||
} else if (isTeamConstrained) {
|
||||
// If there is no channel constraint, but a team constraint - only show groups for team
|
||||
g = await searchGroupsByNameInTeam(serverUrl, matchTerm, teamId!);
|
||||
} else {
|
||||
// No constraints? Search all groups
|
||||
g = await searchGroupsByName(serverUrl, matchTerm || '');
|
||||
}
|
||||
|
||||
return g.length ? g : emptyGroupList;
|
||||
}
|
||||
return emptyGroupList;
|
||||
} catch (error) {
|
||||
return emptyGroupList;
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
channelId?: string;
|
||||
teamId: string;
|
||||
teamId?: string;
|
||||
cursorPosition: number;
|
||||
isSearch: boolean;
|
||||
maxListHeight: number;
|
||||
updateValue: (v: string) => void;
|
||||
onShowingChange: (c: boolean) => void;
|
||||
value: string;
|
||||
@@ -214,10 +187,19 @@ type Props = {
|
||||
useGroupMentions: boolean;
|
||||
isChannelConstrained: boolean;
|
||||
isTeamConstrained: boolean;
|
||||
listStyle: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const emptyUserlList: Array<UserModel | UserProfile> = [];
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
listView: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderRadius: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const emptyProfileList: UserProfile[] = [];
|
||||
const emptyModelList: UserModel[] = [];
|
||||
const emptySectionList: UserMentionSections = [];
|
||||
const emptyGroupList: GroupModel[] = [];
|
||||
|
||||
@@ -235,6 +217,7 @@ const AtMention = ({
|
||||
teamId,
|
||||
cursorPosition,
|
||||
isSearch,
|
||||
maxListHeight,
|
||||
updateValue,
|
||||
onShowingChange,
|
||||
value,
|
||||
@@ -243,37 +226,25 @@ const AtMention = ({
|
||||
useGroupMentions,
|
||||
isChannelConstrained,
|
||||
isTeamConstrained,
|
||||
listStyle,
|
||||
}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const [sections, setSections] = useState<UserMentionSections>(emptySectionList);
|
||||
const [usersInChannel, setUsersInChannel] = useState<Array<UserProfile | UserModel>>(emptyUserlList);
|
||||
const [usersOutOfChannel, setUsersOutOfChannel] = useState<Array<UserProfile | UserModel>>(emptyUserlList);
|
||||
const [usersInChannel, setUsersInChannel] = useState<UserProfile[]>(emptyProfileList);
|
||||
const [usersOutOfChannel, setUsersOutOfChannel] = useState<UserProfile[]>(emptyProfileList);
|
||||
const [groups, setGroups] = useState<GroupModel[]>(emptyGroupList);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [noResultsTerm, setNoResultsTerm] = useState<string|null>(null);
|
||||
const [localCursorPosition, setLocalCursorPosition] = useState(cursorPosition); // To avoid errors due to delay between value changes and cursor position changes.
|
||||
const [useLocal, setUseLocal] = useState(true);
|
||||
const [localUsers, setLocalUsers] = useState<UserModel[]>();
|
||||
const [filteredLocalUsers, setFilteredLocalUsers] = useState(emptyUserlList);
|
||||
const [filteredLocalUsers, setFilteredLocalUsers] = useState(emptyModelList);
|
||||
|
||||
const latestSearchAt = useRef(0);
|
||||
|
||||
const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, groupMentions: boolean, channelConstrained: boolean, teamConstrained: boolean, tId: string, cId?: string) => {
|
||||
const searchAt = Date.now();
|
||||
latestSearchAt.current = searchAt;
|
||||
|
||||
const [{users: receivedUsers, error}, groupsResult] = await Promise.all([
|
||||
searchUsers(sUrl, term, tId, cId),
|
||||
searchGroups(sUrl, term, groupMentions, channelConstrained, teamConstrained, cId, tId),
|
||||
]);
|
||||
|
||||
if (latestSearchAt.current > searchAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
setGroups(groupsResult);
|
||||
const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, cId?: string) => {
|
||||
setLoading(true);
|
||||
const {users: receivedUsers, error} = await searchUsers(sUrl, term, cId);
|
||||
|
||||
setUseLocal(Boolean(error));
|
||||
if (error) {
|
||||
@@ -282,23 +253,11 @@ const AtMention = ({
|
||||
fallbackUsers = await getAllUsers(sUrl);
|
||||
setLocalUsers(fallbackUsers);
|
||||
}
|
||||
if (latestSearchAt.current > searchAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredUsers = filterResults(fallbackUsers, term);
|
||||
setFilteredLocalUsers(filteredUsers.length ? filteredUsers : emptyUserlList);
|
||||
const filteredUsers = filterLocalResults(fallbackUsers, term);
|
||||
setFilteredLocalUsers(filteredUsers.length ? filteredUsers : emptyModelList);
|
||||
} else if (receivedUsers) {
|
||||
if (hasTrailingSpaces(term)) {
|
||||
const filteredReceivedUsers = filterResults(receivedUsers.users, term);
|
||||
const filteredReceivedOutOfChannelUsers = filterResults(receivedUsers.out_of_channel || [], term);
|
||||
|
||||
setUsersInChannel(filteredReceivedUsers.length ? filteredReceivedUsers : emptyUserlList);
|
||||
setUsersOutOfChannel(filteredReceivedOutOfChannelUsers.length ? filteredReceivedOutOfChannelUsers : emptyUserlList);
|
||||
} else {
|
||||
setUsersInChannel(receivedUsers.users.length ? receivedUsers.users : emptyUserlList);
|
||||
setUsersOutOfChannel(receivedUsers.out_of_channel?.length ? receivedUsers.out_of_channel : emptyUserlList);
|
||||
}
|
||||
setUsersInChannel(receivedUsers.users.length ? receivedUsers.users : emptyProfileList);
|
||||
setUsersOutOfChannel(receivedUsers.out_of_channel?.length ? receivedUsers.out_of_channel : emptyProfileList);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
@@ -311,14 +270,10 @@ const AtMention = ({
|
||||
|
||||
const matchTerm = getMatchTermForAtMention(value.substring(0, localCursorPosition), isSearch);
|
||||
const resetState = () => {
|
||||
setUsersInChannel(emptyUserlList);
|
||||
setUsersOutOfChannel(emptyUserlList);
|
||||
setGroups(emptyGroupList);
|
||||
setFilteredLocalUsers(emptyUserlList);
|
||||
setUsersInChannel(emptyProfileList);
|
||||
setUsersOutOfChannel(emptyProfileList);
|
||||
setFilteredLocalUsers(emptyModelList);
|
||||
setSections(emptySectionList);
|
||||
setNoResultsTerm(null);
|
||||
latestSearchAt.current = Date.now();
|
||||
setLoading(false);
|
||||
runSearch.cancel();
|
||||
};
|
||||
|
||||
@@ -332,7 +287,7 @@ const AtMention = ({
|
||||
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
|
||||
}
|
||||
|
||||
const newCursorPosition = completedDraft.length;
|
||||
const newCursorPosition = completedDraft.length - 1;
|
||||
|
||||
if (value.length > cursorPosition) {
|
||||
completedDraft += value.substring(cursorPosition);
|
||||
@@ -344,7 +299,6 @@ const AtMention = ({
|
||||
onShowingChange(false);
|
||||
setNoResultsTerm(mention);
|
||||
setSections(emptySectionList);
|
||||
latestSearchAt.current = Date.now();
|
||||
}, [value, localCursorPosition, isSearch]);
|
||||
|
||||
const renderSpecialMentions = useCallback((item: SpecialMention) => {
|
||||
@@ -409,6 +363,39 @@ const AtMention = ({
|
||||
}
|
||||
}, [cursorPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (useGroupMentions && matchTerm && matchTerm !== '') {
|
||||
// If the channel is constrained, we only show groups for that channel
|
||||
if (isChannelConstrained && channelId) {
|
||||
searchGroupsByNameInChannel(serverUrl, matchTerm, channelId).then((g) => {
|
||||
setGroups(g.length ? g : emptyGroupList);
|
||||
}).catch(() => {
|
||||
setGroups(emptyGroupList);
|
||||
});
|
||||
}
|
||||
|
||||
// If there is no channel constraint, but a team constraint - only show groups for team
|
||||
if (isTeamConstrained && !isChannelConstrained) {
|
||||
searchGroupsByNameInTeam(serverUrl, matchTerm, teamId!).then((g) => {
|
||||
setGroups(g.length ? g : emptyGroupList);
|
||||
}).catch(() => {
|
||||
setGroups(emptyGroupList);
|
||||
});
|
||||
}
|
||||
|
||||
// No constraints? Search all groups
|
||||
if (!isTeamConstrained && !isChannelConstrained) {
|
||||
searchGroupsByName(serverUrl, matchTerm || '').then((g) => {
|
||||
setGroups(Array.isArray(g) ? g : emptyGroupList);
|
||||
}).catch(() => {
|
||||
setGroups(emptyGroupList);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setGroups(emptyGroupList);
|
||||
}
|
||||
}, [matchTerm, useGroupMentions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (matchTerm === null) {
|
||||
resetState();
|
||||
@@ -421,14 +408,10 @@ const AtMention = ({
|
||||
}
|
||||
|
||||
setNoResultsTerm(null);
|
||||
setLoading(true);
|
||||
runSearch(serverUrl, matchTerm, useGroupMentions, isChannelConstrained, isTeamConstrained, teamId, channelId);
|
||||
}, [matchTerm, teamId, useGroupMentions, isChannelConstrained, isTeamConstrained]);
|
||||
runSearch(serverUrl, matchTerm, channelId);
|
||||
}, [matchTerm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (noResultsTerm && !loading) {
|
||||
return;
|
||||
}
|
||||
const showSpecialMentions = useChannelMentions && matchTerm != null && checkSpecialMentions(matchTerm);
|
||||
const buildMemberSection = isSearch || (!channelId && teamMembers.length > 0);
|
||||
let newSections;
|
||||
@@ -442,10 +425,6 @@ const AtMention = ({
|
||||
if (!loading && !nSections && noResultsTerm == null) {
|
||||
setNoResultsTerm(matchTerm);
|
||||
}
|
||||
|
||||
if (nSections && noResultsTerm) {
|
||||
setNoResultsTerm(null);
|
||||
}
|
||||
setSections(nSections ? newSections : emptySectionList);
|
||||
onShowingChange(Boolean(nSections));
|
||||
}, [!useLocal && usersInChannel, !useLocal && usersOutOfChannel, teamMembers, groups, loading, channelId, useLocal && filteredLocalUsers]);
|
||||
@@ -465,7 +444,7 @@ const AtMention = ({
|
||||
removeClippedSubviews={Platform.OS === 'android'}
|
||||
renderItem={renderItem}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
style={listStyle}
|
||||
style={[style.listView, {maxHeight: maxListHeight}]}
|
||||
sections={sections}
|
||||
testID='autocomplete.at_mention.section_list'
|
||||
/>
|
||||
|
||||
@@ -18,11 +18,8 @@ import AtMention from './at_mention';
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type OwnProps = {
|
||||
channelId?: string;
|
||||
teamId?: string;
|
||||
}
|
||||
const enhanced = withObservables(['teamId'], ({database, channelId, teamId}: WithDatabaseArgs & OwnProps) => {
|
||||
type OwnProps = {channelId?: string}
|
||||
const enhanced = withObservables([], ({database, channelId}: WithDatabaseArgs & OwnProps) => {
|
||||
const currentUser = observeCurrentUser(database);
|
||||
|
||||
const hasLicense = observeLicense(database).pipe(
|
||||
@@ -54,19 +51,20 @@ const enhanced = withObservables(['teamId'], ({database, channelId, teamId}: Wit
|
||||
useGroupMentions = of$(false);
|
||||
isChannelConstrained = of$(false);
|
||||
isTeamConstrained = of$(false);
|
||||
team = teamId ? observeTeam(database, teamId) : observeCurrentTeam(database);
|
||||
team = observeCurrentTeam(database);
|
||||
}
|
||||
|
||||
isTeamConstrained = team.pipe(
|
||||
switchMap((t) => of$(Boolean(t?.isGroupConstrained))),
|
||||
);
|
||||
const teamId = team.pipe(switchMap((t) => of$(t?.id)));
|
||||
|
||||
return {
|
||||
isChannelConstrained,
|
||||
isTeamConstrained,
|
||||
useChannelMentions,
|
||||
useGroupMentions,
|
||||
teamId: team.pipe(switchMap((t) => of$(t?.id))),
|
||||
teamId,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo, useState} from 'react';
|
||||
import {Platform, StyleProp, useWindowDimensions, ViewStyle} from 'react-native';
|
||||
import Animated, {SharedValue, useAnimatedStyle, useDerivedValue} from 'react-native-reanimated';
|
||||
import {Platform, useWindowDimensions, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {MAX_LIST_HEIGHT, MAX_LIST_TABLET_DIFF} from '@constants/autocomplete';
|
||||
import {LIST_BOTTOM, MAX_LIST_DIFF, MAX_LIST_HEIGHT, MAX_LIST_TABLET_DIFF} from '@constants/autocomplete';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
@@ -20,8 +20,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
base: {
|
||||
left: 8,
|
||||
right: 8,
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
},
|
||||
borders: {
|
||||
borderWidth: 1,
|
||||
@@ -30,6 +30,16 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
borderRadius: 8,
|
||||
elevation: 3,
|
||||
},
|
||||
searchContainer: {
|
||||
...Platform.select({
|
||||
android: {
|
||||
top: 42,
|
||||
},
|
||||
ios: {
|
||||
top: 55,
|
||||
},
|
||||
}),
|
||||
},
|
||||
shadow: {
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.12,
|
||||
@@ -39,18 +49,16 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
height: 6,
|
||||
},
|
||||
},
|
||||
listStyle: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderRadius: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
cursorPosition: number;
|
||||
position: SharedValue<number>;
|
||||
postInputTop: number;
|
||||
paddingTop?: number;
|
||||
rootId?: string;
|
||||
channelId?: string;
|
||||
fixedBottomPosition?: boolean;
|
||||
isSearch?: boolean;
|
||||
value: string;
|
||||
enableDateSuggestion?: boolean;
|
||||
@@ -58,21 +66,20 @@ type Props = {
|
||||
nestedScrollEnabled?: boolean;
|
||||
updateValue: (v: string) => void;
|
||||
hasFilesAttached?: boolean;
|
||||
availableSpace: SharedValue<number>;
|
||||
maxHeightOverride?: number;
|
||||
inPost?: boolean;
|
||||
growDown?: boolean;
|
||||
teamId?: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const Autocomplete = ({
|
||||
cursorPosition,
|
||||
position,
|
||||
postInputTop,
|
||||
paddingTop,
|
||||
rootId,
|
||||
channelId,
|
||||
isSearch = false,
|
||||
fixedBottomPosition,
|
||||
value,
|
||||
availableSpace,
|
||||
maxHeightOverride,
|
||||
|
||||
//enableDateSuggestion = false,
|
||||
isAppsEnabled,
|
||||
@@ -80,14 +87,12 @@ const Autocomplete = ({
|
||||
updateValue,
|
||||
hasFilesAttached,
|
||||
inPost = false,
|
||||
growDown = false,
|
||||
containerStyle,
|
||||
teamId,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const dimensions = useWindowDimensions();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [showingAtMention, setShowingAtMention] = useState(false);
|
||||
const [showingChannelMention, setShowingChannelMention] = useState(false);
|
||||
@@ -101,76 +106,88 @@ const Autocomplete = ({
|
||||
const appsTakeOver = showingAppCommand;
|
||||
const showCommands = !(showingChannelMention || showingEmoji || showingAtMention);
|
||||
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
const maxHeightAdjust = (isTablet && isLandscape) ? MAX_LIST_TABLET_DIFF : 0;
|
||||
const defaultMaxHeight = MAX_LIST_HEIGHT - maxHeightAdjust;
|
||||
const maxHeight = useDerivedValue(() => {
|
||||
return Math.min(availableSpace.value, defaultMaxHeight);
|
||||
}, [defaultMaxHeight]);
|
||||
|
||||
const containerAnimatedStyle = useAnimatedStyle(() => {
|
||||
return growDown ?
|
||||
{top: position.value, bottom: Platform.OS === 'ios' ? 'auto' : undefined, maxHeight: maxHeight.value} :
|
||||
{top: Platform.OS === 'ios' ? 'auto' : undefined, bottom: position.value, maxHeight: maxHeight.value};
|
||||
}, [growDown, position]);
|
||||
|
||||
const containerStyles = useMemo(() => {
|
||||
const s = [style.base, containerAnimatedStyle];
|
||||
if (hasElements) {
|
||||
s.push(style.borders);
|
||||
const maxListHeight = useMemo(() => {
|
||||
if (maxHeightOverride) {
|
||||
return maxHeightOverride;
|
||||
}
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
let postInputDiff = 0;
|
||||
if (isTablet && postInputTop && isLandscape) {
|
||||
postInputDiff = MAX_LIST_TABLET_DIFF;
|
||||
} else if (postInputTop) {
|
||||
postInputDiff = MAX_LIST_DIFF;
|
||||
}
|
||||
return MAX_LIST_HEIGHT - postInputDiff;
|
||||
}, [maxHeightOverride, postInputTop, isTablet, dimensions.width]);
|
||||
|
||||
const wrapperStyles = useMemo(() => {
|
||||
const s = [];
|
||||
if (Platform.OS === 'ios') {
|
||||
s.push(style.shadow);
|
||||
}
|
||||
if (containerStyle) {
|
||||
s.push(containerStyle);
|
||||
if (isSearch) {
|
||||
s.push(style.base, paddingTop ? {top: paddingTop} : style.searchContainer, {maxHeight: maxListHeight});
|
||||
}
|
||||
return s;
|
||||
}, [hasElements, style, containerStyle, containerAnimatedStyle]);
|
||||
}, [style, isSearch && maxListHeight, paddingTop]);
|
||||
|
||||
const containerStyles = useMemo(() => {
|
||||
const s = [];
|
||||
if (!isSearch && !fixedBottomPosition) {
|
||||
const iOSInsets = Platform.OS === 'ios' && (!isTablet || rootId) ? insets.bottom : 0;
|
||||
s.push(style.base, {bottom: postInputTop + LIST_BOTTOM + iOSInsets});
|
||||
} else if (fixedBottomPosition) {
|
||||
s.push(style.base, {bottom: 0});
|
||||
}
|
||||
if (hasElements) {
|
||||
s.push(style.borders);
|
||||
}
|
||||
return s;
|
||||
}, [!isSearch, isTablet, hasElements, postInputTop]);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
testID='autocomplete'
|
||||
style={containerStyles}
|
||||
<View
|
||||
style={wrapperStyles}
|
||||
>
|
||||
{isAppsEnabled && channelId && (
|
||||
<AppSlashSuggestion
|
||||
listStyle={style.listStyle}
|
||||
updateValue={updateValue}
|
||||
onShowingChange={setShowingAppCommand}
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
/>
|
||||
)}
|
||||
{(!appsTakeOver || !isAppsEnabled) && (<>
|
||||
<AtMention
|
||||
cursorPosition={cursorPosition}
|
||||
listStyle={style.listStyle}
|
||||
updateValue={updateValue}
|
||||
onShowingChange={setShowingAtMention}
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
isSearch={isSearch}
|
||||
channelId={channelId}
|
||||
teamId={teamId}
|
||||
/>
|
||||
<ChannelMention
|
||||
cursorPosition={cursorPosition}
|
||||
listStyle={style.listStyle}
|
||||
updateValue={updateValue}
|
||||
onShowingChange={setShowingChannelMention}
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
isSearch={isSearch}
|
||||
channelId={channelId}
|
||||
teamId={teamId}
|
||||
/>
|
||||
{!isSearch &&
|
||||
<View
|
||||
testID='autocomplete'
|
||||
style={containerStyles}
|
||||
>
|
||||
{isAppsEnabled && channelId && (
|
||||
<AppSlashSuggestion
|
||||
maxListHeight={maxListHeight}
|
||||
updateValue={updateValue}
|
||||
onShowingChange={setShowingAppCommand}
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
/>
|
||||
)}
|
||||
{(!appsTakeOver || !isAppsEnabled) && (<>
|
||||
<AtMention
|
||||
cursorPosition={cursorPosition}
|
||||
maxListHeight={maxListHeight}
|
||||
updateValue={updateValue}
|
||||
onShowingChange={setShowingAtMention}
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
isSearch={isSearch}
|
||||
channelId={channelId}
|
||||
/>
|
||||
<ChannelMention
|
||||
cursorPosition={cursorPosition}
|
||||
maxListHeight={maxListHeight}
|
||||
updateValue={updateValue}
|
||||
onShowingChange={setShowingChannelMention}
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
isSearch={isSearch}
|
||||
/>
|
||||
{!isSearch &&
|
||||
<EmojiSuggestion
|
||||
cursorPosition={cursorPosition}
|
||||
listStyle={style.listStyle}
|
||||
maxListHeight={maxListHeight}
|
||||
updateValue={updateValue}
|
||||
onShowingChange={setShowingEmoji}
|
||||
value={value || ''}
|
||||
@@ -179,10 +196,10 @@ const Autocomplete = ({
|
||||
hasFilesAttached={hasFilesAttached}
|
||||
inPost={inPost}
|
||||
/>
|
||||
}
|
||||
{showCommands && channelId &&
|
||||
}
|
||||
{showCommands && channelId &&
|
||||
<SlashSuggestion
|
||||
listStyle={style.listStyle}
|
||||
maxListHeight={maxListHeight}
|
||||
updateValue={updateValue}
|
||||
onShowingChange={setShowingCommand}
|
||||
value={value || ''}
|
||||
@@ -190,8 +207,8 @@ const Autocomplete = ({
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
/>
|
||||
}
|
||||
{/* {(isSearch && enableDateSuggestion) &&
|
||||
}
|
||||
{/* {(isSearch && enableDateSuggestion) &&
|
||||
<DateSuggestion
|
||||
cursorPosition={cursorPosition}
|
||||
updateValue={updateValue}
|
||||
@@ -199,8 +216,9 @@ const Autocomplete = ({
|
||||
value={value || ''}
|
||||
/>
|
||||
} */}
|
||||
</>)}
|
||||
</Animated.View>
|
||||
</>)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {debounce} from 'lodash';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {Platform, SectionList, SectionListData, SectionListRenderItemInfo, StyleProp, ViewStyle} from 'react-native';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {Platform, SectionList, SectionListData, SectionListRenderItemInfo} from 'react-native';
|
||||
|
||||
import {searchChannels} from '@actions/remote/channel';
|
||||
import AutocompleteSectionHeader from '@components/autocomplete/autocomplete_section_header';
|
||||
@@ -11,9 +11,13 @@ import ChannelMentionItem from '@components/autocomplete/channel_mention_item';
|
||||
import {General} from '@constants';
|
||||
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import {t} from '@i18n';
|
||||
import {hasTrailingSpaces} from '@utils/helpers';
|
||||
import {queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
@@ -22,6 +26,32 @@ const keyExtractor = (item: Channel) => {
|
||||
return item.id;
|
||||
};
|
||||
|
||||
const getMatchTermForChannelMention = (() => {
|
||||
let lastMatchTerm: string | null = null;
|
||||
let lastValue: string;
|
||||
let lastIsSearch: boolean;
|
||||
return (value: string, isSearch: boolean) => {
|
||||
if (value !== lastValue || isSearch !== lastIsSearch) {
|
||||
const regex = isSearch ? CHANNEL_MENTION_SEARCH_REGEX : CHANNEL_MENTION_REGEX;
|
||||
const match = value.match(regex);
|
||||
lastValue = value;
|
||||
lastIsSearch = isSearch;
|
||||
if (match) {
|
||||
if (isSearch) {
|
||||
lastMatchTerm = match[1].toLowerCase();
|
||||
} else if (match.index && match.index > 0 && value[match.index - 1] === '~') {
|
||||
lastMatchTerm = null;
|
||||
} else {
|
||||
lastMatchTerm = match[2].toLowerCase();
|
||||
}
|
||||
} else {
|
||||
lastMatchTerm = null;
|
||||
}
|
||||
}
|
||||
return lastMatchTerm;
|
||||
};
|
||||
})();
|
||||
|
||||
const reduceChannelsForSearch = (channels: Array<Channel | ChannelModel>, members: MyChannelModel[]) => {
|
||||
const memberIds = new Set(members.map((m) => m.id));
|
||||
return channels.reduce<Array<Array<Channel | ChannelModel>>>(([pubC, priC, dms], c) => {
|
||||
@@ -54,7 +84,7 @@ const reduceChannelsForAutocomplete = (channels: Array<Channel | ChannelModel>,
|
||||
}, [[], []]);
|
||||
};
|
||||
|
||||
const makeSections = (channels: Array<Channel | ChannelModel>, myMembers: MyChannelModel[], loading: boolean, isSearch = false) => {
|
||||
const makeSections = (channels: Array<Channel | ChannelModel>, myMembers: MyChannelModel[], isSearch = false) => {
|
||||
const newSections = [];
|
||||
if (isSearch) {
|
||||
const [publicChannels, privateChannels, directAndGroupMessages] = reduceChannelsForSearch(channels, myMembers);
|
||||
@@ -99,7 +129,7 @@ const makeSections = (channels: Array<Channel | ChannelModel>, myMembers: MyChan
|
||||
});
|
||||
}
|
||||
|
||||
if (otherChannels.length || (!myChannels.length && loading)) {
|
||||
if (otherChannels.length) {
|
||||
newSections.push({
|
||||
id: t('suggestion.mention.morechannels'),
|
||||
defaultMessage: 'Other Channels',
|
||||
@@ -118,79 +148,99 @@ const makeSections = (channels: Array<Channel | ChannelModel>, myMembers: MyChan
|
||||
return newSections;
|
||||
};
|
||||
|
||||
const filterResults = (channels: Array<Channel | ChannelModel>, term: string) => {
|
||||
return channels.filter((c) => {
|
||||
const displayName = ('displayName' in c ? c.displayName : c.display_name).toLowerCase();
|
||||
return c.name.toLowerCase().includes(term) ||
|
||||
displayName.includes(term);
|
||||
});
|
||||
const filterLocalResults = (channels: ChannelModel[], term: string) => {
|
||||
return channels.filter((c) =>
|
||||
c.name.toLowerCase().startsWith(term) ||
|
||||
c.displayName.toLowerCase().startsWith(term),
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
cursorPosition: number;
|
||||
isSearch: boolean;
|
||||
maxListHeight: number;
|
||||
myMembers: MyChannelModel[];
|
||||
updateValue: (v: string) => void;
|
||||
onShowingChange: (c: boolean) => void;
|
||||
value: string;
|
||||
nestedScrollEnabled: boolean;
|
||||
listStyle: StyleProp<ViewStyle>;
|
||||
matchTerm: string;
|
||||
localChannels: ChannelModel[];
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
listView: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderRadius: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const getAllChannels = async (serverUrl: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const teamId = await getCurrentTeamId(database);
|
||||
return queryAllChannelsForTeam(database, teamId).fetch();
|
||||
};
|
||||
|
||||
const emptySections: Array<SectionListData<Channel>> = [];
|
||||
const emptyChannels: Array<Channel | ChannelModel> = [];
|
||||
const emptyChannels: Channel[] = [];
|
||||
const emptyModelList: ChannelModel[] = [];
|
||||
|
||||
const ChannelMention = ({
|
||||
cursorPosition,
|
||||
isSearch,
|
||||
maxListHeight,
|
||||
myMembers,
|
||||
updateValue,
|
||||
onShowingChange,
|
||||
value,
|
||||
nestedScrollEnabled,
|
||||
listStyle,
|
||||
matchTerm,
|
||||
localChannels,
|
||||
teamId,
|
||||
}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const [sections, setSections] = useState<Array<SectionListData<(Channel | ChannelModel)>>>(emptySections);
|
||||
const [remoteChannels, setRemoteChannels] = useState<Array<ChannelModel | Channel>>(emptyChannels);
|
||||
const [channels, setChannels] = useState<Channel[]>(emptyChannels);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [noResultsTerm, setNoResultsTerm] = useState<string|null>(null);
|
||||
const [localCursorPosition, setLocalCursorPosition] = useState(cursorPosition); // To avoid errors due to delay between value changes and cursor position changes.
|
||||
const [useLocal, setUseLocal] = useState(true);
|
||||
const [localChannels, setlocalChannels] = useState<ChannelModel[]>();
|
||||
const [filteredLocalChannels, setFilteredLocalChannels] = useState(emptyModelList);
|
||||
|
||||
const latestSearchAt = useRef(0);
|
||||
const listStyle = useMemo(() =>
|
||||
[style.listView, {maxHeight: maxListHeight}]
|
||||
, [style, maxListHeight]);
|
||||
|
||||
const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, tId: string) => {
|
||||
const searchAt = Date.now();
|
||||
latestSearchAt.current = searchAt;
|
||||
const runSearch = useMemo(() => debounce(async (sUrl: string, term: string) => {
|
||||
setLoading(true);
|
||||
const {channels: receivedChannels, error} = await searchChannels(sUrl, term, isSearch);
|
||||
setUseLocal(Boolean(error));
|
||||
|
||||
const {channels: receivedChannels} = await searchChannels(sUrl, term, tId, isSearch);
|
||||
|
||||
if (latestSearchAt.current > searchAt) {
|
||||
return;
|
||||
if (error) {
|
||||
let fallbackChannels = localChannels;
|
||||
if (!fallbackChannels) {
|
||||
fallbackChannels = await getAllChannels(sUrl);
|
||||
setlocalChannels(fallbackChannels);
|
||||
}
|
||||
const filteredChannels = filterLocalResults(fallbackChannels, term);
|
||||
setFilteredLocalChannels(filteredChannels.length ? filteredChannels : emptyModelList);
|
||||
} else if (receivedChannels) {
|
||||
setChannels(receivedChannels.length ? receivedChannels : emptyChannels);
|
||||
}
|
||||
let channelsToStore: Array<Channel | ChannelModel> = receivedChannels || [];
|
||||
if (hasTrailingSpaces(term)) {
|
||||
channelsToStore = filterResults(receivedChannels || [], term);
|
||||
}
|
||||
setRemoteChannels(channelsToStore.length ? channelsToStore : emptyChannels);
|
||||
|
||||
setLoading(false);
|
||||
}, 200), []);
|
||||
|
||||
const matchTerm = getMatchTermForChannelMention(value.substring(0, localCursorPosition), isSearch);
|
||||
const resetState = () => {
|
||||
latestSearchAt.current = Date.now();
|
||||
setRemoteChannels(emptyChannels);
|
||||
setFilteredLocalChannels(emptyModelList);
|
||||
setChannels(emptyChannels);
|
||||
setSections(emptySections);
|
||||
setNoResultsTerm(null);
|
||||
runSearch.cancel();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const completeMention = useCallback((mention: string) => {
|
||||
@@ -226,11 +276,8 @@ const ChannelMention = ({
|
||||
}
|
||||
|
||||
onShowingChange(false);
|
||||
setLoading(false);
|
||||
setNoResultsTerm(mention);
|
||||
setSections(emptySections);
|
||||
setRemoteChannels(emptyChannels);
|
||||
latestSearchAt.current = Date.now();
|
||||
}, [value, localCursorPosition, isSearch]);
|
||||
|
||||
const renderItem = useCallback(({item}: SectionListRenderItemInfo<Channel | ChannelModel>) => {
|
||||
@@ -271,41 +318,21 @@ const ChannelMention = ({
|
||||
}
|
||||
|
||||
setNoResultsTerm(null);
|
||||
setLoading(true);
|
||||
runSearch(serverUrl, matchTerm, teamId);
|
||||
}, [matchTerm, teamId]);
|
||||
|
||||
const channels = useMemo(() => {
|
||||
const ids = new Set(localChannels.map((c) => c.id));
|
||||
return [...localChannels, ...remoteChannels.filter((c) => !ids.has(c.id))].sort((a, b) => {
|
||||
const aDisplay = 'display_name' in a ? a.display_name : a.displayName;
|
||||
const bDisplay = 'display_name' in b ? b.display_name : b.displayName;
|
||||
const displayResult = aDisplay.localeCompare(bDisplay);
|
||||
if (displayResult === 0) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
return displayResult;
|
||||
});
|
||||
}, [localChannels, remoteChannels]);
|
||||
runSearch(serverUrl, matchTerm);
|
||||
}, [matchTerm]);
|
||||
|
||||
useDidUpdate(() => {
|
||||
if (noResultsTerm && !loading) {
|
||||
return;
|
||||
}
|
||||
const newSections = makeSections(channels, myMembers, loading, isSearch);
|
||||
const newSections = makeSections(useLocal ? filteredLocalChannels : channels, myMembers, isSearch);
|
||||
const nSections = newSections.length;
|
||||
|
||||
if (!loading && !nSections && noResultsTerm == null) {
|
||||
setNoResultsTerm(matchTerm);
|
||||
}
|
||||
if (nSections) {
|
||||
setNoResultsTerm(null);
|
||||
}
|
||||
setSections(newSections.length ? newSections : emptySections);
|
||||
onShowingChange(Boolean(nSections));
|
||||
}, [channels, myMembers, loading]);
|
||||
|
||||
if (!loading && (sections.length === 0 || noResultsTerm != null)) {
|
||||
if (sections.length === 0 || noResultsTerm != null) {
|
||||
// If we are not in an active state or the mention has been completed return null so nothing is rendered
|
||||
// other components are not blocked.
|
||||
return null;
|
||||
|
||||
@@ -3,90 +3,17 @@
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
|
||||
import {observeChannel, queryAllMyChannel, queryChannelsForAutocomplete} from '@queries/servers/channel';
|
||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||
import {queryAllMyChannel} from '@queries/servers/channel';
|
||||
|
||||
import ChannelMention from './channel_mention';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
const getMatchTermForChannelMention = (() => {
|
||||
let lastMatchTerm: string | null = null;
|
||||
let lastValue: string;
|
||||
let lastIsSearch: boolean;
|
||||
return (value: string, isSearch: boolean) => {
|
||||
if (value !== lastValue || isSearch !== lastIsSearch) {
|
||||
const regex = isSearch ? CHANNEL_MENTION_SEARCH_REGEX : CHANNEL_MENTION_REGEX;
|
||||
const match = value.match(regex);
|
||||
lastValue = value;
|
||||
lastIsSearch = isSearch;
|
||||
if (match) {
|
||||
if (isSearch) {
|
||||
lastMatchTerm = match[1].toLowerCase();
|
||||
} else if (match.index && match.index > 0 && value[match.index - 1] === '~') {
|
||||
lastMatchTerm = null;
|
||||
} else {
|
||||
lastMatchTerm = match[2].toLowerCase();
|
||||
}
|
||||
} else {
|
||||
lastMatchTerm = null;
|
||||
}
|
||||
}
|
||||
return lastMatchTerm;
|
||||
};
|
||||
})();
|
||||
|
||||
type WithTeamIdProps = {
|
||||
teamId?: string;
|
||||
channelId?: string;
|
||||
} & WithDatabaseArgs;
|
||||
|
||||
type OwnProps = {
|
||||
value: string;
|
||||
isSearch: boolean;
|
||||
cursorPosition: number;
|
||||
teamId: string;
|
||||
} & WithDatabaseArgs;
|
||||
|
||||
const emptyChannelList: ChannelModel[] = [];
|
||||
|
||||
const withMembers = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
return {
|
||||
myMembers: queryAllMyChannel(database).observe(),
|
||||
};
|
||||
});
|
||||
|
||||
const withTeamId = withObservables(['teamId', 'channelId'], ({teamId, channelId, database}: WithTeamIdProps) => {
|
||||
let currentTeamId;
|
||||
if (teamId) {
|
||||
currentTeamId = of$(teamId);
|
||||
} else if (channelId) {
|
||||
currentTeamId = observeChannel(database, channelId).pipe(switchMap((c) => {
|
||||
return c?.teamId ? of$(c.teamId) : observeCurrentTeamId(database);
|
||||
}));
|
||||
} else {
|
||||
currentTeamId = observeCurrentTeamId(database);
|
||||
}
|
||||
|
||||
return {
|
||||
teamId: currentTeamId,
|
||||
};
|
||||
});
|
||||
|
||||
const enhanced = withObservables(['value', 'isSearch', 'teamId', 'cursorPosition'], ({value, isSearch, teamId, cursorPosition, database}: OwnProps) => {
|
||||
const matchTerm = getMatchTermForChannelMention(value.substring(0, cursorPosition), isSearch);
|
||||
|
||||
const localChannels = matchTerm === null ? of$(emptyChannelList) : queryChannelsForAutocomplete(database, matchTerm, isSearch, teamId).observe();
|
||||
|
||||
return {
|
||||
matchTerm: of$(matchTerm),
|
||||
localChannels,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(withMembers(withTeamId(enhanced(ChannelMention))));
|
||||
export default withDatabase(enhanced(ChannelMention));
|
||||
|
||||
@@ -27,7 +27,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
alignItems: 'center',
|
||||
},
|
||||
rowDisplayName: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
@@ -130,17 +129,16 @@ const ChannelMentionItem = ({
|
||||
style={style.icon}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={style.rowDisplayName}
|
||||
testID={`${channelMentionItemTestId}.display_name`}
|
||||
>
|
||||
{displayName}
|
||||
<Text
|
||||
style={style.rowName}
|
||||
testID={`${channelMentionItemTestId}.name`}
|
||||
>
|
||||
{` ~${channel.name}`}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text
|
||||
style={style.rowName}
|
||||
testID={`${channelMentionItemTestId}.name`}
|
||||
>
|
||||
{` ~${channel.name}`}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Fuse from 'fuse.js';
|
||||
import {debounce} from 'lodash';
|
||||
import React, {useCallback, useEffect, useMemo} from 'react';
|
||||
import {FlatList, Platform, StyleProp, Text, View, ViewStyle} from 'react-native';
|
||||
import {FlatList, Platform, Text, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {searchCustomEmojis} from '@actions/remote/custom_emoji';
|
||||
@@ -48,6 +48,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
listView: {
|
||||
paddingTop: 16,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderRadius: 8,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
@@ -65,6 +67,7 @@ const keyExtractor = (item: string) => item;
|
||||
type Props = {
|
||||
cursorPosition: number;
|
||||
customEmojis: CustomEmojiModel[];
|
||||
maxListHeight: number;
|
||||
updateValue: (v: string) => void;
|
||||
onShowingChange: (c: boolean) => void;
|
||||
rootId?: string;
|
||||
@@ -72,12 +75,12 @@ type Props = {
|
||||
nestedScrollEnabled: boolean;
|
||||
skinTone: string;
|
||||
hasFilesAttached?: boolean;
|
||||
inPost: boolean;
|
||||
listStyle: StyleProp<ViewStyle>;
|
||||
inPost?: boolean;
|
||||
}
|
||||
const EmojiSuggestion = ({
|
||||
cursorPosition,
|
||||
customEmojis = [],
|
||||
maxListHeight,
|
||||
updateValue,
|
||||
onShowingChange,
|
||||
rootId,
|
||||
@@ -85,14 +88,15 @@ const EmojiSuggestion = ({
|
||||
nestedScrollEnabled,
|
||||
skinTone,
|
||||
hasFilesAttached = false,
|
||||
inPost,
|
||||
listStyle,
|
||||
inPost = true,
|
||||
}: Props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const flatListStyle = useMemo(() =>
|
||||
[style.listView, {maxHeight: maxListHeight}]
|
||||
, [style, maxListHeight]);
|
||||
const containerStyle = useMemo(() =>
|
||||
({paddingBottom: insets.bottom + 12})
|
||||
, [insets.bottom]);
|
||||
@@ -215,7 +219,7 @@ const EmojiSuggestion = ({
|
||||
return (
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps='always'
|
||||
style={[style.listView, listStyle]}
|
||||
style={flatListStyle}
|
||||
data={data}
|
||||
keyExtractor={keyExtractor}
|
||||
removeClippedSubviews={true}
|
||||
|
||||
@@ -1899,7 +1899,7 @@ export class AppCommandParser {
|
||||
if (input[0] === '@') {
|
||||
input = input.substring(1);
|
||||
}
|
||||
const res = await searchUsers(this.serverUrl, input, this.teamID, this.channelID);
|
||||
const res = await searchUsers(this.serverUrl, input, this.channelID);
|
||||
return getUserSuggestions(res.users);
|
||||
};
|
||||
|
||||
@@ -1908,7 +1908,7 @@ export class AppCommandParser {
|
||||
if (input[0] === '~') {
|
||||
input = input.substring(1);
|
||||
}
|
||||
const res = await searchChannels(this.serverUrl, input, this.teamID);
|
||||
const res = await searchChannels(this.serverUrl, input);
|
||||
return getChannelSuggestions(res.channels);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function inTextMentionSuggestions(serverUrl: string, pretext: strin
|
||||
const incompleteLessLastWord = separatedWords.slice(0, -1).join(' ');
|
||||
const lastWord = separatedWords[separatedWords.length - 1];
|
||||
if (lastWord.startsWith('@')) {
|
||||
const res = await searchUsers(serverUrl, lastWord.substring(1), teamID, channelID);
|
||||
const res = await searchUsers(serverUrl, lastWord.substring(1), channelID);
|
||||
const users = await getUserSuggestions(res.users);
|
||||
users.forEach((u) => {
|
||||
let complete = incompleteLessLastWord ? incompleteLessLastWord + ' ' + u.Complete : u.Complete;
|
||||
@@ -23,7 +23,7 @@ export async function inTextMentionSuggestions(serverUrl: string, pretext: strin
|
||||
}
|
||||
|
||||
if (lastWord.startsWith('~') && !lastWord.startsWith('~~')) {
|
||||
const res = await searchChannels(serverUrl, lastWord.substring(1), teamID);
|
||||
const res = await searchChannels(serverUrl, lastWord.substring(1));
|
||||
const channels = await getChannelSuggestions(res.channels);
|
||||
channels.forEach((c) => {
|
||||
let complete = incompleteLessLastWord ? incompleteLessLastWord + ' ' + c.Complete : c.Complete;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {debounce} from 'lodash';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {FlatList, Platform, StyleProp, ViewStyle} from 'react-native';
|
||||
import {FlatList, Platform} from 'react-native';
|
||||
|
||||
import AtMentionItem from '@components/autocomplete/at_mention_item';
|
||||
import ChannelMentionItem from '@components/autocomplete/channel_mention_item';
|
||||
@@ -12,6 +12,7 @@ import {COMMAND_SUGGESTION_CHANNEL, COMMAND_SUGGESTION_USER} from '@constants/ap
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import analytics from '@managers/analytics';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {AppCommandParser, ExtendedAutocompleteSuggestion} from '../app_command_parser/app_command_parser';
|
||||
import SlashSuggestionItem from '../slash_suggestion_item';
|
||||
@@ -22,6 +23,7 @@ import type UserModel from '@typings/database/models/servers/user';
|
||||
export type Props = {
|
||||
currentTeamId: string;
|
||||
isSearch?: boolean;
|
||||
maxListHeight?: number;
|
||||
updateValue: (text: string) => void;
|
||||
onShowingChange: (c: boolean) => void;
|
||||
value: string;
|
||||
@@ -29,11 +31,21 @@ export type Props = {
|
||||
rootId?: string;
|
||||
channelId: string;
|
||||
isAppsEnabled: boolean;
|
||||
listStyle: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
const keyExtractor = (item: ExtendedAutocompleteSuggestion): string => item.Suggestion + item.type + item.item;
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
paddingTop: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const emptySuggestonList: AutocompleteSuggestion[] = [];
|
||||
|
||||
const AppSlashSuggestion = ({
|
||||
@@ -42,10 +54,10 @@ const AppSlashSuggestion = ({
|
||||
rootId,
|
||||
value = '',
|
||||
isAppsEnabled,
|
||||
maxListHeight,
|
||||
nestedScrollEnabled,
|
||||
updateValue,
|
||||
onShowingChange,
|
||||
listStyle,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
@@ -53,8 +65,11 @@ const AppSlashSuggestion = ({
|
||||
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme));
|
||||
const [dataSource, setDataSource] = useState<AutocompleteSuggestion[]>(emptySuggestonList);
|
||||
const active = isAppsEnabled && Boolean(dataSource.length);
|
||||
const style = getStyleFromTheme(theme);
|
||||
const mounted = useRef(false);
|
||||
|
||||
const listStyle = useMemo(() => [style.listView, {maxHeight: maxListHeight}], [maxListHeight, style]);
|
||||
|
||||
const fetchAndShowAppCommandSuggestions = useMemo(() => debounce(async (pretext: string, cId: string, tId = '', rId?: string) => {
|
||||
appCommandParser.current.setChannelContext(cId, tId, rId);
|
||||
const suggestions = await appCommandParser.current.getSuggestions(pretext);
|
||||
|
||||
@@ -7,8 +7,6 @@ import {useIntl} from 'react-intl';
|
||||
import {
|
||||
FlatList,
|
||||
Platform,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
|
||||
import {fetchSuggestions} from '@actions/remote/command';
|
||||
@@ -16,6 +14,7 @@ import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import analytics from '@managers/analytics';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {AppCommandParser} from './app_command_parser/app_command_parser';
|
||||
import SlashSuggestionItem from './slash_suggestion_item';
|
||||
@@ -28,6 +27,17 @@ const COMMANDS_TO_HIDE_ON_MOBILE = new Set([...COMMANDS_TO_IMPLEMENT_LATER, ...N
|
||||
|
||||
const commandFilter = (v: Command) => !COMMANDS_TO_HIDE_ON_MOBILE.has(v.trigger);
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
paddingTop: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const filterCommands = (matchTerm: string, commands: Command[]): AutocompleteSuggestion[] => {
|
||||
const data = commands.filter((command) => {
|
||||
if (!command.auto_complete) {
|
||||
@@ -53,6 +63,7 @@ const keyExtractor = (item: Command & AutocompleteSuggestion): string => item.id
|
||||
|
||||
type Props = {
|
||||
currentTeamId: string;
|
||||
maxListHeight?: number;
|
||||
updateValue: (text: string) => void;
|
||||
onShowingChange: (c: boolean) => void;
|
||||
value: string;
|
||||
@@ -60,7 +71,6 @@ type Props = {
|
||||
rootId?: string;
|
||||
channelId: string;
|
||||
isAppsEnabled: boolean;
|
||||
listStyle: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
const emptyCommandList: Command[] = [];
|
||||
@@ -72,13 +82,14 @@ const SlashSuggestion = ({
|
||||
rootId,
|
||||
onShowingChange,
|
||||
isAppsEnabled,
|
||||
maxListHeight,
|
||||
nestedScrollEnabled,
|
||||
updateValue,
|
||||
value = '',
|
||||
listStyle,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme));
|
||||
const mounted = useRef(false);
|
||||
@@ -89,6 +100,8 @@ const SlashSuggestion = ({
|
||||
|
||||
const active = Boolean(dataSource.length);
|
||||
|
||||
const listStyle = useMemo(() => [style.listView, {maxHeight: maxListHeight}], [maxListHeight, style]);
|
||||
|
||||
const updateSuggestions = useCallback((matches: AutocompleteSuggestion[]) => {
|
||||
setDataSource(matches);
|
||||
onShowingChange(Boolean(matches.length));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"minHeight": 40,
|
||||
@@ -29,7 +29,7 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
},
|
||||
false,
|
||||
undefined,
|
||||
{
|
||||
Object {
|
||||
"minHeight": 40,
|
||||
},
|
||||
]
|
||||
@@ -38,7 +38,7 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
@@ -46,12 +46,12 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"height": 24,
|
||||
"width": 24,
|
||||
},
|
||||
@@ -63,13 +63,13 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
<Icon
|
||||
name="globe"
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.4)",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
Object {
|
||||
"fontSize": 24,
|
||||
"left": 1,
|
||||
},
|
||||
@@ -83,14 +83,14 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"marginTop": -1,
|
||||
"paddingLeft": 12,
|
||||
@@ -126,15 +126,15 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"minHeight": 40,
|
||||
@@ -142,7 +142,7 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
},
|
||||
false,
|
||||
undefined,
|
||||
{
|
||||
Object {
|
||||
"minHeight": 40,
|
||||
},
|
||||
]
|
||||
@@ -151,7 +151,7 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
@@ -159,12 +159,12 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"height": 24,
|
||||
"width": 24,
|
||||
},
|
||||
@@ -176,13 +176,13 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
<Icon
|
||||
name="pencil-outline"
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.4)",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
Object {
|
||||
"fontSize": 24,
|
||||
"left": 2,
|
||||
},
|
||||
@@ -196,14 +196,14 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"marginTop": -1,
|
||||
"paddingLeft": 12,
|
||||
@@ -226,14 +226,14 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
name="phone-in-talk"
|
||||
size={16}
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"marginTop": -1,
|
||||
"paddingLeft": 12,
|
||||
@@ -244,8 +244,9 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
{
|
||||
"paddingRight": 0,
|
||||
Object {
|
||||
"flex": 1,
|
||||
"marginRight": 20,
|
||||
"textAlign": "right",
|
||||
},
|
||||
]
|
||||
@@ -268,15 +269,15 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"minHeight": 40,
|
||||
@@ -284,7 +285,7 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
},
|
||||
false,
|
||||
undefined,
|
||||
{
|
||||
Object {
|
||||
"minHeight": 40,
|
||||
},
|
||||
]
|
||||
@@ -293,7 +294,7 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
@@ -301,12 +302,12 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"height": 24,
|
||||
"width": 24,
|
||||
},
|
||||
@@ -318,13 +319,13 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
<Icon
|
||||
name="pencil-outline"
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.4)",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
Object {
|
||||
"fontSize": 24,
|
||||
"left": 2,
|
||||
},
|
||||
@@ -338,14 +339,14 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"marginTop": -1,
|
||||
"paddingLeft": 12,
|
||||
|
||||
@@ -116,8 +116,9 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
top: 5,
|
||||
},
|
||||
hasCall: {
|
||||
flex: 1,
|
||||
textAlign: 'right',
|
||||
paddingRight: 0,
|
||||
marginRight: 20,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -180,14 +181,12 @@ const ChannelListItem = ({
|
||||
displayName = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName});
|
||||
}
|
||||
|
||||
const channelItemTestId = `${testID}.${channel.name}`;
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={handleOnPress}>
|
||||
<>
|
||||
<View
|
||||
style={containerStyle}
|
||||
testID={channelItemTestId}
|
||||
testID={`${testID}.${channel.name}`}
|
||||
>
|
||||
<View style={styles.wrapper}>
|
||||
<ChannelIcon
|
||||
@@ -208,7 +207,7 @@ const ChannelListItem = ({
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={textStyles}
|
||||
testID={`${channelItemTestId}.display_name`}
|
||||
testID={`${testID}.${channel.name}.display_name`}
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
@@ -217,7 +216,7 @@ const ChannelListItem = ({
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={[styles.teamName, isMuted && styles.teamNameMuted]}
|
||||
testID={`${channelItemTestId}.team_display_name`}
|
||||
testID={`${testID}.${channel.name}.team_display_name`}
|
||||
>
|
||||
{teamDisplayName}
|
||||
</Text>
|
||||
@@ -226,7 +225,6 @@ const ChannelListItem = ({
|
||||
{Boolean(teammateId) &&
|
||||
<CustomStatus
|
||||
isInfo={isInfo}
|
||||
testID={channelItemTestId}
|
||||
userId={teammateId!}
|
||||
/>
|
||||
}
|
||||
@@ -235,7 +233,7 @@ const ChannelListItem = ({
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={[styles.teamName, styles.teamNameTablet, isMuted && styles.teamNameMuted]}
|
||||
testID={`${channelItemTestId}.team_display_name`}
|
||||
testID={`${testID}.${channel.name}.team_display_name`}
|
||||
>
|
||||
{teamDisplayName}
|
||||
</Text>
|
||||
|
||||
@@ -11,7 +11,6 @@ type Props = {
|
||||
customStatusExpired: boolean;
|
||||
isCustomStatusEnabled: boolean;
|
||||
isInfo?: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
@@ -25,7 +24,7 @@ const style = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled, isInfo, testID}: Props) => {
|
||||
const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled, isInfo}: Props) => {
|
||||
const showCustomStatusEmoji = Boolean(isCustomStatusEnabled && customStatus?.emoji && !customStatusExpired);
|
||||
|
||||
if (!showCustomStatusEmoji) {
|
||||
@@ -36,7 +35,7 @@ const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled,
|
||||
<CustomStatusEmoji
|
||||
customStatus={customStatus!}
|
||||
style={[style.customStatusEmoji, isInfo && style.info]}
|
||||
testID={testID}
|
||||
testID={`channel_item.custom_status.${customStatus!.emoji}-${customStatus!.text}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
import {BaseOption} from '@components/common_post_options';
|
||||
|
||||
@@ -13,7 +13,7 @@ exports[`components/custom_status/clear_button should match snapshot 1`] = `
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"height": 40,
|
||||
@@ -28,7 +28,7 @@ exports[`components/custom_status/clear_button should match snapshot 1`] = `
|
||||
name="close-circle"
|
||||
size={20}
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"borderRadius": 1000,
|
||||
"color": "rgba(63,67,80,0.52)",
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
exports[`components/custom_status/custom_status_emoji should match snapshot 1`] = `
|
||||
<View
|
||||
testID="test.custom_status.custom_status_emoji.calendar"
|
||||
testID="custom_status_emoji.calendar"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
[
|
||||
Array [
|
||||
undefined,
|
||||
{
|
||||
Object {
|
||||
"color": "#000",
|
||||
"fontSize": 16,
|
||||
},
|
||||
@@ -22,13 +22,13 @@ exports[`components/custom_status/custom_status_emoji should match snapshot 1`]
|
||||
|
||||
exports[`components/custom_status/custom_status_emoji should match snapshot with props 1`] = `
|
||||
<View
|
||||
testID="test.custom_status.custom_status_emoji.calendar"
|
||||
testID="custom_status_emoji.calendar"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
[
|
||||
Array [
|
||||
undefined,
|
||||
{
|
||||
Object {
|
||||
"color": "#000",
|
||||
"fontSize": 34,
|
||||
},
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
exports[`components/custom_status/custom_status_text should match snapshot 1`] = `
|
||||
<Text
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.5)",
|
||||
"fontSize": 17,
|
||||
"includeFontPadding": false,
|
||||
@@ -21,8 +21,8 @@ exports[`components/custom_status/custom_status_text should match snapshot 1`] =
|
||||
exports[`components/custom_status/custom_status_text should match snapshot with empty text 1`] = `
|
||||
<Text
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.5)",
|
||||
"fontSize": 17,
|
||||
"includeFontPadding": false,
|
||||
|
||||
@@ -23,10 +23,7 @@ describe('components/custom_status/custom_status_emoji', () => {
|
||||
};
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = renderWithEverything(
|
||||
<CustomStatusEmoji
|
||||
customStatus={customStatus}
|
||||
testID='test'
|
||||
/>,
|
||||
<CustomStatusEmoji customStatus={customStatus}/>,
|
||||
{database},
|
||||
);
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
@@ -37,7 +34,6 @@ describe('components/custom_status/custom_status_emoji', () => {
|
||||
<CustomStatusEmoji
|
||||
customStatus={customStatus}
|
||||
emojiSize={34}
|
||||
testID='test'
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
@@ -14,15 +14,16 @@ interface ComponentProps {
|
||||
}
|
||||
|
||||
const CustomStatusEmoji = ({customStatus, emojiSize = 16, style, testID}: ComponentProps) => {
|
||||
const testIdPrefix = testID ? `${testID}.` : '';
|
||||
if (customStatus.emoji) {
|
||||
return (
|
||||
<View
|
||||
style={style}
|
||||
testID={`${testID}.custom_status.custom_status_emoji.${customStatus.emoji}`}
|
||||
testID={`${testIdPrefix}custom_status_emoji.${customStatus.emoji}`}
|
||||
>
|
||||
<Emoji
|
||||
size={emojiSize}
|
||||
emojiName={customStatus.emoji}
|
||||
emojiName={customStatus.emoji!}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@ interface ComponentProps {
|
||||
textStyle?: TextStyle;
|
||||
ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';
|
||||
numberOfLines?: number;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
@@ -26,12 +25,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
const CustomStatusText = ({text, theme, textStyle, ellipsizeMode, numberOfLines, testID}: ComponentProps) => (
|
||||
const CustomStatusText = ({text, theme, textStyle, ellipsizeMode, numberOfLines}: ComponentProps) => (
|
||||
<Text
|
||||
style={[getStyleSheet(theme).label, textStyle]}
|
||||
ellipsizeMode={ellipsizeMode}
|
||||
numberOfLines={numberOfLines}
|
||||
testID={testID}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
exports[`ErrorText should match snapshot 1`] = `
|
||||
<Text
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"color": "#d24b4e",
|
||||
"fontSize": 12,
|
||||
"marginBottom": 15,
|
||||
"marginTop": 15,
|
||||
"textAlign": "left",
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"marginHorizontal": 15,
|
||||
},
|
||||
|
||||
@@ -30,8 +30,7 @@ type FileProps = {
|
||||
onPress: (index: number) => void;
|
||||
publicLinkEnabled: boolean;
|
||||
channelName?: string;
|
||||
onOptionsPress?: (fileInfo: FileInfo) => void;
|
||||
optionSelected?: boolean;
|
||||
onOptionsPress?: (index: number) => void;
|
||||
wrapperWidth?: number;
|
||||
showDate?: boolean;
|
||||
updateFileForGallery: (idx: number, file: FileInfo) => void;
|
||||
@@ -75,7 +74,6 @@ const File = ({
|
||||
nonVisibleImagesCount = 0,
|
||||
onOptionsPress,
|
||||
onPress,
|
||||
optionSelected,
|
||||
publicLinkEnabled,
|
||||
showDate = false,
|
||||
updateFileForGallery,
|
||||
@@ -96,15 +94,19 @@ const File = ({
|
||||
const {styles, onGestureEvent, ref} = useGalleryItem(galleryIdentifier, index, handlePreviewPress);
|
||||
|
||||
const handleOnOptionsPress = useCallback(() => {
|
||||
onOptionsPress?.(file);
|
||||
}, [file, onOptionsPress]);
|
||||
onOptionsPress?.(index);
|
||||
}, [index, onOptionsPress]);
|
||||
|
||||
const optionsButton = (
|
||||
<FileOptionsIcon
|
||||
onPress={handleOnOptionsPress}
|
||||
selected={optionSelected}
|
||||
/>
|
||||
);
|
||||
const renderOptionsButton = () => {
|
||||
if (onOptionsPress) {
|
||||
return (
|
||||
<FileOptionsIcon
|
||||
onPress={handleOnOptionsPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const fileInfo = (
|
||||
<FileInfo
|
||||
@@ -172,7 +174,7 @@ const File = ({
|
||||
{fileIcon}
|
||||
</View>
|
||||
{fileInfo}
|
||||
{onOptionsPress && optionsButton}
|
||||
{renderOptionsButton()}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -187,7 +189,7 @@ const File = ({
|
||||
<View style={[style.fileWrapper]}>
|
||||
{renderDocumentFile}
|
||||
{fileInfo}
|
||||
{onOptionsPress && optionsButton}
|
||||
{renderOptionsButton()}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -2,38 +2,32 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
import {TouchableOpacity, StyleSheet} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
onPress: () => void;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
threeDotContainer: {
|
||||
alignItems: 'flex-end',
|
||||
borderRadius: 4,
|
||||
marginHorizontal: 20,
|
||||
padding: 7,
|
||||
},
|
||||
selected: {
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
|
||||
},
|
||||
};
|
||||
const styles = StyleSheet.create({
|
||||
threeDotContainer: {
|
||||
alignItems: 'flex-end',
|
||||
marginHorizontal: 20,
|
||||
},
|
||||
});
|
||||
|
||||
export default function FileOptionsIcon({onPress, selected = false}: Props) {
|
||||
const hitSlop = {top: 5, bottom: 5, left: 5, right: 5};
|
||||
|
||||
export default function FileOptionsIcon({onPress}: Props) {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
style={[styles.threeDotContainer, selected ? styles.selected : null]}
|
||||
style={styles.threeDotContainer}
|
||||
hitSlop={hitSlop}
|
||||
>
|
||||
<CompassIcon
|
||||
name='dots-horizontal'
|
||||
|
||||
@@ -66,6 +66,7 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l
|
||||
|
||||
const updateFileForGallery = (idx: number, file: FileInfo) => {
|
||||
'worklet';
|
||||
|
||||
filesForGallery.value[idx] = file;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import Svg, {
|
||||
Mask,
|
||||
G,
|
||||
} from 'react-native-svg';
|
||||
import {EMaskUnits} from 'react-native-svg/src/elements/Mask';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
@@ -59,7 +60,7 @@ function SvgComponent({theme}: Props) {
|
||||
/>
|
||||
<Mask
|
||||
id='a'
|
||||
maskUnits='userSpaceOnUse'
|
||||
maskUnits={EMaskUnits.USER_SPACE_ON_USE}
|
||||
x={76}
|
||||
y={43}
|
||||
width={134}
|
||||
|
||||
@@ -6,6 +6,7 @@ import Svg, {
|
||||
Mask,
|
||||
G,
|
||||
} from 'react-native-svg';
|
||||
import {EMaskUnits} from 'react-native-svg/src/elements/Mask';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
@@ -59,7 +60,7 @@ function SvgComponent({theme}: Props) {
|
||||
/>
|
||||
<Mask
|
||||
id='a'
|
||||
maskUnits='userSpaceOnUse'
|
||||
maskUnits={EMaskUnits.USER_SPACE_ON_USE}
|
||||
x={76}
|
||||
y={43}
|
||||
width={134}
|
||||
|
||||
@@ -11,6 +11,7 @@ import Svg, {
|
||||
Use,
|
||||
Image,
|
||||
} from 'react-native-svg';
|
||||
import {EMaskUnits} from 'react-native-svg/src/elements/Mask';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
@@ -30,7 +31,7 @@ function SvgComponent({theme}: Props) {
|
||||
/>
|
||||
<Mask
|
||||
id='a'
|
||||
maskUnits='userSpaceOnUse'
|
||||
maskUnits={EMaskUnits.USER_SPACE_ON_USE}
|
||||
x={3}
|
||||
y={0}
|
||||
width={117}
|
||||
@@ -52,7 +53,7 @@ function SvgComponent({theme}: Props) {
|
||||
/>
|
||||
<Mask
|
||||
id='b'
|
||||
maskUnits='userSpaceOnUse'
|
||||
maskUnits={EMaskUnits.USER_SPACE_ON_USE}
|
||||
x={32}
|
||||
y={42}
|
||||
width={71}
|
||||
@@ -101,7 +102,7 @@ function SvgComponent({theme}: Props) {
|
||||
/>
|
||||
<Mask
|
||||
id='c'
|
||||
maskUnits='userSpaceOnUse'
|
||||
maskUnits={EMaskUnits.USER_SPACE_ON_USE}
|
||||
x={25}
|
||||
y={3}
|
||||
width={53}
|
||||
@@ -124,7 +125,7 @@ function SvgComponent({theme}: Props) {
|
||||
/>
|
||||
<Mask
|
||||
id='d'
|
||||
maskUnits='userSpaceOnUse'
|
||||
maskUnits={EMaskUnits.USER_SPACE_ON_USE}
|
||||
x={71}
|
||||
y={30}
|
||||
width={51}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`Loading Error should match snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"justifyContent": "center",
|
||||
@@ -13,7 +13,7 @@ exports[`Loading Error should match snapshot 1`] = `
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(255,255,255,0.08)",
|
||||
"borderRadius": 60,
|
||||
@@ -26,7 +26,7 @@ exports[`Loading Error should match snapshot 1`] = `
|
||||
<Icon
|
||||
name="alert-circle-outline"
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.48)",
|
||||
"fontSize": 72,
|
||||
"lineHeight": 72,
|
||||
@@ -36,14 +36,14 @@ exports[`Loading Error should match snapshot 1`] = `
|
||||
</View>
|
||||
<Text
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "Metropolis-SemiBold",
|
||||
"fontSize": 20,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 28,
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"marginTop": 20,
|
||||
"textAlign": "center",
|
||||
@@ -55,14 +55,14 @@ exports[`Loading Error should match snapshot 1`] = `
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"marginTop": 4,
|
||||
"textAlign": "center",
|
||||
@@ -84,7 +84,7 @@ exports[`Loading Error should match snapshot 1`] = `
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderRadius": 4,
|
||||
@@ -100,8 +100,8 @@ exports[`Loading Error should match snapshot 1`] = `
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontWeight": "600",
|
||||
@@ -109,12 +109,12 @@ exports[`Loading Error should match snapshot 1`] = `
|
||||
"padding": 1,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"fontSize": 16,
|
||||
"lineHeight": 18,
|
||||
"marginTop": 1,
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"color": "#1c58d9",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
import React, {useCallback, useEffect, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {GestureResponderEvent, Keyboard, StyleProp, StyleSheet, Text, TextStyle, View} from 'react-native';
|
||||
|
||||
@@ -21,6 +21,7 @@ type ChannelMentionProps = {
|
||||
channelName: string;
|
||||
channels: ChannelModel[];
|
||||
currentTeamId: string;
|
||||
currentUserId: string;
|
||||
linkStyle: StyleProp<TextStyle>;
|
||||
team: TeamModel;
|
||||
textStyle: StyleProp<TextStyle>;
|
||||
@@ -56,7 +57,7 @@ function getChannelFromChannelName(name: string, channels: ChannelModel[], chann
|
||||
}
|
||||
|
||||
const ChannelMention = ({
|
||||
channelMentions, channelName, channels, currentTeamId,
|
||||
channelMentions, channelName, channels, currentTeamId, currentUserId,
|
||||
linkStyle, team, textStyle,
|
||||
}: ChannelMentionProps) => {
|
||||
const intl = useIntl();
|
||||
@@ -67,7 +68,7 @@ const ChannelMention = ({
|
||||
let c = channel;
|
||||
|
||||
if (!c?.id && c?.display_name) {
|
||||
const result = await joinChannel(serverUrl, currentTeamId, undefined, channelName);
|
||||
const result = await joinChannel(serverUrl, currentUserId, currentTeamId, undefined, channelName);
|
||||
if (result.error || !result.channel) {
|
||||
const joinFailedMessage = {
|
||||
id: t('mobile.join_channel.error'),
|
||||
|
||||
@@ -6,7 +6,7 @@ import withObservables from '@nozbe/with-observables';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||
import {observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeTeam} from '@queries/servers/team';
|
||||
|
||||
import ChannelMention from './channel_mention';
|
||||
@@ -17,6 +17,7 @@ export type ChannelMentions = Record<string, {id?: string; display_name: string;
|
||||
|
||||
const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const currentTeamId = observeCurrentTeamId(database);
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
const channels = currentTeamId.pipe(
|
||||
switchMap((id) => queryAllChannelsForTeam(database, id).observeWithColumns(['display_name'])),
|
||||
);
|
||||
@@ -27,6 +28,7 @@ const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
return {
|
||||
channels,
|
||||
currentTeamId,
|
||||
currentUserId,
|
||||
team,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, StyleSheet, Text, TextStyle, TouchableOpacity, View} from 'react-native';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
import React, {useCallback, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Alert, Platform, StyleProp, Text, TextStyle, TouchableWithoutFeedback, View} from 'react-native';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user