Compare commits

...

31 Commits

Author SHA1 Message Date
Michael Kochell
70f51889e5 try gitpod with vnc 2022-10-26 18:47:58 -04:00
Michael Kochell
11242def6a add .gitpod.yml 2022-10-25 22:21:47 -04:00
Daniel Espino García
bb051b83b9 Use always first the local channels (#6594)
* Use always first the local channels

* Fix several race condition related issues

* Remove clean after cursor change

* Address feedback

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Daniel Espino <danielespino@MacBook-Pro-de-Daniel.local>
2022-10-24 10:55:31 +02:00
Daniel Espino García
c82c634523 Hide autocomplete when the input is blurred. (#6692) 2022-10-21 10:35:59 +02:00
Elias Nahum
0be105e8fb Fix gallery header & footer as well as download video toast (#6695) 2022-10-20 10:19:04 -03:00
Elias Nahum
e5d2e1d364 auto update timezone when device changes its location (#6691) 2022-10-20 10:18:41 -03:00
Elias Nahum
cae9dc21d2 [Gekidou] Upgrade to RN 0.70 (#6690)
* Upgrade to RN 0.70

* fix assembleAndroidTest
2022-10-20 10:18:25 -03:00
Jason Frerich
6b2d8ceff3 [Gekidou MM-47486] Clear the search input after selecting a user (#6683) 2022-10-20 05:54:00 -05:00
Daniel Espino García
b90eaefcc0 Remove not member teams on startup (#6688) 2022-10-20 11:31:37 +02:00
Guillermo Vayá
5b695d17b2 Merge pull request #6696 from mattermost/fix-translations
remove last comma
2022-10-19 19:36:13 +02:00
=
64f582952c remove last comma 2022-10-19 19:30:31 +02:00
Tom De Moor
388a72e2b6 adding current translations to gekidou (#6694)
Co-authored-by: Tom De Moor <tom@controlaltdieliet.be>
2022-10-19 11:37:44 -03:00
Ossi Väänänen
6c89f461c0 Merge pull request #6693 from mattermost/check-splitview-in-main-queue
iOS: Perform the split view check in main queue. (Some call paths caused the check to be done in a background thread)
2022-10-19 17:19:25 +03:00
Ossi Vaananen
756e842aa4 Perform the split view check in main queue. (Some call paths caused the check to be done in background threads) 2022-10-19 17:02:36 +03:00
Daniel Espino García
4f4f96ff24 Fix channel links when it implies joining a channel (#6686) 2022-10-18 16:16:06 +02:00
Rohitesh Gupta
b086776c33 Updated CodeQL to use latest version (#6689)
* Created CodeQL config

* Updated CodeQL to latest branch
2022-10-18 10:26:22 -03:00
Elias Nahum
52215b7749 CI Build fix (#6685)
* use xcode 14.0.0 in ci

* update jvmargs to use 2048m
2022-10-14 14:57:49 -03:00
Joseph Baylon
084d342e1d Detox/E2E: Verification fixes and added a step to post message on direct messages (#6664)
* Detox/E2E: Minor fixes and added known issue tag

* Add step to post message on direct message
2022-10-14 10:25:38 -07:00
Avinash Lingaloo
f000014809 Bump app build number to 428 (#6684) 2022-10-14 21:02:56 +04:00
Joseph Baylon
fa923a7dfd Detox/E2E: Modify reporting according to sharding (#6663) 2022-10-13 15:59:42 -07:00
Elias Nahum
a23b156f7a Fix navbar theme in share extension (#6678) 2022-10-13 08:42:00 -03:00
Elias Nahum
5c7e89d7de [Gekidou] update deps (#6667)
* Fix login not showing the usernames (#6652)

* update dependencies

* feedback review

* feedback review 2

* remove unused dependencies

* remove duplicate gesture-handler dependency

* add -XX:+HeapDumpOnOutOfMemoryError to gradle

* adjust gradle memory

* update android ci image

* android executor xlarge

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2022-10-13 08:41:18 -03:00
Elias Nahum
e1c74124e2 Fix switch to channel when notification is dismissed (#6675) 2022-10-11 10:36:45 -03:00
Elias Nahum
391c120db9 Fix iOS keyboard tracking when paused (#6676) 2022-10-11 10:29:14 -03:00
Jason Frerich
380b375411 [Gekidou MM-45790] Create File Options Menu for Tablet View (#6531) 2022-10-11 08:19:13 -05:00
Elias Nahum
d201035a89 [Gekidou] Add custom status to user profile sheet (#6670)
* Add custom status to user profile sheet

* add missing translations
2022-10-11 09:18:46 -03:00
Avinash Lingaloo
3e7ebfe95c [Gekidou] Bump app build number to 427 (#6671)
* Bump app build number to  427

* Update Gemfile.lock
2022-10-07 21:08:02 +04:00
Elias Nahum
4c4d5475f3 Fix channel calls icon alignment (#6668) 2022-10-07 09:14:48 -03:00
Daniel Espino García
2b4b7c7e92 Fix permissions defaulting to true (#6653) 2022-10-04 11:40:31 +02:00
Christopher Poile
681b6b0b0f MM-47003 - Calls - Fix for: peer connection timeout (#6658)
* force negotiation

* fix message bug

* remove forced negotiation
2022-10-03 13:12:02 -04:00
Daniel Espino García
e905f7df29 Fix login not showing the usernames (#6652) 2022-09-22 16:34:12 -04:00
198 changed files with 11032 additions and 9015 deletions

View File

@@ -1,20 +1,20 @@
version: 2.1
orbs:
owasp: entur/owasp@0.0.10
node: circleci/node@5.0.2
node: circleci/node@5.0.3
executors:
android:
parameters:
resource_class:
default: large
default: xlarge
type: string
environment:
NODE_OPTIONS: --max_old_space_size=12000
NODE_ENV: production
BABEL_ENV: production
docker:
- image: cimg/android:2022.03-node
- image: cimg/android:2022.09.2-node
working_directory: ~/mattermost-mobile
resource_class: <<parameters.resource_class>>
@@ -105,7 +105,7 @@ commands:
description: "Get JavaScript dependencies"
steps:
- node/install:
node-version: '16.14.2'
node-version: '18.7.0'
- restore_cache:
name: Restore npm cache
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}

View File

@@ -1,6 +1,7 @@
{
"extends": [
"plugin:mattermost/react",
"./eslint/eslint-mattermost",
"./eslint/eslint-react",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended"
@@ -8,7 +9,6 @@
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"mattermost",
"import"
],
"settings": {

View File

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

13
.github/codeql/codeql-config.yml vendored Normal file
View File

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

View File

@@ -9,8 +9,13 @@ on:
schedule:
- cron: '0 0 * * 0'
permissions:
contents: read
jobs:
analyze:
permissions:
security-events: write
name: Analyze
runs-on: ubuntu-latest
@@ -18,26 +23,20 @@ jobs:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# 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
config-file: ./.github/codeql/codeql-config.yml
# 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)
# Autobuild attempts to build any compiled languages
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

2
.gitignore vendored
View File

@@ -44,6 +44,8 @@ ios/.xcode.env.local
.gradle
local.properties
*.iml
*.hprof
.cxx/
android/app/bin
android/app/build
android/build

159
.gitpod.Dockerfile vendored Normal file
View File

@@ -0,0 +1,159 @@
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/

6
.gitpod.yml Normal file
View File

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

View File

@@ -224,18 +224,18 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
## @nozbe/watermelondb
This product contains 'cameraroll' by Bartol Karuza.
This product contains '@nozbe/watermelondb' by Nozbe.
React-native native module that provides access to the local camera roll or photo library
Build powerful React and React Native apps that scale from hundreds to tens of thousands of records and remain fast ⚡️
* HOMEPAGE:
* https://github.com/react-native-community/react-native-cameraroll
* https://github.com/Nozbe/WatermelonDB/
* LICENSE: MIT
MIT License
Copyright (c) 2020 Elias Nahum
Copyright (c) Nozbe
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,50 +293,14 @@ SOFTWARE.
---
## @react-native-community/art
## @react-native-cameraroll/react-native-cameraroll
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.
This product contains 'react-native-cameraroll' by Bartol Karuza.
React-native native module that provides access to the local camera roll or photo library
* HOMEPAGE:
* https://github.com/react-native-community/react-native-cameraroll
* https://github.com/react-native-cameraroll/react-native-cameraroll
* LICENSE: MIT
@@ -365,14 +329,14 @@ SOFTWARE.
---
## @react-native-community/clipboard
## @react-native-clipboard/clipboard
This product contains '@react-native-community/clipboard' by React Native Community.
This product contains '@react-native-clipboard/clipboard' by React Native Community.
React Native Clipboard API for both iOS and Android
* HOMEPAGE:
* https://github.com/react-native-community/clipboard
* https://github.com/react-native-clipboard/clipboard
* LICENSE: MIT
@@ -2350,20 +2314,20 @@ SOFTWARE.
---
## react-native-neomorph-shadows
## react-native-shadow-2
This product contains a modified version of 'react-native-neomorph-shadows' by Daniel.
This product contains a modified version of 'react-native-shadow-2' by Henrique Bruno Fantauzzi de Almeida.
Shadows and neumorphism/neomorphism for iOS & Android (like iOS).
Cross-platform shadow for React Native. Supports Android, iOS, Web and Expo.
* HOMEPAGE:
* https://github.com/tokkozhin/react-native-neomorph-shadows
* https://github.com/SrBrahma/react-native-shadow-2
* LICENSE: MIT
MIT License
Copyright (c) 2020 tokkozhin
Copyright (c) 2021 Henrique Bruno Fantauzzi de Almeida
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -2957,41 +2921,6 @@ 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,6 +1,7 @@
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
@@ -144,33 +145,21 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 426
versionCode 428
versionName "2.0.0"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
if (isNewArchitectureEnabled()) {
// We configure the NDK build only if you decide to opt-in for the New Architecture.
// We configure the CMake build only if you decide to opt-in for the New Architecture.
externalNativeBuild {
ndkBuild {
arguments "APP_PLATFORM=android-21",
"APP_STL=c++_shared",
"NDK_TOOLCHAIN_VERSION=clang",
"GENERATED_SRC_DIR=$buildDir/generated/source",
"PROJECT_BUILD_DIR=$buildDir",
"REACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
"REACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build",
"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"
}
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"
}
}
if (!enableSeparateBuildPerCPUArchitecture) {
@@ -184,8 +173,8 @@ android {
if (isNewArchitectureEnabled()) {
// We configure the NDK build only if you decide to opt-in for the New Architecture.
externalNativeBuild {
ndkBuild {
path "$projectDir/src/main/jni/Android.mk"
cmake {
path "$projectDir/src/main/jni/CMakeLists.txt"
}
}
def reactAndroidProjectDir = project(':ReactAndroid').projectDir
@@ -207,15 +196,15 @@ android {
preReleaseBuild.dependsOn(packageReactNdkReleaseLibs)
// Due to a bug inside AGP, we have to explicitly set a dependency
// between configureNdkBuild* tasks and the preBuild tasks.
// between configureCMakeDebug* tasks and the preBuild tasks.
// This can be removed once this is solved: https://issuetracker.google.com/issues/207403732
configureNdkBuildRelease.dependsOn(preReleaseBuild)
configureNdkBuildDebug.dependsOn(preDebugBuild)
configureCMakeDebugRelease.dependsOn(preReleaseBuild)
configureCMakeDebugDebug.dependsOn(preDebugBuild)
reactNativeArchitectures().each { architecture ->
tasks.findByName("configureNdkBuildDebug[${architecture}]")?.configure {
tasks.findByName("configureCMakeDebugDebug[${architecture}]")?.configure {
dependsOn("preDebugBuild")
}
tasks.findByName("configureNdkBuildRelease[${architecture}]")?.configure {
tasks.findByName("configureCMakeDebugRelease[${architecture}]")?.configure {
dependsOn("preReleaseBuild")
}
}

View File

@@ -5,6 +5,8 @@
<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" />

View File

@@ -1,48 +0,0 @@
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

@@ -0,0 +1,7 @@
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,12 +1,13 @@
#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
@@ -17,6 +18,13 @@ 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(
std::string name) {
const 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/mattermost/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;";
"Lcom/rndiffapp/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(std::string name);
bool canCreateTurboModule(const std::string &name);
};
} // namespace react
} // namespace facebook
} // namespace facebook

View File

@@ -4,6 +4,7 @@
#include <fbjni/fbjni.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/components/rncore/ComponentDescriptors.h>
#include <rncli.h>
namespace facebook {
namespace react {
@@ -14,6 +15,9 @@ 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.
//
@@ -58,4 +62,4 @@ void MainComponentsRegistry::registerNatives() {
}
} // namespace react
} // namespace facebook
} // namespace facebook

View File

@@ -1,5 +1,3 @@
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 {
@@ -27,7 +25,7 @@ buildscript {
google()
}
dependencies {
classpath("com.android.tools.build:gradle:7.1.1")
classpath("com.android.tools.build:gradle:7.2.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=1g
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# 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.3.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip

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 {prepareMyTeams, getNthLastChannelFromTeam, getMyTeamById, getTeamById, getTeamByName, queryMyTeams} from '@queries/servers/team';
import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams} from '@queries/servers/team';
import {getCurrentUser} from '@queries/servers/user';
import EphemeralStore from '@store/ephemeral_store';
import {generateChannelNameFromDisplayName, getDirectChannelName, isArchived, isDMorGM} from '@utils/channel';
import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from '@utils/channel';
import {isTablet} from '@utils/helpers';
import {logError, logInfo} from '@utils/log';
import {showMuteChannelSnackbar} from '@utils/snack_bar';
@@ -32,14 +32,11 @@ import {fetchPostsForChannel} from './post';
import {setDirectChannelVisible} from './preference';
import {fetchRolesIfNeeded} from './role';
import {forceLogoutIfNecessary} from './session';
import {addUserToTeam, fetchTeamByName, removeUserFromTeam} from './team';
import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} 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[];
@@ -531,11 +528,12 @@ export async function fetchDirectChannelsInfo(serverUrl: string, directChannels:
return fetchMissingDirectChannelsInfo(serverUrl, channels, currentUser?.locale, teammateDisplayNameSetting, currentUser?.id);
}
export async function joinChannel(serverUrl: string, userId: string, teamId: string, channelId?: string, channelName?: string, fetchOnly = false) {
export async function joinChannel(serverUrl: 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 {
@@ -544,6 +542,8 @@ export async function joinChannel(serverUrl: string, userId: string, teamId: str
return {error};
}
const userId = await getCurrentUserId(database);
let member: ChannelMembership | undefined;
let channel: Channel | undefined;
try {
@@ -614,8 +614,7 @@ export async function joinChannelIfNeeded(serverUrl: string, channelId: string)
return {error: undefined};
}
const userId = await getCurrentUserId(database);
return joinChannel(serverUrl, userId, '', channelId);
return joinChannel(serverUrl, '', channelId);
} catch (error) {
return {error};
}
@@ -634,167 +633,77 @@ 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 {
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 (name === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
name = currentTeam!.name;
if (teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
teamId = await getCurrentTeamId(database);
} else {
team = await getTeamByName(database, teamName);
}
const team = await getTeamByName(database, teamName);
const isTeamMember = team ? await getMyTeamById(database, team.id) : false;
teamId = team?.id || '';
if (!team) {
const fetchTeam = await fetchTeamByName(serverUrl, name, true);
if (fetchTeam.error) {
errorHandler(intl);
return {error: fetchTeam.error};
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;
}
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};
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'};
}
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 (fetchRequest.channel.type === General.PRIVATE_CHANNEL) {
const {join} = await privateChannelJoinPrompt(fetchRequest.channel.display_name, intl);
if (!join) {
if (joinedNewTeam) {
await removeUserFromTeam(serverUrl, team.id, system.currentUserId, true);
}
errorHandler(intl);
onError(joinedTeam, teamId);
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);
}
errorHandler(intl);
return {error: result.error};
}
myChannel = result.member!;
roles.push(...myChannel.roles.split(' '));
}
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'};
}
channelId = fetchRequest.channel.id;
}
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) {
errorHandler(intl);
onError(joinedTeam, teamId);
return {error};
}
}
@@ -1147,7 +1056,7 @@ export async function switchToLastChannel(serverUrl: string, teamId?: string) {
}
}
export async function searchChannels(serverUrl: string, term: string, isSearch = false) {
export async function searchChannels(serverUrl: string, term: string, teamId: string, isSearch = false) {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
@@ -1161,9 +1070,8 @@ export async function searchChannels(serverUrl: string, term: string, isSearch =
}
try {
const currentTeamId = await getCurrentTeamId(database);
const autoCompleteFunc = isSearch ? client.autocompleteChannelsForSearch : client.autocompleteChannels;
const channels = await autoCompleteFunc(currentTeamId, term);
const channels = await autoCompleteFunc(teamId, term);
return {channels};
} catch (error) {
return {error};

View File

@@ -11,7 +11,7 @@ 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 {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
import {autoUpdateTimezone, fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
import {gqlAllChannels} from '@client/graphQL/entry';
import {General, Preferences, Screens} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
@@ -65,6 +65,12 @@ 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) {
@@ -162,7 +168,6 @@ 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;
@@ -179,10 +184,7 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
}
}
const removedFromTeam = teamData.memberships?.filter((m) => m.delete_at > 0);
if (removedFromTeam?.length) {
removeTeamIds.push(...removedFromTeam.map((m) => m.team_id));
}
const removeTeamIds = await getRemoveTeamIds(database, teamData);
let data: AppEntryData = {
initialTeamId,
@@ -195,10 +197,6 @@ 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: '',
@@ -383,6 +381,7 @@ export const syncOtherServers = async (serverUrl: string) => {
if (server.url !== serverUrl && server.lastActiveAt > 0) {
registerDeviceToken(server.url);
syncAllChannelMembersAndThreads(server.url);
autoUpdateTimezone(server.url);
}
}
}

View File

@@ -9,7 +9,7 @@ import {fetchGroupsForMember} from '@actions/remote/groups';
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
import {MyTeamsRequest} from '@actions/remote/team';
import {fetchNewThreads} from '@actions/remote/thread';
import {updateAllUsersSince} from '@actions/remote/user';
import {autoUpdateTimezone, updateAllUsersSince} from '@actions/remote/user';
import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry';
import {Preferences} from '@constants';
import DatabaseManager from '@database/manager';
@@ -19,16 +19,14 @@ import {queryAllChannels, queryAllChannelsForTeam} from '@queries/servers/channe
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
import {getHasCRTChanged} from '@queries/servers/preference';
import {getConfig} from '@queries/servers/system';
import {queryMyTeams} from '@queries/servers/team';
import {filterAndTransformRoles, getMemberChannelsFromGQLQuery, getMemberTeamsFromGQLQuery, gqlToClientChannelMembership, gqlToClientPreference, gqlToClientSidebarCategory, gqlToClientTeamMembership, gqlToClientUser} from '@utils/graphql';
import {logDebug} from '@utils/log';
import {processIsCRTEnabled} from '@utils/thread';
import {teamsToRemove, FETCH_UNREADS_TIMEOUT, entryRest, EntryResponse, entryInitialChannelId, restDeferredAppEntryActions} from './common';
import {teamsToRemove, FETCH_UNREADS_TIMEOUT, entryRest, EntryResponse, entryInitialChannelId, restDeferredAppEntryActions, getRemoveTeamIds} 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,
@@ -227,23 +225,9 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
const roles = filterAndTransformRoles(gqlRoles);
const initialChannelId = await entryInitialChannelId(database, currentChannelId, currentTeamId, initialTeamId, meData.user.id, chData?.channels, chData?.memberships);
let removeTeams: TeamModel[] = [];
const removeChannels = await getRemoveChannels(database, chData, initialTeamId, true);
const removeTeamIds = [];
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 removeTeamIds = await getRemoveTeamIds(database, teamData);
const removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData}, true);
if (roles.length) {
@@ -285,5 +269,7 @@ export async function deferredAppEntryActions(
result = restDeferredAppEntryActions(serverUrl, since, currentUserId, currentUserLocale, preferences, config, license, teamData, chData, initialTeamId, initialChannelId);
}
autoUpdateTimezone(serverUrl);
return result;
}

View File

@@ -19,18 +19,16 @@ import EphemeralStore from '@store/ephemeral_store';
import {logWarning, logError} from '@utils/log';
import {scheduleExpiredNotification} from '@utils/notification';
import {getCSRFFromCookie} from '@utils/security';
import {getDeviceTimezone, isTimezoneEnabled} from '@utils/timezone';
import {loginEntry} from './entry';
import {fetchDataRetentionPolicy} from './systems';
import {autoUpdateTimezone} from './user';
import type ClientError from '@client/rest/error';
import type {LoginArgs} from '@typings/database/database';
const HTTP_UNAUTHORIZED = 401;
export const completeLogin = async (serverUrl: string, user: UserProfile) => {
export const completeLogin = async (serverUrl: string) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
@@ -43,12 +41,6 @@ export const completeLogin = async (serverUrl: string, user: UserProfile) => {
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);
@@ -165,7 +157,7 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
try {
const {error, hasTeams, time} = await loginEntry({serverUrl, user});
completeLogin(serverUrl, user);
completeLogin(serverUrl);
return {error: error as ClientError, failed: false, hasTeams, time};
} catch (error) {
return {error: error as ClientError, failed: false, time: 0};
@@ -309,7 +301,7 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
try {
const {error, hasTeams, time} = await loginEntry({serverUrl, user, deviceToken});
completeLogin(serverUrl, user);
completeLogin(serverUrl);
return {error: error as ClientError, failed: false, hasTeams, time};
} catch (error) {
return {error: error as ClientError, failed: false, time: 0};

View File

@@ -63,7 +63,7 @@ export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false
]);
if (!fetchOnly) {
storeConfigAndLicense(serverUrl, config, license);
await storeConfigAndLicense(serverUrl, config, license);
}
return {config, license};

View File

@@ -55,6 +55,7 @@ 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) {
@@ -68,6 +69,7 @@ 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 || []),
@@ -248,6 +250,16 @@ 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

@@ -15,9 +15,10 @@ 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 {getCurrentTeamId, getCurrentUserId} from '@queries/servers/system';
import {getCurrentUser, getUserById, prepareUsers, queryAllUsers, queryUsersById, queryUsersByIdsOrUsernames, queryUsersByUsername} from '@queries/servers/user';
import {getConfig, getCurrentUserId} from '@queries/servers/system';
import {getCurrentUser, 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';
@@ -817,7 +818,7 @@ export const uploadUserProfileImage = async (serverUrl: string, localPath: strin
return {error: undefined};
};
export const searchUsers = async (serverUrl: string, term: string, channelId?: string) => {
export const searchUsers = async (serverUrl: string, term: string, teamId: string, channelId?: string) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
@@ -831,8 +832,7 @@ export const searchUsers = async (serverUrl: string, term: string, channelId?: s
}
try {
const currentTeamId = await getCurrentTeamId(database);
const users = await client.autocompleteUsers(term, currentTeamId, channelId);
const users = await client.autocompleteUsers(term, teamId, 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, {deviceTimezone, userId}: {deviceTimezone: string; userId: string}) => {
export const autoUpdateTimezone = async (serverUrl: string) => {
let database;
try {
const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@@ -859,12 +859,16 @@ export const autoUpdateTimezone = async (serverUrl: string, {deviceTimezone, use
return {error: `${serverUrl} database not found`};
}
const currentUser = await getUserById(database, userId);
const config = await getConfig(database);
const currentUser = await getCurrentUser(database);
if (!currentUser) {
if (!currentUser || !config || !isTimezoneEnabled(config)) {
return null;
}
// Set timezone
const deviceTimezone = getDeviceTimezone();
const currentTimezone = getUserTimezoneProps(currentUser);
const newTimezoneExists = currentTimezone.automaticTimezone !== deviceTimezone;

View File

@@ -3,8 +3,8 @@
exports[`@components/app_version should match snapshot 1`] = `
<View
animatedStyle={
Object {
"value": Object {
{
"value": {
"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,7 +2,7 @@
// See LICENSE.txt for license information.
import {debounce} from 'lodash';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {Platform, SectionList, SectionListData, SectionListRenderItemInfo, StyleProp, ViewStyle} from 'react-native';
import {searchGroupsByName, searchGroupsByNameInChannel, searchGroupsByNameInTeam} from '@actions/local/group';
@@ -175,9 +175,35 @@ const makeSections = (teamMembers: Array<UserProfile | UserModel>, usersInChanne
return newSections;
};
const searchGroups = async (serverUrl: string, matchTerm: string, useGroupMentions: boolean, isChannelConstrained: boolean, isTeamConstrained: boolean, channelId?: string, teamId?: string) => {
try {
if (useGroupMentions && matchTerm && matchTerm !== '') {
let g = emptyGroupList;
if (isChannelConstrained) {
// If the channel is constrained, we only show groups for that channel
if (channelId) {
g = await searchGroupsByNameInChannel(serverUrl, matchTerm, channelId);
}
} else if (isTeamConstrained) {
// If there is no channel constraint, but a team constraint - only show groups for team
g = await searchGroupsByNameInTeam(serverUrl, matchTerm, teamId!);
} else {
// No constraints? Search all groups
g = await searchGroupsByName(serverUrl, matchTerm || '');
}
return g.length ? g : emptyGroupList;
}
return emptyGroupList;
} catch (error) {
return emptyGroupList;
}
};
type Props = {
channelId?: string;
teamId?: string;
teamId: string;
cursorPosition: number;
isSearch: boolean;
updateValue: (v: string) => void;
@@ -232,9 +258,22 @@ const AtMention = ({
const [localUsers, setLocalUsers] = useState<UserModel[]>();
const [filteredLocalUsers, setFilteredLocalUsers] = useState(emptyUserlList);
const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, cId?: string) => {
setLoading(true);
const {users: receivedUsers, error} = await searchUsers(sUrl, term, cId);
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);
setUseLocal(Boolean(error));
if (error) {
@@ -243,6 +282,10 @@ const AtMention = ({
fallbackUsers = await getAllUsers(sUrl);
setLocalUsers(fallbackUsers);
}
if (latestSearchAt.current > searchAt) {
return;
}
const filteredUsers = filterResults(fallbackUsers, term);
setFilteredLocalUsers(filteredUsers.length ? filteredUsers : emptyUserlList);
} else if (receivedUsers) {
@@ -270,8 +313,12 @@ const AtMention = ({
const resetState = () => {
setUsersInChannel(emptyUserlList);
setUsersOutOfChannel(emptyUserlList);
setGroups(emptyGroupList);
setFilteredLocalUsers(emptyUserlList);
setSections(emptySectionList);
setNoResultsTerm(null);
latestSearchAt.current = Date.now();
setLoading(false);
runSearch.cancel();
};
@@ -285,7 +332,7 @@ const AtMention = ({
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
}
const newCursorPosition = completedDraft.length - 1;
const newCursorPosition = completedDraft.length;
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
@@ -297,6 +344,7 @@ const AtMention = ({
onShowingChange(false);
setNoResultsTerm(mention);
setSections(emptySectionList);
latestSearchAt.current = Date.now();
}, [value, localCursorPosition, isSearch]);
const renderSpecialMentions = useCallback((item: SpecialMention) => {
@@ -361,39 +409,6 @@ 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();
@@ -406,10 +421,14 @@ const AtMention = ({
}
setNoResultsTerm(null);
runSearch(serverUrl, matchTerm, channelId);
}, [matchTerm]);
setLoading(true);
runSearch(serverUrl, matchTerm, useGroupMentions, isChannelConstrained, isTeamConstrained, teamId, channelId);
}, [matchTerm, teamId, useGroupMentions, isChannelConstrained, isTeamConstrained]);
useEffect(() => {
if (noResultsTerm && !loading) {
return;
}
const showSpecialMentions = useChannelMentions && matchTerm != null && checkSpecialMentions(matchTerm);
const buildMemberSection = isSearch || (!channelId && teamMembers.length > 0);
let newSections;

View File

@@ -18,8 +18,11 @@ 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}
const enhanced = withObservables([], ({database, channelId}: WithDatabaseArgs & OwnProps) => {
type OwnProps = {
channelId?: string;
teamId?: string;
}
const enhanced = withObservables(['teamId'], ({database, channelId, teamId}: WithDatabaseArgs & OwnProps) => {
const currentUser = observeCurrentUser(database);
const hasLicense = observeLicense(database).pipe(
@@ -51,20 +54,19 @@ const enhanced = withObservables([], ({database, channelId}: WithDatabaseArgs &
useGroupMentions = of$(false);
isChannelConstrained = of$(false);
isTeamConstrained = of$(false);
team = observeCurrentTeam(database);
team = teamId ? observeTeam(database, teamId) : 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,
teamId: team.pipe(switchMap((t) => of$(t?.id))),
};
});

View File

@@ -61,6 +61,7 @@ type Props = {
availableSpace: SharedValue<number>;
inPost?: boolean;
growDown?: boolean;
teamId?: string;
containerStyle?: StyleProp<ViewStyle>;
}
@@ -81,6 +82,7 @@ const Autocomplete = ({
inPost = false,
growDown = false,
containerStyle,
teamId,
}: Props) => {
const theme = useTheme();
const isTablet = useIsTablet();
@@ -152,6 +154,7 @@ const Autocomplete = ({
nestedScrollEnabled={nestedScrollEnabled}
isSearch={isSearch}
channelId={channelId}
teamId={teamId}
/>
<ChannelMention
cursorPosition={cursorPosition}
@@ -161,6 +164,8 @@ const Autocomplete = ({
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
isSearch={isSearch}
channelId={channelId}
teamId={teamId}
/>
{!isSearch &&
<EmojiSuggestion

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {debounce} from 'lodash';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {Platform, SectionList, SectionListData, SectionListRenderItemInfo, StyleProp, ViewStyle} from 'react-native';
import {searchChannels} from '@actions/remote/channel';
@@ -11,11 +11,8 @@ import ChannelMentionItem from '@components/autocomplete/channel_mention_item';
import {General} from '@constants';
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
import {useServerUrl} from '@context/server';
import DatabaseManager from '@database/manager';
import useDidUpdate from '@hooks/did_update';
import {t} from '@i18n';
import {queryAllChannelsForTeam} from '@queries/servers/channel';
import {getCurrentTeamId} from '@queries/servers/system';
import {hasTrailingSpaces} from '@utils/helpers';
import type ChannelModel from '@typings/database/models/servers/channel';
@@ -25,32 +22,6 @@ 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) => {
@@ -83,7 +54,7 @@ const reduceChannelsForAutocomplete = (channels: Array<Channel | ChannelModel>,
}, [[], []]);
};
const makeSections = (channels: Array<Channel | ChannelModel>, myMembers: MyChannelModel[], isSearch = false) => {
const makeSections = (channels: Array<Channel | ChannelModel>, myMembers: MyChannelModel[], loading: boolean, isSearch = false) => {
const newSections = [];
if (isSearch) {
const [publicChannels, privateChannels, directAndGroupMessages] = reduceChannelsForSearch(channels, myMembers);
@@ -128,7 +99,7 @@ const makeSections = (channels: Array<Channel | ChannelModel>, myMembers: MyChan
});
}
if (otherChannels.length) {
if (otherChannels.length || (!myChannels.length && loading)) {
newSections.push({
id: t('suggestion.mention.morechannels'),
defaultMessage: 'Other Channels',
@@ -164,18 +135,11 @@ type Props = {
value: string;
nestedScrollEnabled: boolean;
listStyle: StyleProp<ViewStyle>;
matchTerm: string;
localChannels: ChannelModel[];
teamId: string;
}
const getAllChannels = async (serverUrl: string) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return [];
}
const teamId = await getCurrentTeamId(database);
return queryAllChannelsForTeam(database, teamId).fetch();
};
const emptySections: Array<SectionListData<Channel>> = [];
const emptyChannels: Array<Channel | ChannelModel> = [];
@@ -188,47 +152,45 @@ const ChannelMention = ({
value,
nestedScrollEnabled,
listStyle,
matchTerm,
localChannels,
teamId,
}: Props) => {
const serverUrl = useServerUrl();
const [sections, setSections] = useState<Array<SectionListData<(Channel | ChannelModel)>>>(emptySections);
const [channels, setChannels] = useState<Array<ChannelModel | Channel>>(emptyChannels);
const [remoteChannels, setRemoteChannels] = useState<Array<ChannelModel | Channel>>(emptyChannels);
const [loading, setLoading] = useState(false);
const [noResultsTerm, setNoResultsTerm] = useState<string|null>(null);
const [localCursorPosition, setLocalCursorPosition] = useState(cursorPosition); // To avoid errors due to delay between value changes and cursor position changes.
const [useLocal, setUseLocal] = useState(true);
const [localChannels, setlocalChannels] = useState<ChannelModel[]>();
const [filteredLocalChannels, setFilteredLocalChannels] = useState(emptyChannels);
const runSearch = useMemo(() => debounce(async (sUrl: string, term: string) => {
setLoading(true);
const {channels: receivedChannels, error} = await searchChannels(sUrl, term, isSearch);
setUseLocal(Boolean(error));
const latestSearchAt = useRef(0);
if (error) {
let fallbackChannels = localChannels;
if (!fallbackChannels) {
fallbackChannels = await getAllChannels(sUrl);
setlocalChannels(fallbackChannels);
}
const filteredChannels = filterResults(fallbackChannels, term);
setFilteredLocalChannels(filteredChannels.length ? filteredChannels : emptyChannels);
} else if (receivedChannels) {
let channelsToStore: Array<Channel | ChannelModel> = receivedChannels;
if (hasTrailingSpaces(term)) {
channelsToStore = filterResults(receivedChannels, term);
}
setChannels(channelsToStore.length ? channelsToStore : emptyChannels);
const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, tId: string) => {
const searchAt = Date.now();
latestSearchAt.current = searchAt;
const {channels: receivedChannels} = await searchChannels(sUrl, term, tId, isSearch);
if (latestSearchAt.current > searchAt) {
return;
}
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 = () => {
setFilteredLocalChannels(emptyChannels);
setChannels(emptyChannels);
latestSearchAt.current = Date.now();
setRemoteChannels(emptyChannels);
setSections(emptySections);
setNoResultsTerm(null);
runSearch.cancel();
setLoading(false);
};
const completeMention = useCallback((mention: string) => {
@@ -264,8 +226,11 @@ 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>) => {
@@ -306,21 +271,41 @@ const ChannelMention = ({
}
setNoResultsTerm(null);
runSearch(serverUrl, matchTerm);
}, [matchTerm]);
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]);
useDidUpdate(() => {
const newSections = makeSections(useLocal ? filteredLocalChannels : channels, myMembers, isSearch);
if (noResultsTerm && !loading) {
return;
}
const newSections = makeSections(channels, myMembers, loading, 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 (sections.length === 0 || noResultsTerm != null) {
if (!loading && (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,17 +3,90 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {queryAllMyChannel} from '@queries/servers/channel';
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 ChannelMention from './channel_mention';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
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) => {
return {
myMembers: queryAllMyChannel(database).observe(),
};
});
export default withDatabase(enhanced(ChannelMention));
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))));

View File

@@ -1899,7 +1899,7 @@ export class AppCommandParser {
if (input[0] === '@') {
input = input.substring(1);
}
const res = await searchUsers(this.serverUrl, input, this.channelID);
const res = await searchUsers(this.serverUrl, input, this.teamID, 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);
const res = await searchChannels(this.serverUrl, input, this.teamID);
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), channelID);
const res = await searchUsers(serverUrl, lastWord.substring(1), teamID, 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));
const res = await searchChannels(serverUrl, lastWord.substring(1), teamID);
const channels = await getChannelSuggestions(res.channels);
channels.forEach((c) => {
let complete = incompleteLessLastWord ? incompleteLessLastWord + ' ' + c.Complete : c.Complete;

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-community/clipboard';
import Clipboard from '@react-native-clipboard/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-community/clipboard';
import Clipboard from '@react-native-clipboard/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,9 +244,8 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
null,
false,
false,
Object {
"flex": 1,
"marginRight": 20,
{
"paddingRight": 0,
"textAlign": "right",
},
]
@@ -269,15 +268,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,
@@ -285,7 +284,7 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
},
false,
undefined,
Object {
{
"minHeight": 40,
},
]
@@ -294,7 +293,7 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
>
<View
style={
Object {
{
"flex": 1,
"flexDirection": "row",
}
@@ -302,12 +301,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,
},
@@ -319,13 +318,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,
},
@@ -339,14 +338,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,9 +116,8 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
top: 5,
},
hasCall: {
flex: 1,
textAlign: 'right',
marginRight: 20,
paddingRight: 0,
},
}));

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-community/clipboard';
import Clipboard from '@react-native-clipboard/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

@@ -6,9 +6,9 @@ exports[`components/custom_status/custom_status_emoji should match snapshot 1`]
>
<Text
style={
Array [
[
undefined,
Object {
{
"color": "#000",
"fontSize": 16,
},
@@ -26,9 +26,9 @@ exports[`components/custom_status/custom_status_emoji should match snapshot with
>
<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

@@ -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,7 +30,8 @@ type FileProps = {
onPress: (index: number) => void;
publicLinkEnabled: boolean;
channelName?: string;
onOptionsPress?: (index: number) => void;
onOptionsPress?: (fileInfo: FileInfo) => void;
optionSelected?: boolean;
wrapperWidth?: number;
showDate?: boolean;
updateFileForGallery: (idx: number, file: FileInfo) => void;
@@ -74,6 +75,7 @@ const File = ({
nonVisibleImagesCount = 0,
onOptionsPress,
onPress,
optionSelected,
publicLinkEnabled,
showDate = false,
updateFileForGallery,
@@ -94,19 +96,15 @@ const File = ({
const {styles, onGestureEvent, ref} = useGalleryItem(galleryIdentifier, index, handlePreviewPress);
const handleOnOptionsPress = useCallback(() => {
onOptionsPress?.(index);
}, [index, onOptionsPress]);
onOptionsPress?.(file);
}, [file, onOptionsPress]);
const renderOptionsButton = () => {
if (onOptionsPress) {
return (
<FileOptionsIcon
onPress={handleOnOptionsPress}
/>
);
}
return null;
};
const optionsButton = (
<FileOptionsIcon
onPress={handleOnOptionsPress}
selected={optionSelected}
/>
);
const fileInfo = (
<FileInfo
@@ -174,7 +172,7 @@ const File = ({
{fileIcon}
</View>
{fileInfo}
{renderOptionsButton()}
{onOptionsPress && optionsButton}
</View>
);
};
@@ -189,7 +187,7 @@ const File = ({
<View style={[style.fileWrapper]}>
{renderDocumentFile}
{fileInfo}
{renderOptionsButton()}
{onOptionsPress && optionsButton}
</View>
);
} else {

View File

@@ -2,32 +2,38 @@
// See LICENSE.txt for license information.
import React from 'react';
import {TouchableOpacity, StyleSheet} from 'react-native';
import {TouchableOpacity} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {useTheme} from '@context/theme';
import {changeOpacity} from '@utils/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
onPress: () => void;
selected?: boolean;
}
const styles = StyleSheet.create({
threeDotContainer: {
alignItems: 'flex-end',
marginHorizontal: 20,
},
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
threeDotContainer: {
alignItems: 'flex-end',
borderRadius: 4,
marginHorizontal: 20,
padding: 7,
},
selected: {
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
},
};
});
const hitSlop = {top: 5, bottom: 5, left: 5, right: 5};
export default function FileOptionsIcon({onPress}: Props) {
export default function FileOptionsIcon({onPress, selected = false}: Props) {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (
<TouchableOpacity
onPress={onPress}
style={styles.threeDotContainer}
hitSlop={hitSlop}
style={[styles.threeDotContainer, selected ? styles.selected : null]}
>
<CompassIcon
name='dots-horizontal'

View File

@@ -6,7 +6,6 @@ import Svg, {
Mask,
G,
} from 'react-native-svg';
import {EMaskUnits} from 'react-native-svg/src/elements/Mask';
type Props = {
theme: Theme;
@@ -60,7 +59,7 @@ function SvgComponent({theme}: Props) {
/>
<Mask
id='a'
maskUnits={EMaskUnits.USER_SPACE_ON_USE}
maskUnits='userSpaceOnUse'
x={76}
y={43}
width={134}

View File

@@ -6,7 +6,6 @@ import Svg, {
Mask,
G,
} from 'react-native-svg';
import {EMaskUnits} from 'react-native-svg/src/elements/Mask';
type Props = {
theme: Theme;
@@ -60,7 +59,7 @@ function SvgComponent({theme}: Props) {
/>
<Mask
id='a'
maskUnits={EMaskUnits.USER_SPACE_ON_USE}
maskUnits='userSpaceOnUse'
x={76}
y={43}
width={134}

View File

@@ -11,7 +11,6 @@ import Svg, {
Use,
Image,
} from 'react-native-svg';
import {EMaskUnits} from 'react-native-svg/src/elements/Mask';
type Props = {
theme: Theme;
@@ -31,7 +30,7 @@ function SvgComponent({theme}: Props) {
/>
<Mask
id='a'
maskUnits={EMaskUnits.USER_SPACE_ON_USE}
maskUnits='userSpaceOnUse'
x={3}
y={0}
width={117}
@@ -53,7 +52,7 @@ function SvgComponent({theme}: Props) {
/>
<Mask
id='b'
maskUnits={EMaskUnits.USER_SPACE_ON_USE}
maskUnits='userSpaceOnUse'
x={32}
y={42}
width={71}
@@ -102,7 +101,7 @@ function SvgComponent({theme}: Props) {
/>
<Mask
id='c'
maskUnits={EMaskUnits.USER_SPACE_ON_USE}
maskUnits='userSpaceOnUse'
x={25}
y={3}
width={53}
@@ -125,7 +124,7 @@ function SvgComponent({theme}: Props) {
/>
<Mask
id='d'
maskUnits={EMaskUnits.USER_SPACE_ON_USE}
maskUnits='userSpaceOnUse'
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-community/clipboard';
import Clipboard from '@react-native-clipboard/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,7 +21,6 @@ type ChannelMentionProps = {
channelName: string;
channels: ChannelModel[];
currentTeamId: string;
currentUserId: string;
linkStyle: StyleProp<TextStyle>;
team: TeamModel;
textStyle: StyleProp<TextStyle>;
@@ -57,7 +56,7 @@ function getChannelFromChannelName(name: string, channels: ChannelModel[], chann
}
const ChannelMention = ({
channelMentions, channelName, channels, currentTeamId, currentUserId,
channelMentions, channelName, channels, currentTeamId,
linkStyle, team, textStyle,
}: ChannelMentionProps) => {
const intl = useIntl();
@@ -68,7 +67,7 @@ const ChannelMention = ({
let c = channel;
if (!c?.id && c?.display_name) {
const result = await joinChannel(serverUrl, currentUserId, currentTeamId, undefined, channelName);
const result = await joinChannel(serverUrl, 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, observeCurrentUserId} from '@queries/servers/system';
import {observeCurrentTeamId} from '@queries/servers/system';
import {observeTeam} from '@queries/servers/team';
import ChannelMention from './channel_mention';
@@ -17,7 +17,6 @@ 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'])),
);
@@ -28,7 +27,6 @@ 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-community/clipboard';
import Clipboard from '@react-native-clipboard/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-community/clipboard';
import Clipboard from '@react-native-clipboard/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';

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {useManagedConfig} from '@mattermost/react-native-emm';
import Clipboard from '@react-native-community/clipboard';
import Clipboard from '@react-native-clipboard/clipboard';
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, View, Text, StyleSheet, Platform} 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-community/clipboard';
import Clipboard from '@react-native-clipboard/clipboard';
import React, {Children, ReactElement, useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Alert, StyleSheet, Text, View} from 'react-native';

View File

@@ -24,6 +24,7 @@ type Props = {
updatePostInputTop: (top: number) => void;
updateValue: (value: string) => void;
value: string;
setIsFocused: (isFocused: boolean) => void;
}
const emptyFileList: FileInfo[] = [];
@@ -47,6 +48,7 @@ export default function DraftHandler(props: Props) {
updatePostInputTop,
updateValue,
value,
setIsFocused,
} = props;
const serverUrl = useServerUrl();
@@ -144,6 +146,7 @@ export default function DraftHandler(props: Props) {
updateCursorPosition={updateCursorPosition}
updatePostInputTop={updatePostInputTop}
updateValue={updateValue}
setIsFocused={setIsFocused}
/>
);
}

View File

@@ -36,6 +36,7 @@ type Props = {
updateValue: (value: string) => void;
addFiles: (files: FileInfo[]) => void;
updatePostInputTop: (top: number) => void;
setIsFocused: (isFocused: boolean) => void;
}
const SAFE_AREA_VIEW_EDGES: Edge[] = ['left', 'right'];
@@ -94,6 +95,7 @@ export default function DraftInput({
updateCursorPosition,
cursorPosition,
updatePostInputTop,
setIsFocused,
}: Props) {
const theme = useTheme();
@@ -142,6 +144,7 @@ export default function DraftInput({
value={value}
addFiles={addFiles}
sendMessage={sendMessage}
setIsFocused={setIsFocused}
/>
<Uploads
currentUserId={currentUserId}

View File

@@ -57,6 +57,7 @@ function PostDraft({
const [value, setValue] = useState(message);
const [cursorPosition, setCursorPosition] = useState(message.length);
const [postInputTop, setPostInputTop] = useState(0);
const [isFocused, setIsFocused] = useState(false);
const isTablet = useIsTablet();
const keyboardHeight = useKeyboardHeight(keyboardTracker);
const insets = useSafeAreaInsets();
@@ -110,10 +111,11 @@ function PostDraft({
updatePostInputTop={setPostInputTop}
updateValue={setValue}
value={value}
setIsFocused={setIsFocused}
/>
);
const autoComplete = (
const autoComplete = isFocused ? (
<Autocomplete
position={animatedAutocompletePosition}
updateValue={setValue}
@@ -126,7 +128,7 @@ function PostDraft({
inPost={true}
availableSpace={animatedAutocompleteAvailableSpace}
/>
);
) : null;
if (Platform.OS === 'android') {
return (

View File

@@ -39,6 +39,7 @@ type Props = {
cursorPosition: number;
updateCursorPosition: (pos: number) => void;
sendMessage: () => void;
setIsFocused: (isFocused: boolean) => void;
}
const showPasteFilesErrorDialog = (intl: IntlShape) => {
@@ -108,6 +109,7 @@ export default function PostInput({
cursorPosition,
updateCursorPosition,
sendMessage,
setIsFocused,
}: Props) {
const intl = useIntl();
const isTablet = useIsTablet();
@@ -140,7 +142,12 @@ export default function PostInput({
const onBlur = useCallback(() => {
updateDraftMessage(serverUrl, channelId, rootId, value);
}, [channelId, rootId, value]);
setIsFocused(false);
}, [channelId, rootId, value, setIsFocused]);
const onFocus = useCallback(() => {
setIsFocused(true);
}, [setIsFocused]);
const checkMessageLength = useCallback((newValue: string) => {
const valueLength = newValue.trim().length;
@@ -308,6 +315,7 @@ export default function PostInput({
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
multiline={true}
onBlur={onBlur}
onFocus={onFocus}
blurOnSubmit={false}
underlineColorAndroid='transparent'
keyboardType={keyboardType}

View File

@@ -29,6 +29,7 @@ type Props = {
testID?: string;
channelId: string;
rootId: string;
setIsFocused: (isFocused: boolean) => void;
// From database
currentUserId: string;
@@ -73,6 +74,7 @@ export default function SendHandler({
uploadFileError,
updateCursorPosition,
updatePostInputTop,
setIsFocused,
}: Props) {
const intl = useIntl();
const serverUrl = useServerUrl();
@@ -290,6 +292,7 @@ export default function SendHandler({
canSend={canSend()}
maxMessageLength={maxMessageLength}
updatePostInputTop={updatePostInputTop}
setIsFocused={setIsFocused}
/>
);
}

View File

@@ -3,7 +3,7 @@
exports[`AttachmentFooter it matches snapshot when both footer and footer_icon are provided 1`] = `
<View
style={
Object {
{
"flex": 1,
"flexDirection": "row",
"marginTop": 5,
@@ -12,11 +12,11 @@ exports[`AttachmentFooter it matches snapshot when both footer and footer_icon a
>
<View
style={
Array [
Object {
[
{
"overflow": "hidden",
},
Object {
{
"height": 12,
"marginRight": 5,
"marginTop": 1,
@@ -26,14 +26,15 @@ exports[`AttachmentFooter it matches snapshot when both footer and footer_icon a
}
>
<FastImageView
defaultSource={null}
resizeMode="cover"
source={
Object {
{
"uri": "https://images.com/image.png",
}
}
style={
Object {
{
"bottom": 0,
"left": 0,
"position": "absolute",
@@ -47,7 +48,7 @@ exports[`AttachmentFooter it matches snapshot when both footer and footer_icon a
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
{
"color": "rgba(63,67,80,0.5)",
"fontSize": 11,
}
@@ -61,7 +62,7 @@ exports[`AttachmentFooter it matches snapshot when both footer and footer_icon a
exports[`AttachmentFooter it matches snapshot when footer text is provided 1`] = `
<View
style={
Object {
{
"flex": 1,
"flexDirection": "row",
"marginTop": 5,
@@ -72,7 +73,7 @@ exports[`AttachmentFooter it matches snapshot when footer text is provided 1`] =
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
{
"color": "rgba(63,67,80,0.5)",
"fontSize": 11,
}

View File

@@ -29,6 +29,7 @@ type HeaderProps = {
enablePostUsernameOverride: boolean;
isAutoResponse: boolean;
isCRTEnabled?: boolean;
isCustomStatusEnabled: boolean;
isEphemeral: boolean;
isMilitaryTime: boolean;
isPendingOrFailed: boolean;
@@ -76,7 +77,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const Header = (props: HeaderProps) => {
const {
author, commentCount = 0, currentUser, enablePostUsernameOverride, isAutoResponse, isCRTEnabled,
author, commentCount = 0, currentUser, enablePostUsernameOverride, isAutoResponse, isCRTEnabled, isCustomStatusEnabled,
isEphemeral, isMilitaryTime, isPendingOrFailed, isPostPriorityEnabled, isSystemPost, isTimezoneEnabled, isWebHook,
location, post, rootPostAuthor, shouldRenderReplyButton, teammateNameDisplay,
} = props;
@@ -90,7 +91,7 @@ const Header = (props: HeaderProps) => {
const customStatus = getUserCustomStatus(author);
const customStatusExpired = isCustomStatusExpired(author);
const showCustomStatusEmoji = Boolean(
displayName && customStatus &&
isCustomStatusEnabled && displayName && customStatus &&
!(isSystemPost || author.isBot || isAutoResponse || isWebHook),
);

View File

@@ -34,6 +34,7 @@ const withHeaderProps = withObservables(
const isMilitaryTime = preferences.pipe(map((prefs) => getPreferenceAsBool(prefs, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)));
const teammateNameDisplay = observeTeammateNameDisplay(database);
const commentCount = queryPostReplies(database, post.rootId || post.id).observeCount();
const isCustomStatusEnabled = observeConfigBooleanValue(database, 'EnableCustomUserStatuses');
const rootPostAuthor = differentThreadSequence ? post.root.observe().pipe(switchMap((root) => {
if (root.length) {
return root[0].author.observe();
@@ -46,6 +47,7 @@ const withHeaderProps = withObservables(
author,
commentCount,
enablePostUsernameOverride,
isCustomStatusEnabled,
isMilitaryTime,
isTimezoneEnabled,
rootPostAuthor,

View File

@@ -3,15 +3,15 @@
exports[`renderSystemMessage uses renderer for Channel Display Name update 1`] = `
<View
style={
Object {
{
"marginBottom": 5,
}
}
>
<View
style={
Array [
Object {
[
{
"alignItems": "flex-start",
"flexDirection": "row",
"flexWrap": "wrap",
@@ -23,29 +23,29 @@ exports[`renderSystemMessage uses renderer for Channel Display Name update 1`] =
<Text>
<Text
style={
Array [
Object {
[
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
{
"opacity": 1,
},
]
}
>
<Text
style={Array []}
style={[]}
>
@username
</Text>
</Text>
<Text
style={
Object {
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
@@ -65,15 +65,15 @@ exports[`renderSystemMessage uses renderer for Channel Display Name update 1`] =
exports[`renderSystemMessage uses renderer for Channel Header update 1`] = `
<View
style={
Object {
{
"marginBottom": 5,
}
}
>
<View
style={
Array [
Object {
[
{
"alignItems": "flex-start",
"flexDirection": "row",
"flexWrap": "wrap",
@@ -85,29 +85,29 @@ exports[`renderSystemMessage uses renderer for Channel Header update 1`] = `
<Text>
<Text
style={
Array [
Object {
[
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
{
"opacity": 1,
},
]
}
>
<Text
style={Array []}
style={[]}
>
@username
</Text>
</Text>
<Text
style={
Object {
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
@@ -127,7 +127,7 @@ exports[`renderSystemMessage uses renderer for Channel Header update 1`] = `
exports[`renderSystemMessage uses renderer for Channel Purpose update 1`] = `
<Text
style={
Object {
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
@@ -143,15 +143,15 @@ exports[`renderSystemMessage uses renderer for Channel Purpose update 1`] = `
exports[`renderSystemMessage uses renderer for Guest added and join to channel 1`] = `
<View
style={
Object {
{
"marginBottom": 5,
}
}
>
<View
style={
Array [
Object {
[
{
"alignItems": "flex-start",
"flexDirection": "row",
"flexWrap": "wrap",
@@ -163,29 +163,29 @@ exports[`renderSystemMessage uses renderer for Guest added and join to channel 1
<Text>
<Text
style={
Array [
Object {
[
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
{
"opacity": 1,
},
]
}
>
<Text
style={Array []}
style={[]}
>
@username
</Text>
</Text>
<Text
style={
Object {
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
@@ -205,15 +205,15 @@ exports[`renderSystemMessage uses renderer for Guest added and join to channel 1
exports[`renderSystemMessage uses renderer for Guest added and join to channel 2`] = `
<View
style={
Object {
{
"marginBottom": 5,
}
}
>
<View
style={
Array [
Object {
[
{
"alignItems": "flex-start",
"flexDirection": "row",
"flexWrap": "wrap",
@@ -225,29 +225,29 @@ exports[`renderSystemMessage uses renderer for Guest added and join to channel 2
<Text>
<Text
style={
Array [
Object {
[
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
{
"opacity": 1,
},
]
}
>
<Text
style={Array []}
style={[]}
>
@other.user
</Text>
</Text>
<Text
style={
Object {
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
@@ -261,22 +261,22 @@ exports[`renderSystemMessage uses renderer for Guest added and join to channel 2
</Text>
<Text
style={
Array [
Object {
[
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
{
"opacity": 1,
},
]
}
>
<Text
style={Array []}
style={[]}
>
@username.
</Text>
@@ -289,15 +289,15 @@ exports[`renderSystemMessage uses renderer for Guest added and join to channel 2
exports[`renderSystemMessage uses renderer for OLD archived channel without a username 1`] = `
<View
style={
Object {
{
"marginBottom": 5,
}
}
>
<View
style={
Array [
Object {
[
{
"alignItems": "flex-start",
"flexDirection": "row",
"flexWrap": "wrap",
@@ -309,7 +309,7 @@ exports[`renderSystemMessage uses renderer for OLD archived channel without a us
<Text>
<Text
style={
Object {
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
@@ -329,15 +329,15 @@ exports[`renderSystemMessage uses renderer for OLD archived channel without a us
exports[`renderSystemMessage uses renderer for archived channel 1`] = `
<View
style={
Object {
{
"marginBottom": 5,
}
}
>
<View
style={
Array [
Object {
[
{
"alignItems": "flex-start",
"flexDirection": "row",
"flexWrap": "wrap",
@@ -349,29 +349,29 @@ exports[`renderSystemMessage uses renderer for archived channel 1`] = `
<Text>
<Text
style={
Array [
Object {
[
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
{
"opacity": 1,
},
]
}
>
<Text
style={Array []}
style={[]}
>
@username
</Text>
</Text>
<Text
style={
Object {
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
@@ -391,15 +391,15 @@ exports[`renderSystemMessage uses renderer for archived channel 1`] = `
exports[`renderSystemMessage uses renderer for unarchived channel 1`] = `
<View
style={
Object {
{
"marginBottom": 5,
}
}
>
<View
style={
Array [
Object {
[
{
"alignItems": "flex-start",
"flexDirection": "row",
"flexWrap": "wrap",
@@ -411,29 +411,29 @@ exports[`renderSystemMessage uses renderer for unarchived channel 1`] = `
<Text>
<Text
style={
Array [
Object {
[
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
{
"opacity": 1,
},
]
}
>
<Text
style={Array []}
style={[]}
>
@username
</Text>
</Text>
<Text
style={
Object {
{
"color": "rgba(63,67,80,0.6)",
"fontFamily": "OpenSans",
"fontSize": 16,

View File

@@ -3,8 +3,8 @@
exports[`ThreadOverview should match snapshot when post is not saved and 0 replies 1`] = `
<View
style={
Array [
Object {
[
{
"borderBottomWidth": 1,
"borderColor": "rgba(63,67,80,0.1)",
"borderTopWidth": 1,
@@ -13,7 +13,7 @@ exports[`ThreadOverview should match snapshot when post is not saved and 0 repli
"paddingHorizontal": 16,
"paddingVertical": 10,
},
Object {
{
"borderBottomWidth": 0,
},
undefined,
@@ -23,14 +23,14 @@ exports[`ThreadOverview should match snapshot when post is not saved and 0 repli
>
<View
style={
Object {
{
"flex": 1,
}
}
>
<Text
style={
Object {
{
"color": "rgba(63,67,80,0.64)",
"fontFamily": "OpenSans",
"fontSize": 16,
@@ -46,13 +46,15 @@ exports[`ThreadOverview should match snapshot when post is not saved and 0 repli
</View>
<View
style={
Object {
{
"flexDirection": "row",
}
}
>
<RNGestureHandlerButton
collapsable={false}
delayLongPress={600}
enabled={true}
exclusive={true}
handlerTag={1}
handlerType="NativeViewGestureHandler"
@@ -68,7 +70,7 @@ exports[`ThreadOverview should match snapshot when post is not saved and 0 repli
accessible={true}
collapsable={false}
style={
Object {
{
"marginLeft": 16,
"opacity": 1,
}
@@ -83,6 +85,8 @@ exports[`ThreadOverview should match snapshot when post is not saved and 0 repli
</RNGestureHandlerButton>
<RNGestureHandlerButton
collapsable={false}
delayLongPress={600}
enabled={true}
exclusive={true}
handlerTag={2}
handlerType="NativeViewGestureHandler"
@@ -98,7 +102,7 @@ exports[`ThreadOverview should match snapshot when post is not saved and 0 repli
accessible={true}
collapsable={false}
style={
Object {
{
"marginLeft": 16,
"opacity": 1,
}
@@ -118,8 +122,8 @@ exports[`ThreadOverview should match snapshot when post is not saved and 0 repli
exports[`ThreadOverview should match snapshot when post is saved and has replies 1`] = `
<View
style={
Array [
Object {
[
{
"borderBottomWidth": 1,
"borderColor": "rgba(63,67,80,0.1)",
"borderTopWidth": 1,
@@ -135,14 +139,14 @@ exports[`ThreadOverview should match snapshot when post is saved and has replies
>
<View
style={
Object {
{
"flex": 1,
}
}
>
<Text
style={
Object {
{
"color": "rgba(63,67,80,0.64)",
"fontFamily": "OpenSans",
"fontSize": 16,
@@ -158,13 +162,15 @@ exports[`ThreadOverview should match snapshot when post is saved and has replies
</View>
<View
style={
Object {
{
"flexDirection": "row",
}
}
>
<RNGestureHandlerButton
collapsable={false}
delayLongPress={600}
enabled={true}
exclusive={true}
handlerTag={3}
handlerType="NativeViewGestureHandler"
@@ -180,7 +186,7 @@ exports[`ThreadOverview should match snapshot when post is saved and has replies
accessible={true}
collapsable={false}
style={
Object {
{
"marginLeft": 16,
"opacity": 1,
}
@@ -195,6 +201,8 @@ exports[`ThreadOverview should match snapshot when post is saved and has replies
</RNGestureHandlerButton>
<RNGestureHandlerButton
collapsable={false}
delayLongPress={600}
enabled={true}
exclusive={true}
handlerTag={4}
handlerType="NativeViewGestureHandler"
@@ -210,7 +218,7 @@ exports[`ThreadOverview should match snapshot when post is saved and has replies
accessible={true}
collapsable={false}
style={
Object {
{
"marginLeft": 16,
"opacity": 1,
}

View File

@@ -90,7 +90,7 @@ const Search = forwardRef<SearchRef, SearchProps>((props: SearchProps, ref) => {
const onClear = useCallback(() => {
setValue('');
props.onClear?.();
}, []);
}, [props.onClear]);
const onChangeText = useCallback((text: string) => {
setValue(text);
@@ -105,8 +105,8 @@ const Search = forwardRef<SearchRef, SearchProps>((props: SearchProps, ref) => {
}), [theme]);
useEffect(() => {
setValue(props.defaultValue || value || '');
}, [props.defaultValue]);
setValue(props.defaultValue || props.value || '');
}, [props.defaultValue, props.value]);
const clearIcon = (
<CompassIcon

View File

@@ -4,7 +4,7 @@ exports[`Server Icon Server Icon Component should match snapshot 1`] = `
<View>
<View
accessibilityState={
Object {
{
"disabled": true,
}
}
@@ -12,7 +12,7 @@ exports[`Server Icon Server Icon Component should match snapshot 1`] = `
collapsable={false}
focusable={false}
hitSlop={
Object {
{
"bottom": 5,
"left": 40,
"right": 20,
@@ -27,7 +27,7 @@ exports[`Server Icon Server Icon Component should match snapshot 1`] = `
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
{
"opacity": 1,
}
}
@@ -45,7 +45,7 @@ exports[`Server Icon Server Icon Component should match snapshot with mentions 1
<View>
<View
accessibilityState={
Object {
{
"disabled": true,
}
}
@@ -53,7 +53,7 @@ exports[`Server Icon Server Icon Component should match snapshot with mentions 1
collapsable={false}
focusable={false}
hitSlop={
Object {
{
"bottom": 5,
"left": 40,
"right": 20,
@@ -68,7 +68,7 @@ exports[`Server Icon Server Icon Component should match snapshot with mentions 1
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
{
"opacity": 1,
}
}
@@ -83,7 +83,7 @@ exports[`Server Icon Server Icon Component should match snapshot with mentions 1
collapsable={false}
numberOfLines={1}
style={
Object {
{
"alignSelf": "flex-end",
"backgroundColor": "#ffffff",
"borderColor": "#14213e",
@@ -102,8 +102,8 @@ exports[`Server Icon Server Icon Component should match snapshot with mentions 1
"position": "absolute",
"textAlign": "center",
"top": -8,
"transform": Array [
Object {
"transform": [
{
"scale": 1,
},
],
@@ -121,7 +121,7 @@ exports[`Server Icon Server Icon Component should match snapshot with unreads 1`
<View>
<View
accessibilityState={
Object {
{
"disabled": true,
}
}
@@ -129,7 +129,7 @@ exports[`Server Icon Server Icon Component should match snapshot with unreads 1`
collapsable={false}
focusable={false}
hitSlop={
Object {
{
"bottom": 5,
"left": 40,
"right": 20,
@@ -144,7 +144,7 @@ exports[`Server Icon Server Icon Component should match snapshot with unreads 1`
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
{
"opacity": 1,
}
}
@@ -159,7 +159,7 @@ exports[`Server Icon Server Icon Component should match snapshot with unreads 1`
collapsable={false}
numberOfLines={1}
style={
Object {
{
"alignSelf": "flex-end",
"backgroundColor": "#ffffff",
"borderColor": "#14213e",
@@ -178,8 +178,8 @@ exports[`Server Icon Server Icon Component should match snapshot with unreads 1`
"position": "absolute",
"textAlign": "center",
"top": -5,
"transform": Array [
Object {
"transform": [
{
"scale": 1,
},
],

View File

@@ -5,6 +5,7 @@ import React, {useMemo} from 'react';
import {StyleProp, Text, TextStyle, useWindowDimensions, View, ViewStyle} from 'react-native';
import Animated, {AnimatedStyleProp} from 'react-native-reanimated';
import {useIsTablet} from '@app/hooks/device';
import CompassIcon from '@components/compass_icon';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -20,6 +21,9 @@ type ToastProps = {
}
export const TOAST_HEIGHT = 56;
const TOAST_MARGIN = 40;
const WIDTH_TABLET = 484;
const WIDTH_MOBILE = 400;
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
center: {
@@ -53,10 +57,10 @@ const Toast = ({animatedStyle, children, style, iconName, message, textStyle}: T
const theme = useTheme();
const styles = getStyleSheet(theme);
const dim = useWindowDimensions();
const isTablet = useIsTablet();
const containerStyle = useMemo(() => {
const totalMargin = 40;
const width = Math.min(dim.height, dim.width, 400) - totalMargin;
const toast_width = isTablet ? WIDTH_TABLET : WIDTH_MOBILE;
const width = Math.min(dim.height, dim.width, toast_width) - TOAST_MARGIN;
return [styles.container, {width}, style];
}, [dim, styles.container, style]);

View File

@@ -4,7 +4,7 @@ exports[`UserStatus should match snapshot, away status 1`] = `
<Icon
name="clock"
style={
Object {
{
"color": "#ffbc1f",
"fontSize": 32,
}
@@ -17,7 +17,7 @@ exports[`UserStatus should match snapshot, dnd status 1`] = `
<Icon
name="minus-circle"
style={
Object {
{
"color": "#d24b4e",
"fontSize": 32,
}
@@ -30,7 +30,7 @@ exports[`UserStatus should match snapshot, online status 1`] = `
<Icon
name="check-circle"
style={
Object {
{
"color": "#3db887",
"fontSize": 32,
}
@@ -43,7 +43,7 @@ exports[`UserStatus should match snapshot, should default to offline status 1`]
<Icon
name="circle-outline"
style={
Object {
{
"color": "rgba(184,184,184,0.64)",
"fontSize": 32,
}

View File

@@ -149,6 +149,12 @@ export const SCREENS_WITH_TRANSPARENT_BACKGROUND = new Set<string>([
USER_PROFILE,
]);
export const OVERLAY_SCREENS = new Set<string>([
IN_APP_NOTIFICATION,
GALLERY,
SNACK_BAR,
]);
export const NOT_READY = [
CHANNEL_ADD_PEOPLE,
CHANNEL_MENTION,

View File

@@ -19,8 +19,8 @@ type State = {
serverDisplayName: string;
};
export function withServerDatabase<T>(Component: ComponentType<T>): ComponentType<T> {
return function ServerDatabaseComponent(props) {
export function withServerDatabase<T extends JSX.IntrinsicAttributes>(Component: ComponentType<T>): ComponentType<T> {
return function ServerDatabaseComponent(props: T) {
const [state, setState] = useState<State | undefined>();
const observer = (servers: ServersModel[]) => {

View File

@@ -113,7 +113,6 @@ class PushNotifications {
const screen = Screens.IN_APP_NOTIFICATION;
const passProps = {
notification,
overlay: true,
serverName,
serverUrl,
};

View File

@@ -2,14 +2,19 @@
// See LICENSE.txt for license information.
import {Alert, DeviceEventEmitter, Linking} from 'react-native';
import RNLocalize from 'react-native-localize';
import semver from 'semver';
import {autoUpdateTimezone} from '@actions/remote/user';
import LocalConfig from '@assets/config.json';
import {Events, Sso} from '@constants';
import DatabaseManager from '@database/manager';
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
import {getServerCredentials} from '@init/credentials';
import {getLaunchPropsFromDeepLink, relaunchApp} from '@init/launch';
import * as analytics from '@managers/analytics';
import {queryAllServers} from '@queries/app/servers';
import {logError} from '@utils/log';
import type {jsAndNativeErrorHandler} from '@typings/global/error_handling';
@@ -21,6 +26,19 @@ class GlobalEventHandler {
constructor() {
DeviceEventEmitter.addListener(Events.SERVER_VERSION_CHANGED, this.onServerVersionChanged);
DeviceEventEmitter.addListener(Events.CONFIG_CHANGED, this.onServerConfigChanged);
RNLocalize.addEventListener('change', async () => {
try {
const {database} = DatabaseManager.getAppDatabaseAndOperator();
const servers = await queryAllServers(database);
for (const server of servers) {
if (server.url && server.lastActiveAt > 0) {
autoUpdateTimezone(server.url);
}
}
} catch (e) {
logError('Localize change', e);
}
});
Linking.addEventListener('url', this.onDeepLink);
}

View File

@@ -231,15 +231,15 @@ export async function newConnection(serverUrl: string, channelID: string, closeC
ws.on('message', ({data}: { data: string }) => {
const msg = JSON.parse(data);
if (msg.type === 'answer' || msg.type === 'offer') {
if (msg.type === 'answer' || msg.type === 'candidate' || msg.type === 'offer') {
peer?.signal(data);
}
});
const waitForPeerConnection = () => {
const waitForReadyImpl = (callback: () => void, fail: () => void, timeout: number) => {
const waitForReadyImpl = (callback: () => void, fail: (reason: string) => void, timeout: number) => {
if (timeout <= 0) {
fail();
fail('timed out waiting for peer connection');
return;
}
setTimeout(() => {

View File

@@ -131,6 +131,12 @@ export const userLeftCall = (serverUrl: string, channelId: string, userId: strin
return;
}
// Was the user me?
if (userId === callsState.myUserId) {
myselfLeftCall();
return;
}
const nextCurrentCall = {
...currentCall,
participants: {...currentCall.participants},

View File

@@ -251,7 +251,7 @@ export const getDefaultChannelForTeam = async (database: Database, teamId: strin
const roles = await queryRoles(database).fetch();
if (roles.length) {
canIJoinPublicChannelsInTeam = hasPermission(roles, Permissions.JOIN_PUBLIC_CHANNELS, true);
canIJoinPublicChannelsInTeam = hasPermission(roles, Permissions.JOIN_PUBLIC_CHANNELS);
}
const myChannels = await database.get<ChannelModel>(CHANNEL).query(
@@ -605,3 +605,61 @@ export const observeChannelsByLastPostAt = (database: Database, myChannels: MyCh
ORDER BY CASE mc.last_post_at WHEN 0 THEN c.create_at ELSE mc.last_post_at END DESC`),
).observe();
};
export const queryChannelsForAutocomplete = (database: Database, matchTerm: string, isSearch: boolean, teamId: string) => {
const likeTerm = `%${Q.sanitizeLikeString(matchTerm)}%`;
const clauses: Q.Clause[] = [];
if (isSearch) {
clauses.push(
Q.experimentalJoinTables([CHANNEL_MEMBERSHIP]),
Q.experimentalNestedJoin(CHANNEL_MEMBERSHIP, USER),
);
}
const orConditions: Q.Condition[] = [
Q.where('display_name', Q.like(matchTerm)),
Q.where('name', Q.like(likeTerm)),
];
if (isSearch) {
orConditions.push(
Q.and(
Q.where('type', Q.oneOf([General.DM_CHANNEL, General.GM_CHANNEL])),
Q.on(CHANNEL_MEMBERSHIP, Q.on(USER,
Q.or(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: condition type error
Q.unsafeSqlExpr(`first_name || ' ' || last_name LIKE '${likeTerm}'`),
Q.where('nickname', Q.like(likeTerm)),
Q.where('email', Q.like(likeTerm)),
Q.where('username', Q.like(likeTerm)),
),
)),
),
);
}
const teamsToSearch = [teamId];
if (isSearch) {
teamsToSearch.push('');
}
const andConditions: Q.Condition[] = [
Q.where('team_id', Q.oneOf(teamsToSearch)),
];
if (!isSearch) {
andConditions.push(
Q.where('type', Q.oneOf([General.OPEN_CHANNEL, General.PRIVATE_CHANNEL])),
Q.where('delete_at', 0),
);
}
clauses.push(
...andConditions,
Q.or(...orConditions),
Q.sortBy('display_name', Q.asc),
Q.sortBy('name', Q.asc),
Q.take(25),
);
return database.get<ChannelModel>(CHANNEL).query(...clauses);
};

View File

@@ -53,7 +53,7 @@ export function observePermissionForChannel(database: Database, channel: Channel
rolesArray.push(...mt.roles.split(' '));
}
return queryRolesByNames(database, rolesArray).observeWithColumns(['permissions']).pipe(
switchMap((r) => of$(hasPermission(r, permission, defaultValue))),
switchMap((r) => of$(hasPermission(r, permission))),
);
}),
distinctUntilChanged(),
@@ -74,7 +74,7 @@ export function observePermissionForTeam(database: Database, team: TeamModel | u
}
return queryRolesByNames(database, rolesArray).observeWithColumns(['permissions']).pipe(
switchMap((roles) => of$(hasPermission(roles, permission, defaultValue))),
switchMap((roles) => of$(hasPermission(roles, permission))),
);
}),
distinctUntilChanged(),

View File

@@ -77,7 +77,6 @@ type Props = {
closeButton: ImageResource;
// Properties not changing during the lifetime of the screen)
currentUserId: string;
currentTeamId: string;
// Calculated Props
@@ -102,7 +101,6 @@ export default function BrowseChannels(props: Props) {
canCreateChannels,
sharedChannelsEnabled,
closeButton,
currentUserId,
currentTeamId,
canShowArchivedChannels,
typeOfChannels,
@@ -137,7 +135,7 @@ export default function BrowseChannels(props: Props) {
setHeaderButtons(false);
setAdding(true);
const result = await joinChannel(serverUrl, currentUserId, currentTeamId, channel.id, '', false);
const result = await joinChannel(serverUrl, currentTeamId, channel.id, '', false);
if (result.error) {
alertErrorWithFallback(

View File

@@ -39,11 +39,10 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
switchMap((values) => queryRolesByNames(database, values).observeWithColumns(['permissions'])),
);
const canCreateChannels = roles.pipe(switchMap((r) => of$(hasPermission(r, Permissions.CREATE_PUBLIC_CHANNEL, false))));
const canCreateChannels = roles.pipe(switchMap((r) => of$(hasPermission(r, Permissions.CREATE_PUBLIC_CHANNEL))));
return {
canCreateChannels,
currentUserId,
currentTeamId,
joinedChannels,
sharedChannelsEnabled,

View File

@@ -218,7 +218,7 @@ export default function SearchHandler(props: Props) {
clearTimeout(searchTimeout.current);
}
searchTimeout.current = setTimeout(async () => {
const results = await searchChannels(serverUrl, text);
const results = await searchChannels(serverUrl, text, currentTeamId);
if (results.channels) {
setSearchResults(results.channels);
}

View File

@@ -87,11 +87,11 @@ const PrivateChannelIllustration = ({theme}: Props) => (
/>
<Mask
id='a'
// @ts-expect-error style not intrinsic
style={{
maskType: 'alpha',
}}
// @ts-expect-error string instead of enum
maskUnits='userSpaceOnUse'
x={51}
y={63}
@@ -132,11 +132,11 @@ const PrivateChannelIllustration = ({theme}: Props) => (
/>
<Mask
id='b'
// @ts-expect-error style not intrinsic
style={{
maskType: 'alpha',
}}
// @ts-expect-error string instead of enum
maskUnits='userSpaceOnUse'
x={41}
y={35}

View File

@@ -103,11 +103,11 @@ const PublicChannelIllustration = ({theme}: Props) => (
/>
<Mask
id='a'
// @ts-expect-error style not intrinsic
style={{
maskType: 'alpha',
}}
// @ts-expect-error string instead of enum
maskUnits='userSpaceOnUse'
x={77}
y={31}

View File

@@ -47,11 +47,11 @@ const TownSquareIllustration = ({theme}: Props) => (
/>
<Mask
id='a'
// @ts-expect-error style not intrinsic
style={{
maskType: 'alpha',
}}
// @ts-expect-error string instead of enum
maskUnits='userSpaceOnUse'
x={61}
y={65}
@@ -117,11 +117,11 @@ const TownSquareIllustration = ({theme}: Props) => (
/>
<Mask
id='b'
// @ts-expect-error style not intrinsic
style={{
maskType: 'alpha',
}}
// @ts-expect-error string instead of enum
maskUnits='userSpaceOnUse'
x={40}
y={33}
@@ -159,11 +159,11 @@ const TownSquareIllustration = ({theme}: Props) => (
/>
<Mask
id='c'
// @ts-expect-error style not intrinsic
style={{
maskType: 'alpha',
}}
// @ts-expect-error string instead of enum
maskUnits='userSpaceOnUse'
x={69}
y={23}

View File

@@ -74,12 +74,12 @@ const PublicOrPrivateChannel = ({channel, creator, roles, theme}: Props) => {
const canManagePeople = useMemo(() => {
const permission = channel.type === General.OPEN_CHANNEL ? Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS : Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS;
return hasPermission(roles, permission, false);
return hasPermission(roles, permission);
}, [channel.type, roles]);
const canSetHeader = useMemo(() => {
const permission = channel.type === General.OPEN_CHANNEL ? Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES : Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES;
return hasPermission(roles, permission, false);
return hasPermission(roles, permission);
}, [channel.type, roles]);
const createdBy = useMemo(() => {

View File

@@ -60,7 +60,7 @@ const TownSquare = ({channelId, displayName, roles, theme}: Props) => {
/>
<IntroOptions
channelId={channelId}
header={hasPermission(roles, Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES, false)}
header={hasPermission(roles, Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES)}
/>
</View>
);

View File

@@ -31,6 +31,7 @@ type ChannelProps = {
channelId: string;
channelType: ChannelType;
customStatus?: UserCustomStatus;
isCustomStatusEnabled: boolean;
isCustomStatusExpired: boolean;
componentId?: string;
displayName: string;
@@ -66,7 +67,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
const ChannelHeader = ({
channelId, channelType, componentId, customStatus, displayName,
isCustomStatusExpired, isOwnDirectMessage, memberCount,
isCustomStatusEnabled, isCustomStatusExpired, isOwnDirectMessage, memberCount,
searchTerm, teamId, callsEnabledInChannel, callsFeatureRestricted,
}: ChannelProps) => {
const intl = useIntl();
@@ -194,7 +195,7 @@ const ChannelHeader = ({
} else if (customStatus && customStatus.text) {
return (
<View style={styles.customStatusContainer}>
{Boolean(customStatus.emoji) &&
{isCustomStatusEnabled && Boolean(customStatus.emoji) &&
<CustomStatusEmoji
customStatus={customStatus}
emojiSize={13}

View File

@@ -9,7 +9,7 @@ import {combineLatestWith, switchMap} from 'rxjs/operators';
import {observeIsCallsFeatureRestricted} from '@calls/observers';
import {General} from '@constants';
import {observeChannel, observeChannelInfo} from '@queries/servers/channel';
import {observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
import {observeConfigBooleanValue, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
import {observeUser} from '@queries/servers/user';
import {
getUserCustomStatus,
@@ -60,6 +60,8 @@ const enhanced = withObservables(['channelId'], ({serverUrl, channelId, database
switchMap((dm) => of$(checkCustomStatusIsExpired(dm))),
);
const isCustomStatusEnabled = observeConfigBooleanValue(database, 'EnableCustomUserStatuses');
const searchTerm = channel.pipe(
combineLatestWith(dmUser),
switchMap(([c, dm]) => {
@@ -82,6 +84,7 @@ const enhanced = withObservables(['channelId'], ({serverUrl, channelId, database
channelType,
customStatus,
displayName,
isCustomStatusEnabled,
isCustomStatusExpired,
isOwnDirectMessage,
memberCount,

View File

@@ -22,6 +22,7 @@ type Props = {
createdBy: string;
customStatus?: UserCustomStatus;
header?: string;
isCustomStatusEnabled: boolean;
}
const headerMetadata = {header: {width: 1, height: 1}};
@@ -65,7 +66,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const Extra = ({channelId, createdAt, createdBy, customStatus, header}: Props) => {
const Extra = ({channelId, createdAt, createdBy, customStatus, header, isCustomStatusEnabled}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const blockStyles = getMarkdownBlockStyles(theme);
@@ -82,7 +83,7 @@ const Extra = ({channelId, createdAt, createdBy, customStatus, header}: Props) =
return (
<View style={styles.container}>
{Boolean(customStatus) &&
{isCustomStatusEnabled && Boolean(customStatus) &&
<View style={styles.item}>
<FormattedText
id='channel_info.custom_status'

View File

@@ -8,6 +8,7 @@ import {combineLatestWith, switchMap} from 'rxjs/operators';
import {General} from '@constants';
import {observeChannel, observeChannelInfo} from '@queries/servers/channel';
import {observeConfigBooleanValue} from '@queries/servers/system';
import {observeCurrentUser, observeTeammateNameDisplay, observeUser} from '@queries/servers/user';
import {displayUsername, getUserCustomStatus, getUserIdFromChannelName, isCustomStatusExpired as checkCustomStatusIsExpired} from '@utils/user';
@@ -48,11 +49,14 @@ const enhanced = withObservables(['channelId'], ({channelId, database}: Props) =
switchMap((dm) => of$(checkCustomStatusIsExpired(dm) ? undefined : getUserCustomStatus(dm))),
);
const isCustomStatusEnabled = observeConfigBooleanValue(database, 'EnableCustomUserStatuses');
return {
createdAt,
createdBy,
customStatus,
header,
isCustomStatusEnabled,
};
});

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-community/clipboard';
import Clipboard from '@react-native-clipboard/clipboard';
import React, {useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {StyleSheet} from 'react-native';
@@ -16,6 +16,7 @@ import {useServerUrl} from '@context/server';
import type {GalleryAction, GalleryItemType} from '@typings/screens/gallery';
type Props = {
galleryView?: boolean;
item: GalleryItemType;
setAction: (action: GalleryAction) => void;
}
@@ -29,7 +30,7 @@ const styles = StyleSheet.create({
},
});
const CopyPublicLink = ({item, setAction}: Props) => {
const CopyPublicLink = ({item, galleryView = true, setAction}: Props) => {
const {formatMessage} = useIntl();
const serverUrl = useServerUrl();
const insets = useSafeAreaInsets();
@@ -37,11 +38,14 @@ const CopyPublicLink = ({item, setAction}: Props) => {
const [error, setError] = useState('');
const mounted = useRef(false);
const animatedStyle = useAnimatedStyle(() => ({
position: 'absolute',
bottom: GALLERY_FOOTER_HEIGHT + 8 + insets.bottom,
opacity: withTiming(showToast ? 1 : 0, {duration: 300}),
}));
const animatedStyle = useAnimatedStyle(() => {
const marginBottom = galleryView ? GALLERY_FOOTER_HEIGHT + 8 : 0;
return {
position: 'absolute',
bottom: insets.bottom + marginBottom,
opacity: withTiming(showToast ? 1 : 0, {duration: 300}),
};
});
const copyLink = async () => {
try {
@@ -87,7 +91,7 @@ const CopyPublicLink = ({item, setAction}: Props) => {
animatedStyle={animatedStyle}
style={error ? styles.error : styles.toast}
message={error || formatMessage({id: 'public_link_copied', defaultMessage: 'Link copied to clipboard'})}
iconName='check'
iconName='link-variant'
/>
);
};

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import CameraRoll from '@react-native-community/cameraroll';
import {CameraRoll} from '@react-native-camera-roll/camera-roll';
import React, {useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {NativeModules, Platform, StyleSheet, Text, View} from 'react-native';
@@ -29,6 +29,7 @@ import type {GalleryAction, GalleryItemType} from '@typings/screens/gallery';
type Props = {
action: GalleryAction;
galleryView?: boolean;
item: GalleryItemType;
setAction: (action: GalleryAction) => void;
onDownloadSuccess?: (path: string) => void;
@@ -65,7 +66,7 @@ const styles = StyleSheet.create({
},
});
const DownloadWithAction = ({action, item, onDownloadSuccess, setAction}: Props) => {
const DownloadWithAction = ({action, item, onDownloadSuccess, setAction, galleryView = true}: Props) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const insets = useSafeAreaInsets();
@@ -111,11 +112,14 @@ const DownloadWithAction = ({action, item, onDownloadSuccess, setAction}: Props)
}
}
const animatedStyle = useAnimatedStyle(() => ({
position: 'absolute',
bottom: GALLERY_FOOTER_HEIGHT + 8 + insets.bottom,
opacity: withTiming(showToast ? 1 : 0, {duration: 300}),
}));
const animatedStyle = useAnimatedStyle(() => {
const marginBottom = galleryView ? GALLERY_FOOTER_HEIGHT + 8 : 0;
return {
position: 'absolute',
bottom: insets.bottom + marginBottom,
opacity: withTiming(showToast ? 1 : 0, {duration: 300}),
};
});
const cancel = async () => {
try {

View File

@@ -1,10 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useState} from 'react';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {DeviceEventEmitter, StyleProp, StyleSheet, View, ViewStyle} from 'react-native';
import Animated from 'react-native-reanimated';
import {SafeAreaView, Edge} from 'react-native-safe-area-context';
import {SafeAreaView, Edge, useSafeAreaInsets} from 'react-native-safe-area-context';
import {Events} from '@constants';
import {GALLERY_FOOTER_HEIGHT} from '@constants/gallery';
@@ -39,11 +39,11 @@ type Props = {
}
const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
const edges: Edge[] = ['left', 'right', 'bottom'];
const edges: Edge[] = ['left', 'right'];
const styles = StyleSheet.create({
container: {
alignItems: 'center',
backgroundColor: changeOpacity('#000', 0.6),
backgroundColor: '#000',
borderTopColor: changeOpacity('#fff', 0.4),
borderTopWidth: 1,
flex: 1,
@@ -71,6 +71,9 @@ const Footer = ({
}: Props) => {
const showActions = !hideActions && Boolean(item.id) && !item.id?.startsWith('uid');
const [action, setAction] = useState<GalleryAction>('none');
const {bottom} = useSafeAreaInsets();
const bottomStyle = useMemo(() => ({height: bottom, backgroundColor: '#000'}), [bottom]);
let overrideIconUrl;
if (enablePostIconOverride && post?.props?.use_user_icon !== 'true' && post?.props?.override_icon_url) {
@@ -152,6 +155,7 @@ const Footer = ({
/>
}
</View>
<View style={bottomStyle}/>
</AnimatedSafeAreaView>
);
};

View File

@@ -5,7 +5,7 @@ import React, {useMemo} from 'react';
import {StyleProp, StyleSheet, useWindowDimensions, View, ViewStyle} from 'react-native';
import {TouchableOpacity} from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
import {SafeAreaView, Edge} from 'react-native-safe-area-context';
import {SafeAreaView, Edge, useSafeAreaInsets} from 'react-native-safe-area-context';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
@@ -23,7 +23,7 @@ type Props = {
const styles = StyleSheet.create({
container: {
alignItems: 'center',
backgroundColor: changeOpacity('#000', 0.6),
backgroundColor: '#000',
borderBottomColor: changeOpacity('#fff', 0.4),
borderBottomWidth: 1,
flexDirection: 'row',
@@ -39,12 +39,14 @@ const styles = StyleSheet.create({
},
});
const edges: Edge[] = ['left', 'right', 'top'];
const edges: Edge[] = ['left', 'right'];
const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
const Header = ({index, onClose, style, total}: Props) => {
const {width} = useWindowDimensions();
const height = useDefaultHeaderHeight();
const {top} = useSafeAreaInsets();
const topContainerStyle = useMemo(() => [{height: top, backgroundColor: '#000'}], [top]);
const containerStyle = useMemo(() => [styles.container, {height}], [height]);
const iconStyle = useMemo(() => [{width: height}, styles.icon], [height]);
const titleStyle = useMemo(() => ({width: width - (height * 2)}), [height, width]);
@@ -55,6 +57,7 @@ const Header = ({index, onClose, style, total}: Props) => {
edges={edges}
style={style}
>
<Animated.View style={topContainerStyle}/>
<Animated.View style={containerStyle}>
<TouchableOpacity
onPress={onClose}

View File

@@ -172,10 +172,7 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
}, [isPageActive.value, paused]);
return (
<Animated.View
onTouchStart={handleTouchStart}
style={styles.video}
>
<>
<AnimatedVideo
ref={videoRef}
source={source}
@@ -189,17 +186,18 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
onFullscreenPlayerWillPresent={onFullscreenPlayerWillPresent}
onReadyForDisplay={onReadyForDisplay}
onEnd={onEnd}
onTouchStart={handleTouchStart}
/>
{Platform.OS === 'android' && paused && videoReady &&
<Animated.View style={styles.playContainer}>
<CompassIcon
color={changeOpacity('#fff', 0.8)}
style={styles.play}
name='play'
onPress={onPlay}
size={80}
/>
</Animated.View>
<Animated.View style={styles.playContainer}>
<CompassIcon
color={changeOpacity('#fff', 0.8)}
style={styles.play}
name='play'
onPress={onPlay}
size={80}
/>
</Animated.View>
}
{downloading &&
<DownloadWithAction
@@ -209,7 +207,7 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
item={item}
/>
}
</Animated.View>
</>
);
};

View File

@@ -1,13 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useIsFocused, useRoute} from '@react-navigation/native';
import {useRoute} from '@react-navigation/native';
import React, {useCallback, useState} from 'react';
import {ScrollView, View} from 'react-native';
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
import FreezeScreen from '@components/freeze_screen';
import {View as ViewConstants} from '@constants';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
@@ -60,7 +59,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const AccountScreen = ({currentUser, enableCustomUserStatuses, customStatusExpirySupported, showFullName}: AccountScreenProps) => {
const theme = useTheme();
const isFocused = useIsFocused();
const [start, setStart] = useState(false);
const route = useRoute();
const insets = useSafeAreaInsets();
@@ -96,46 +94,44 @@ const AccountScreen = ({currentUser, enableCustomUserStatuses, customStatusExpir
const styles = getStyleSheet(theme);
return (
<FreezeScreen freeze={!isFocused}>
<SafeAreaView
edges={edges}
style={styles.container}
testID='account.screen'
<SafeAreaView
edges={edges}
style={styles.container}
testID='account.screen'
>
<View style={[{height: insets.top, flexDirection: 'row'}]}>
<View style={[styles.container, tabletSidebarStyle]}/>
{isTablet && <View style={styles.tabletContainer}/>}
</View>
<Animated.View
onLayout={onLayout}
style={[styles.flexRow, animated]}
>
<View style={[{height: insets.top, flexDirection: 'row'}]}>
<View style={[styles.container, tabletSidebarStyle]}/>
{isTablet && <View style={styles.tabletContainer}/>}
</View>
<Animated.View
onLayout={onLayout}
style={[styles.flexRow, animated]}
<ScrollView
alwaysBounceVertical={false}
style={tabletSidebarStyle}
contentContainerStyle={styles.totalHeight}
>
<ScrollView
alwaysBounceVertical={false}
style={tabletSidebarStyle}
contentContainerStyle={styles.totalHeight}
>
<AccountUserInfo
user={currentUser}
showFullName={showFullName}
theme={theme}
/>
<AccountOptions
enableCustomUserStatuses={enableCustomUserStatuses}
isCustomStatusExpirySupported={customStatusExpirySupported}
isTablet={isTablet}
user={currentUser}
theme={theme}
/>
</ScrollView>
{isTablet &&
<View style={[styles.tabletContainer, styles.tabletDivider]}>
<AccountTabletView/>
</View>
}
</Animated.View>
</SafeAreaView>
</FreezeScreen>
<AccountUserInfo
user={currentUser}
showFullName={showFullName}
theme={theme}
/>
<AccountOptions
enableCustomUserStatuses={enableCustomUserStatuses}
isCustomStatusExpirySupported={customStatusExpirySupported}
isTablet={isTablet}
user={currentUser}
theme={theme}
/>
</ScrollView>
{isTablet &&
<View style={[styles.tabletContainer, styles.tabletDivider]}>
<AccountTabletView/>
</View>
}
</Animated.View>
</SafeAreaView>
);
};

View File

@@ -3,15 +3,15 @@
exports[`components/categories_list should render channels error 1`] = `
<View
animatedStyle={
Object {
"value": Object {
{
"value": {
"maxWidth": "100%",
},
}
}
collapsable={false}
style={
Object {
{
"backgroundColor": "#1e325c",
"flex": 1,
"maxWidth": "100%",
@@ -23,22 +23,22 @@ exports[`components/categories_list should render channels error 1`] = `
>
<View
animatedStyle={
Object {
"value": Object {
{
"value": {
"marginLeft": 0,
},
}
}
collapsable={false}
style={
Object {
{
"marginLeft": 0,
}
}
>
<View
style={
Object {
{
"alignItems": "center",
"flexDirection": "row",
"justifyContent": "space-between",
@@ -57,14 +57,14 @@ exports[`components/categories_list should render channels error 1`] = `
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
{
"opacity": 1,
}
}
>
<View
style={
Object {
{
"alignItems": "center",
"flexDirection": "row",
"justifyContent": "space-between",
@@ -73,7 +73,7 @@ exports[`components/categories_list should render channels error 1`] = `
>
<Text
style={
Object {
{
"color": "#ffffff",
"fontFamily": "Metropolis-SemiBold",
"fontSize": 28,
@@ -87,7 +87,7 @@ exports[`components/categories_list should render channels error 1`] = `
</Text>
<View
style={
Object {
{
"marginLeft": 4,
}
}
@@ -96,7 +96,7 @@ exports[`components/categories_list should render channels error 1`] = `
<Icon
name="chevron-down"
style={
Object {
{
"color": "rgba(255,255,255,0.8)",
"fontSize": 24,
}
@@ -110,7 +110,7 @@ exports[`components/categories_list should render channels error 1`] = `
collapsable={false}
focusable={true}
hitSlop={
Object {
{
"bottom": 30,
"left": 20,
"right": 20,
@@ -125,7 +125,7 @@ exports[`components/categories_list should render channels error 1`] = `
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
{
"alignItems": "center",
"backgroundColor": "rgba(255,255,255,0.08)",
"borderRadius": 14,
@@ -140,7 +140,7 @@ exports[`components/categories_list should render channels error 1`] = `
<Icon
name="plus"
style={
Object {
{
"color": "rgba(255,255,255,0.8)",
"fontSize": 18,
}
@@ -150,7 +150,7 @@ exports[`components/categories_list should render channels error 1`] = `
</View>
<View
style={
Object {
{
"alignItems": "center",
"flexDirection": "row",
"paddingRight": 60,
@@ -161,7 +161,7 @@ exports[`components/categories_list should render channels error 1`] = `
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
{
"color": "rgba(255,255,255,0.64)",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 11,
@@ -173,11 +173,11 @@ exports[`components/categories_list should render channels error 1`] = `
/>
<View
animatedStyle={
Object {
"value": Object {
{
"value": {
"opacity": 1,
"transform": Array [
Object {
"transform": [
{
"rotateZ": "0deg",
},
],
@@ -186,7 +186,7 @@ exports[`components/categories_list should render channels error 1`] = `
}
collapsable={false}
style={
Object {
{
"borderBottomColor": "rgba(255,255,255,0.16)",
"borderLeftColor": "#ffffff",
"borderRadius": 7,
@@ -196,8 +196,8 @@ exports[`components/categories_list should render channels error 1`] = `
"height": 14,
"marginLeft": 5,
"opacity": 1,
"transform": Array [
Object {
"transform": [
{
"rotateZ": "0deg",
},
],
@@ -209,7 +209,7 @@ exports[`components/categories_list should render channels error 1`] = `
</View>
<View
style={
Object {
{
"alignItems": "center",
"flex": 1,
"justifyContent": "center",
@@ -219,7 +219,7 @@ exports[`components/categories_list should render channels error 1`] = `
>
<View
style={
Object {
{
"alignItems": "center",
"backgroundColor": "rgba(255,255,255,0.08)",
"borderRadius": 60,
@@ -232,7 +232,7 @@ exports[`components/categories_list should render channels error 1`] = `
<Icon
name="alert-circle-outline"
style={
Object {
{
"color": "rgba(255,255,255,0.48)",
"fontSize": 72,
"lineHeight": 72,
@@ -242,14 +242,14 @@ exports[`components/categories_list should render channels error 1`] = `
</View>
<Text
style={
Array [
Object {
[
{
"fontFamily": "Metropolis-SemiBold",
"fontSize": 20,
"fontWeight": "600",
"lineHeight": 28,
},
Object {
{
"color": "#ffffff",
"marginTop": 20,
"textAlign": "center",
@@ -261,14 +261,14 @@ exports[`components/categories_list should render channels error 1`] = `
</Text>
<Text
style={
Array [
Object {
[
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
{
"color": "#ffffff",
"marginTop": 4,
"textAlign": "center",
@@ -290,7 +290,7 @@ exports[`components/categories_list should render channels error 1`] = `
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
{
"alignItems": "center",
"backgroundColor": "#ffffff",
"borderRadius": 4,
@@ -306,8 +306,8 @@ exports[`components/categories_list should render channels error 1`] = `
>
<Text
style={
Array [
Object {
[
{
"alignItems": "center",
"fontFamily": "OpenSans-SemiBold",
"fontWeight": "600",
@@ -315,12 +315,12 @@ exports[`components/categories_list should render channels error 1`] = `
"padding": 1,
"textAlignVertical": "center",
},
Object {
{
"fontSize": 16,
"lineHeight": 18,
"marginTop": 1,
},
Object {
{
"color": "#1c58d9",
},
]
@@ -336,15 +336,15 @@ exports[`components/categories_list should render channels error 1`] = `
exports[`components/categories_list should render team error 1`] = `
<View
animatedStyle={
Object {
"value": Object {
{
"value": {
"maxWidth": "100%",
},
}
}
collapsable={false}
style={
Object {
{
"backgroundColor": "#1e325c",
"flex": 1,
"maxWidth": "100%",
@@ -356,22 +356,22 @@ exports[`components/categories_list should render team error 1`] = `
>
<View
animatedStyle={
Object {
"value": Object {
{
"value": {
"marginLeft": 0,
},
}
}
collapsable={false}
style={
Object {
{
"marginLeft": 0,
}
}
>
<View
style={
Object {
{
"alignItems": "center",
"flexDirection": "row",
"height": 40,
@@ -381,14 +381,14 @@ exports[`components/categories_list should render team error 1`] = `
>
<View
style={
Array [
Object {
[
{
"alignItems": "center",
"flexDirection": "row",
"height": 40,
"justifyContent": "space-between",
},
Object {
{
"flex": 1,
},
]
@@ -398,7 +398,7 @@ exports[`components/categories_list should render team error 1`] = `
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
{
"color": "rgba(255,255,255,0.64)",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 14,
@@ -421,7 +421,7 @@ exports[`components/categories_list should render team error 1`] = `
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
{
"opacity": 1,
}
}
@@ -429,7 +429,7 @@ exports[`components/categories_list should render team error 1`] = `
>
<Text
style={
Object {
{
"color": "rgba(255,255,255,0.64)",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 14,
@@ -446,15 +446,15 @@ exports[`components/categories_list should render team error 1`] = `
</View>
<View
style={
Object {
{
"flexDirection": "row",
}
}
>
<View
animatedStyle={
Object {
"value": Object {
{
"value": {
"marginRight": 8,
"opacity": 1,
"width": 40,
@@ -463,7 +463,7 @@ exports[`components/categories_list should render team error 1`] = `
}
collapsable={false}
style={
Object {
{
"marginRight": 8,
"opacity": 1,
"width": 40,
@@ -482,15 +482,15 @@ exports[`components/categories_list should render team error 1`] = `
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
{
"opacity": 1,
}
}
>
<View
style={
Array [
Object {
[
{
"alignItems": "center",
"backgroundColor": "rgba(255,255,255,0.12)",
"borderRadius": 8,
@@ -522,7 +522,7 @@ exports[`components/categories_list should render team error 1`] = `
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
{
"backgroundColor": "rgba(255,255,255,0.12)",
"borderRadius": 8,
"flex": 1,
@@ -538,7 +538,7 @@ exports[`components/categories_list should render team error 1`] = `
<Icon
name="magnify"
style={
Object {
{
"color": "rgba(255,255,255,0.72)",
"fontSize": 24,
"width": 24,
@@ -547,7 +547,7 @@ exports[`components/categories_list should render team error 1`] = `
/>
<Text
style={
Object {
{
"color": "rgba(255,255,255,0.72)",
"fontFamily": "OpenSans",
"fontSize": 16,
@@ -564,7 +564,7 @@ exports[`components/categories_list should render team error 1`] = `
</View>
<View
style={
Object {
{
"alignItems": "center",
"flex": 1,
"justifyContent": "center",
@@ -574,7 +574,7 @@ exports[`components/categories_list should render team error 1`] = `
>
<View
style={
Object {
{
"alignItems": "center",
"backgroundColor": "rgba(255,255,255,0.08)",
"borderRadius": 60,
@@ -587,7 +587,7 @@ exports[`components/categories_list should render team error 1`] = `
<Icon
name="alert-circle-outline"
style={
Object {
{
"color": "rgba(255,255,255,0.48)",
"fontSize": 72,
"lineHeight": 72,
@@ -597,14 +597,14 @@ exports[`components/categories_list should render team error 1`] = `
</View>
<Text
style={
Array [
Object {
[
{
"fontFamily": "Metropolis-SemiBold",
"fontSize": 20,
"fontWeight": "600",
"lineHeight": 28,
},
Object {
{
"color": "#ffffff",
"marginTop": 20,
"textAlign": "center",
@@ -616,14 +616,14 @@ exports[`components/categories_list should render team error 1`] = `
</Text>
<Text
style={
Array [
Object {
[
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
{
"color": "#ffffff",
"marginTop": 4,
"textAlign": "center",
@@ -645,7 +645,7 @@ exports[`components/categories_list should render team error 1`] = `
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
{
"alignItems": "center",
"backgroundColor": "#ffffff",
"borderRadius": 4,
@@ -661,8 +661,8 @@ exports[`components/categories_list should render team error 1`] = `
>
<Text
style={
Array [
Object {
[
{
"alignItems": "center",
"fontFamily": "OpenSans-SemiBold",
"fontWeight": "600",
@@ -670,12 +670,12 @@ exports[`components/categories_list should render team error 1`] = `
"padding": 1,
"textAlignVertical": "center",
},
Object {
{
"fontSize": 16,
"lineHeight": 18,
"marginTop": 1,
},
Object {
{
"color": "#1c58d9",
},
]

Some files were not shown because too many files have changed in this diff Show More