Compare commits

..

44 Commits

Author SHA1 Message Date
Jason Frerich
51425f9bb5 remove dir 2022-08-09 13:24:21 -05:00
Jason Frerich
49b2a332e5 Merge branch 'gekidou' into recent-searches-for-navigation 2022-08-09 13:22:40 -05:00
Jason Frerich
14bd424d18 debuggin 2022-07-10 15:39:29 -05:00
Jason Frerich
e13e2a618d remove the backgroundcolors 2022-07-10 14:54:47 -05:00
Jason Frerich
b6fc5abf92 add color to all component backgrounds 2022-07-07 22:10:37 -05:00
Jason Frerich
423b405e4f set the max searches saved to 15 2022-07-07 20:50:53 -05:00
Jason Frerich
5b36199976 when not useing the onScroll callback you don't need to set the
scrollEventThrottle
2022-07-07 19:26:32 -05:00
Jason Frerich
0094fc9a32 call destroyPermanently directly 2022-07-07 19:22:58 -05:00
Jason Frerich
7902483072 There is only one record, so no need to batch. Just call
destroyPermanently.
2022-07-07 18:53:20 -05:00
Jason Frerich
1232cf45d8 remove surrounding parenthesis 2022-07-07 18:30:24 -05:00
Jason Frerich
91525b6eb0 use logError instead of console.log and trowing an error 2022-07-07 18:19:53 -05:00
Jason Frerich
a3bece2776 increase close button to touchable area of 40x40 and adjust menuitem
container
2022-07-06 16:18:04 -05:00
Jason Frerich
2afe6fb9d6 update styling from UX PR requests 2022-07-06 15:33:01 -05:00
Jason Frerich
fe042bf53c update divider opacity to match figma design 2022-07-06 13:45:16 -05:00
Jason Frerich
58370c48cc update compassIcon to match figma design 2022-07-06 13:42:52 -05:00
Jason Frerich
20f262af8d divider takes up 1ox so only need 15px margin to get the 16px total to
the neighboring veritcal views
2022-07-06 13:40:21 -05:00
Jason Frerich
a15e76bf3c styling changes 2022-07-06 13:37:39 -05:00
Jason Frerich
6a03482406 styling adjustments 2022-07-06 13:33:17 -05:00
Jason Frerich
27ef5838b4 - remove useMemo of recent
- set or remove props on AnimatedFlatlist
2022-07-06 13:16:28 -05:00
Jason Frerich
eb01ec1368 remove unused function 2022-07-06 12:59:24 -05:00
Jason Frerich
cca1e160f6 always update the created_at value in the database. 2022-07-06 12:51:39 -05:00
Jason Frerich
f1b05cfcae move styles to the top 2022-07-06 11:47:15 -05:00
Jason Frerich
26904aa9e5 extract as constant 2022-07-06 11:39:53 -05:00
Jason Frerich
678d3ed711 clean up 2022-07-02 14:56:13 -05:00
Jason Frerich
3df715644c clean up 2022-07-02 14:55:25 -05:00
Jason Frerich
13652dfe2d set display to term for now 2022-07-02 14:45:46 -05:00
Jason Frerich
efc6cb8cc8 limit the number of saved searches to 20 for a team.
return the results a team Search History sorted by createdAt
2022-07-02 14:22:03 -05:00
Elias Nahum
c4d2ffe347 Fix android autoscroll search field to the top 2022-07-02 11:05:49 -04:00
Elias Nahum
e6932781b3 fix eslint 2022-07-02 11:01:10 -04:00
Elias Nahum
e134941de8 Fix search to use a flatlist and remove douplicate reference 2022-07-02 10:58:57 -04:00
Jason Frerich
8b7a9b32b1 s/addRecentTeamSearch/addSearchToTeamSearchHistory/ 2022-07-02 09:33:08 -05:00
Jason Frerich
bd87d80baf s/deleteRecentTeamSearchById/removeSearchFromTeamSearchHistory/ 2022-07-02 09:27:27 -05:00
Elias Nahum
3752f9a516 use flatlist instead of scrolview 2022-07-01 18:45:15 -04:00
Jason Frerich
cd16ec6308 push for scrollview 2022-07-01 16:36:49 -05:00
Jason Frerich
7429f4799f will now add new ters to the table and recreate existing terms with new
timestamp
2022-07-01 12:36:09 -05:00
Jason Frerich
501e03647a can delete recent searches from WDB from recent searches Options 2022-07-01 01:23:58 -05:00
Jason Frerich
809a18a87d search terms from the search bar are getting added 2022-06-30 23:01:55 -05:00
Jason Frerich
64a2d21a73 recent search are getting rendered from WDB 2022-06-30 22:45:02 -05:00
Jason Frerich
1eaecfcc8e Merge branch 'gekidou' into MM-44927-recent-searches 2022-06-30 13:59:54 -05:00
Jason Frerich
f79ae62ead initial commit 2022-06-17 09:26:52 -05:00
Jason Frerich
77eb6774c9 UI adjustments from PR feedback 2022-06-16 14:54:47 -05:00
Jason Frerich
a8250ec8af ignore the back press 2022-06-16 10:44:48 -05:00
Jason Frerich
fd3dea386d add search value to memoized dependencies in modifier component 2022-06-16 10:31:55 -05:00
Jason Frerich
f1f1fa5484 initial check in 2022-06-15 12:34:40 -05:00
470 changed files with 12335 additions and 21481 deletions

View File

@@ -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 }}

View File

@@ -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": {

View File

@@ -67,4 +67,4 @@ untyped-import
untyped-type-import
[version]
^0.182.0
^0.176.3

View File

@@ -1,13 +0,0 @@
name: "CodeQL config"
query-filters:
- exclude:
problem.severity:
- warning
- recommendation
- exclude:
id: js/insecure-randomness
paths-ignore:
- test
- '**/*.test.*'

View File

@@ -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
View File

@@ -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
View File

@@ -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/

View File

@@ -1,6 +0,0 @@
image:
file: .gitpod.Dockerfile
tasks:
- init: npm install
- command: npm run android

View File

@@ -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
View 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
View File

@@ -0,0 +1,5 @@
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
}

View File

@@ -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

View File

@@ -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")
}
}

View File

@@ -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>

View File

@@ -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());
}

View File

@@ -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 ->

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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? {

View File

@@ -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

View File

@@ -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));
}
}

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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) {
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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());

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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)

View File

@@ -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)

View File

@@ -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 &params) {
// 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);
}

View File

@@ -9,7 +9,7 @@ namespace facebook {
namespace react {
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
const std::string &moduleName,
const std::string moduleName,
const JavaTurboModule::InitParams &params);
} // namespace react

View File

@@ -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 &params) {
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

View File

@@ -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 &params) 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

View File

@@ -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

View File

@@ -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')

View File

@@ -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

Binary file not shown.

View File

@@ -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

View File

@@ -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]);
}

View File

@@ -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 [];
}
}

View File

@@ -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);
}
}

View File

@@ -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};

View File

@@ -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};

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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)};
};

View File

@@ -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};
}
};

View File

@@ -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}),

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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};

View File

@@ -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 {

View File

@@ -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};

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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:

View File

@@ -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
}

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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,
}
}

View File

@@ -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'
/>

View File

@@ -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,
};
});

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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));

View File

@@ -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>

View File

@@ -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}

View File

@@ -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);
};

View File

@@ -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;

View File

@@ -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);

View File

@@ -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));

View File

@@ -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';

View File

@@ -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';

View File

@@ -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,

View File

@@ -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>

View File

@@ -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}`}
/>
);
};

View File

@@ -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';

View File

@@ -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)",
}

View File

@@ -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,
},

View File

@@ -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,

View File

@@ -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},
);

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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,
},

View File

@@ -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 {

View File

@@ -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'

View File

@@ -66,6 +66,7 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l
const updateFileForGallery = (idx: number, file: FileInfo) => {
'worklet';
filesForGallery.value[idx] = file;
};

View 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}

View 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}

View File

@@ -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}

View File

@@ -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",
},
]

View File

@@ -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';

View File

@@ -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'),

View File

@@ -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,
};
});

View File

@@ -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';

View File

@@ -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