Compare commits
2 Commits
app-framew
...
gitpod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70f51889e5 | ||
|
|
11242def6a |
@@ -111,9 +111,7 @@ commands:
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
- run:
|
||||
name: Getting JavaScript dependencies
|
||||
command: |
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
node node_modules/\@sentry/cli/scripts/install.js
|
||||
command: NODE_ENV=development npm ci --ignore-scripts
|
||||
- save_cache:
|
||||
name: Save npm cache
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
|
||||
159
.gitpod.Dockerfile
vendored
Normal 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
@@ -0,0 +1,6 @@
|
||||
image:
|
||||
file: .gitpod.Dockerfile
|
||||
|
||||
tasks:
|
||||
- init: npm install
|
||||
- command: npm run android
|
||||
137
NOTICE.txt
@@ -293,17 +293,38 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @react-native-camera-roll/camera-roll
|
||||
## @react-native-cameraroll/react-native-cameraroll
|
||||
|
||||
This product contains '@react-native-camera-roll/camera-roll' by Bartol Karuza.
|
||||
This product contains 'react-native-cameraroll' by Bartol Karuza.
|
||||
|
||||
React Native Camera Roll for iOS & Android
|
||||
React-native native module that provides access to the local camera roll or photo library
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-cameraroll/react-native-cameraroll#readme
|
||||
* https://github.com/react-native-cameraroll/react-native-cameraroll
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
@@ -2011,42 +2032,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-in-app-review
|
||||
|
||||
This product contains 'react-native-in-app-review' by Mina Samir Shafik.
|
||||
|
||||
react native in app review, to rate on Play store, App Store, Generally, the in-app review flow (see figure 1 for play store, figure 2 for ios) can be triggered at any time throughout the user journey of your app. During the flow, the user has the ability
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/MinaSamir11/react-native-in-app-review#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Mina Samir
|
||||
|
||||
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-incall-manager
|
||||
@@ -2327,6 +2312,42 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-shadow-2
|
||||
|
||||
This product contains a modified version of 'react-native-shadow-2' by Henrique Bruno Fantauzzi de Almeida.
|
||||
|
||||
Cross-platform shadow for React Native. Supports Android, iOS, Web and Expo.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/SrBrahma/react-native-shadow-2
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
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
|
||||
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-notifications
|
||||
@@ -2528,42 +2549,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-native-shadow-2
|
||||
|
||||
This product contains a modified version of 'react-native-shadow-2' by Henrique Bruno Fantauzzi de Almeida.
|
||||
|
||||
Cross-platform shadow for React Native. Supports Android, iOS, Web and Expo.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/SrBrahma/react-native-shadow-2
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
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
|
||||
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-share
|
||||
|
||||
18
README.md
@@ -1,10 +1,12 @@
|
||||
# Mattermost Mobile v2
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (7.1.0+)
|
||||
- **Supported iOS versions:** 12.1+
|
||||
This is a work in progress branch for the next major version of the Mattermost mobile app. Once the work is completed and ready to share, this brach will be set as the default branch in this repository.
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (5.25)
|
||||
- **Supported iOS versions:** 11+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 21 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 14 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
|
||||
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
|
||||
|
||||
@@ -49,9 +51,15 @@ You can leave the Beta testing program at any time:
|
||||
|
||||
App data is wiped from the device when a user logs out of the app. If the user is logged in when the account is deactivated, then within one minute the system logs the user out, and as a result all app data is wiped from the device.
|
||||
|
||||
### I need the code for the v1 version
|
||||
### Can I connect to multiple Mattermost servers using the mobile apps?
|
||||
|
||||
You can still access it! We have moved the code from master to the [v1 branch](https://github.com/mattermost/mattermost-mobile/tree/v1). Be aware that we will not be providing any more v1 versions or updates in the public stores.
|
||||
At the moment, we only support connecting to one server at a time. If you need to connect to multiple servers, please [upvote the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/10975938) so we can track demand for it.
|
||||
|
||||
As a work around, you can install both the released "Mattermost" app and sign up to be a [tester](#testing) for the "Mattermost Beta" app so you can connect to two servers at once.
|
||||
|
||||
### Will there be second generation apps available for tablets?
|
||||
|
||||
We plan to add support for tablets in the future, but the timeline depends on how many people have a need for it. If you're looking for a tablet version, please help us out by [upvoting the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/20082079)!
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 440
|
||||
versionCode 428
|
||||
versionName "2.0.0"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 3.0 MiB |
@@ -2,5 +2,5 @@
|
||||
<resources>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
<color name="splashscreen_bg">#090A0B</color>
|
||||
<color name="splashscreen_bg">#1E325C</color>
|
||||
</resources>
|
||||
|
||||
@@ -38,20 +38,6 @@ buildscript {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
exclusiveContent {
|
||||
// We get React Native's Android binaries exclusively through npm,
|
||||
// from a local Maven repo inside node_modules/react-native/.
|
||||
// (The use of exclusiveContent prevents looking elsewhere like Maven Central
|
||||
// and potentially getting a wrong version.)
|
||||
filter {
|
||||
includeGroup "com.facebook.react"
|
||||
}
|
||||
forRepository {
|
||||
maven {
|
||||
url "$rootDir/../node_modules/react-native/android"
|
||||
}
|
||||
}
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
|
||||
@@ -3,45 +3,39 @@
|
||||
|
||||
import {GLOBAL_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export const storeGlobal = async (id: string, value: unknown, prepareRecordsOnly = false) => {
|
||||
export const storeDeviceToken = async (token: string, prepareRecordsOnly = false) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
return operator.handleGlobal({
|
||||
globals: [{id, value}],
|
||||
globals: [{id: GLOBAL_IDENTIFIERS.DEVICE_TOKEN, value: token}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('storeGlobal', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const storeDeviceToken = async (token: string, prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.DEVICE_TOKEN, token, prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeOnboardingViewedValue = async (value = true) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.ONBOARDING, value, false);
|
||||
try {
|
||||
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
return operator.handleGlobal({
|
||||
globals: [{id: GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, value: 'true'}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const storeProfileLongPressTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeDontAskForReview = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.DONT_ASK_FOR_REVIEW, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeLastAskForReview = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.LAST_ASK_FOR_REVIEW, Date.now(), prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeFirstLaunch = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.FIRST_LAUNCH, Date.now(), prepareRecordsOnly);
|
||||
try {
|
||||
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
return operator.handleGlobal({
|
||||
globals: [{id: GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, value: 'true'}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes,
|
||||
} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, removeChannelFromTeamHistory} from '@queries/servers/team';
|
||||
import {getCurrentUser, queryUsersById} from '@queries/servers/user';
|
||||
import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
@@ -365,10 +365,9 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha
|
||||
return {};
|
||||
}
|
||||
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const displaySettings = getTeammateNameDisplaySetting(preferences, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const displaySettings = getTeammateNameDisplaySetting(preferences, config, license);
|
||||
const models: Model[] = [];
|
||||
for await (const channel of channels) {
|
||||
let newDisplayName = '';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchPostAuthors} from '@actions/remote/post';
|
||||
import {ActionType, Post} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPostById, prepareDeletePost, queryPostsById} from '@queries/servers/post';
|
||||
@@ -79,7 +78,7 @@ export const sendEphemeralPost = async (serverUrl: string, message: string, chan
|
||||
user_id: authorId,
|
||||
channel_id: channeId,
|
||||
message,
|
||||
type: Post.POST_TYPES.EPHEMERAL as PostType,
|
||||
type: Post.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL as PostType,
|
||||
create_at: timestamp,
|
||||
edit_at: 0,
|
||||
update_at: timestamp,
|
||||
@@ -95,7 +94,6 @@ export const sendEphemeralPost = async (serverUrl: string, message: string, chan
|
||||
props: {},
|
||||
} as Post;
|
||||
|
||||
await fetchPostAuthors(serverUrl, [post], false);
|
||||
await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||
order: [post.id],
|
||||
|
||||
@@ -6,7 +6,7 @@ import deepEqual from 'deep-equal';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export async function storeConfigAndLicense(serverUrl: string, config: ClientConfig, license: ClientLicense) {
|
||||
@@ -14,11 +14,17 @@ export async function storeConfigAndLicense(serverUrl: string, config: ClientCon
|
||||
// If we have credentials for this server then update the values in the database
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials) {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentLicense = await getLicense(database);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const current = await getCommonSystemValues(operator.database);
|
||||
const systems: IdValue[] = [];
|
||||
if (!deepEqual(config, current.config)) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.CONFIG,
|
||||
value: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
if (!deepEqual(license, currentLicense)) {
|
||||
if (!deepEqual(license, current.license)) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.LICENSE,
|
||||
value: JSON.stringify(license),
|
||||
@@ -28,72 +34,8 @@ export async function storeConfigAndLicense(serverUrl: string, config: ClientCon
|
||||
if (systems.length) {
|
||||
await operator.handleSystem({systems, prepareRecordsOnly: false});
|
||||
}
|
||||
|
||||
await storeConfig(serverUrl, config);
|
||||
}
|
||||
} catch (error) {
|
||||
logError('An error occurred while saving config & license', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function storeConfig(serverUrl: string, config: ClientConfig | undefined, prepareRecordsOnly = false) {
|
||||
if (!config) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentConfig = await getConfig(database);
|
||||
const configsToUpdate: IdValue[] = [];
|
||||
const configsToDelete: IdValue[] = [];
|
||||
|
||||
let k: keyof ClientConfig;
|
||||
for (k in config) {
|
||||
if (currentConfig?.[k] !== config[k]) {
|
||||
configsToUpdate.push({
|
||||
id: k,
|
||||
value: config[k],
|
||||
});
|
||||
}
|
||||
}
|
||||
for (k in currentConfig) {
|
||||
if (config[k] === undefined) {
|
||||
configsToDelete.push({
|
||||
id: k,
|
||||
value: currentConfig[k],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (configsToDelete.length || configsToUpdate.length) {
|
||||
return operator.handleConfigs({configs: configsToUpdate, configsToDelete, prepareRecordsOnly});
|
||||
}
|
||||
} catch (error) {
|
||||
logError('storeConfig', error);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function setLastServerVersionCheck(serverUrl: string, reset = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
await operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.LAST_SERVER_VERSION_CHECK,
|
||||
value: reset ? 0 : Date.now(),
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
} catch (error) {
|
||||
logError('setLastServerVersionCheck', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function dismissAnnouncement(serverUrl: string, announcementText: string) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.LAST_DISMISSED_BANNER, value: announcementText}], prepareRecordsOnly: false});
|
||||
} catch (error) {
|
||||
logError('An error occurred while dismissing an announcement', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import DatabaseManager from '@database/manager';
|
||||
import {getTranslations, t} from '@i18n';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getPostById} from '@queries/servers/post';
|
||||
import {getCurrentTeamId, getCurrentUserId, prepareCommonSystemValues, PrepareCommonSystemValuesArgs, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getCommonSystemValues, getCurrentTeamId, getCurrentUserId, prepareCommonSystemValues, PrepareCommonSystemValuesArgs, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, addTeamToTeamHistory} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled, getThreadById, prepareThreadsFromReceivedPosts, queryThreadsInTeam} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
@@ -74,12 +74,12 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
|
||||
throw new Error('Channel not found');
|
||||
}
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const system = await getCommonSystemValues(database);
|
||||
const isTabletDevice = await isTablet();
|
||||
const teamId = channel.teamId || currentTeamId;
|
||||
const teamId = channel.teamId || system.currentTeamId;
|
||||
|
||||
let switchingTeams = false;
|
||||
if (currentTeamId !== teamId) {
|
||||
if (system.currentTeamId !== teamId) {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
switchingTeams = true;
|
||||
modelPromises.push(addTeamToTeamHistory(operator, teamId, true));
|
||||
|
||||
@@ -207,7 +207,7 @@ export function postEphemeralCallResponseForPost(serverUrl: string, response: Ap
|
||||
serverUrl,
|
||||
message,
|
||||
post.channelId,
|
||||
post.rootId || post.id,
|
||||
post.rootId,
|
||||
response.app_metadata?.bot_user_id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {Events, General, Preferences, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {privateChannelJoinPrompt} from '@helpers/api/channel';
|
||||
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
@@ -523,7 +522,7 @@ export async function fetchDirectChannelsInfo(serverUrl: string, directChannels:
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).fetch();
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences, config?.LockTeammateNameDisplay, config?.TeammateNameDisplay, license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences, config, license);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const channels = directChannels.map((d) => d.toApi());
|
||||
return fetchMissingDirectChannelsInfo(serverUrl, channels, currentUser?.locale, teammateDisplayNameSetting, currentUser?.id);
|
||||
@@ -709,29 +708,6 @@ export async function switchToChannelByName(serverUrl: string, channelName: stri
|
||||
}
|
||||
}
|
||||
|
||||
export async function goToNPSChannel(serverUrl: string) {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await client.getUserByUsername(General.NPS_PLUGIN_BOT_USERNAME);
|
||||
const {data, error} = await createDirectChannel(serverUrl, user.id);
|
||||
if (error || !data) {
|
||||
throw error || new Error('channel not found');
|
||||
}
|
||||
await switchToChannelById(serverUrl, data.id, data.team_id);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export async function createDirectChannel(serverUrl: string, userId: string, displayName = '') {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
@@ -766,9 +742,8 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
|
||||
created.display_name = displayName;
|
||||
} else {
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const system = await getCommonSystemValues(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], system.config, system.license);
|
||||
const {directChannels, users} = await fetchMissingDirectChannelsInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
|
||||
created.display_name = directChannels?.[0].display_name || created.display_name;
|
||||
if (users?.length) {
|
||||
@@ -884,16 +859,19 @@ export async function fetchArchivedChannels(serverUrl: string, teamId: string, p
|
||||
}
|
||||
|
||||
export async function createGroupChannel(serverUrl: string, userIds: string[]) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const currentUser = await getCurrentUser(operator.database);
|
||||
if (!currentUser) {
|
||||
return {error: 'Cannot get the current user'};
|
||||
}
|
||||
@@ -907,10 +885,9 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
|
||||
return {data: created};
|
||||
}
|
||||
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const preferences = await queryPreferencesByCategoryAndName(operator.database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const system = await getCommonSystemValues(operator.database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], system.config, system.license);
|
||||
const {directChannels, users} = await fetchMissingDirectChannelsInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
|
||||
|
||||
const member = {
|
||||
@@ -1046,10 +1023,6 @@ export async function switchToChannelById(serverUrl: string, channelId: string,
|
||||
|
||||
DeviceEventEmitter.emit(Events.CHANNEL_SWITCH, false);
|
||||
|
||||
if (await AppsManager.isAppsEnabled(serverUrl)) {
|
||||
AppsManager.fetchBindings(serverUrl, channelId);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,20 +4,16 @@
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {doAppSubmit, postEphemeralCallResponseForCommandArgs} from '@actions/remote/apps';
|
||||
import {showPermalink} from '@actions/remote/permalink';
|
||||
import {Client} from '@client/rest';
|
||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||
import {AppCallResponseTypes} from '@constants/apps';
|
||||
import DeepLinkType from '@constants/deep_linking';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getConfig, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getTeammateNameDisplay, queryUsersByUsername} from '@queries/servers/user';
|
||||
import {showAppForm, showModal} from '@screens/navigation';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import * as DraftUtils from '@utils/draft';
|
||||
import {matchDeepLink, tryOpenURL} from '@utils/url';
|
||||
import {displayUsername} from '@utils/user';
|
||||
@@ -26,7 +22,7 @@ import {makeDirectChannel, switchToChannelById, switchToChannelByName} from './c
|
||||
|
||||
import type {DeepLinkChannel, DeepLinkPermalink, DeepLinkDM, DeepLinkGM, DeepLinkPlugin} from '@typings/launch';
|
||||
|
||||
export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string): Promise<{data?: CommandResponse; error?: string | {message: string}}> => {
|
||||
export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -39,6 +35,14 @@ export const executeCommand = async (serverUrl: string, intl: IntlShape, message
|
||||
return {error: error as ClientErrorProps};
|
||||
}
|
||||
|
||||
// const config = await queryConfig(operator.database)
|
||||
// if (config.FeatureFlagAppsEnabled) {
|
||||
// const parser = new AppCommandParser(serverUrl, intl, channelId, rootId);
|
||||
// if (parser.isAppCommand(msg)) {
|
||||
// return executeAppCommand(serverUrl, intl, parser);
|
||||
// }
|
||||
// }
|
||||
|
||||
const channel = await getChannelById(operator.database, channelId);
|
||||
const teamId = channel?.teamId || (await getCurrentTeamId(operator.database));
|
||||
|
||||
@@ -49,14 +53,6 @@ export const executeCommand = async (serverUrl: string, intl: IntlShape, message
|
||||
parent_id: rootId,
|
||||
};
|
||||
|
||||
const appsEnabled = await AppsManager.isAppsEnabled(serverUrl);
|
||||
if (appsEnabled) {
|
||||
const parser = new AppCommandParser(serverUrl, intl, channelId, teamId, rootId);
|
||||
if (parser.isAppCommand(message)) {
|
||||
return executeAppCommand(serverUrl, intl, parser, message, args);
|
||||
}
|
||||
}
|
||||
|
||||
let msg = filterEmDashForCommand(message);
|
||||
|
||||
let cmdLength = msg.indexOf(' ');
|
||||
@@ -85,51 +81,44 @@ export const executeCommand = async (serverUrl: string, intl: IntlShape, message
|
||||
return {data};
|
||||
};
|
||||
|
||||
const executeAppCommand = async (serverUrl: string, intl: IntlShape, parser: AppCommandParser, msg: string, args: CommandArgs) => {
|
||||
const {creq, errorMessage} = await parser.composeCommandSubmitCall(msg);
|
||||
const createErrorMessage = (errMessage: string) => {
|
||||
return {error: {message: errMessage}};
|
||||
};
|
||||
// TODO https://mattermost.atlassian.net/browse/MM-41234
|
||||
// const executeAppCommand = (serverUrl: string, intl: IntlShape, parser: any) => {
|
||||
// const {call, errorMessage} = await parser.composeCallFromCommand(msg);
|
||||
// const createErrorMessage = (errMessage: string) => {
|
||||
// return {error: {message: errMessage}};
|
||||
// };
|
||||
|
||||
if (!creq) {
|
||||
return createErrorMessage(errorMessage!);
|
||||
}
|
||||
// if (!call) {
|
||||
// return createErrorMessage(errorMessage!);
|
||||
// }
|
||||
|
||||
const res = await doAppSubmit(serverUrl, creq, intl);
|
||||
if (res.error) {
|
||||
const errorResponse = res.error as AppCallResponse;
|
||||
return createErrorMessage(errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error.',
|
||||
}));
|
||||
}
|
||||
const callResp = res.data as AppCallResponse;
|
||||
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.text) {
|
||||
postEphemeralCallResponseForCommandArgs(serverUrl, callResp, callResp.text, args);
|
||||
}
|
||||
return {data: {}};
|
||||
case AppCallResponseTypes.FORM:
|
||||
if (callResp.form) {
|
||||
showAppForm(callResp.form, creq.context);
|
||||
}
|
||||
return {data: {}};
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
if (callResp.navigate_to_url) {
|
||||
handleGotoLocation(serverUrl, intl, callResp.navigate_to_url);
|
||||
}
|
||||
return {data: {}};
|
||||
default:
|
||||
return createErrorMessage(intl.formatMessage({
|
||||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {
|
||||
type: callResp.type,
|
||||
}));
|
||||
}
|
||||
};
|
||||
// const res = await dispatch(doAppCall(call, AppCallTypes.SUBMIT, intl));
|
||||
// if (res.error) {
|
||||
// const errorResponse = res.error as AppCallResponse;
|
||||
// return createErrorMessage(errorResponse.error || intl.formatMessage({
|
||||
// id: 'apps.error.unknown',
|
||||
// defaultMessage: 'Unknown error.',
|
||||
// }));
|
||||
// }
|
||||
// const callResp = res.data as AppCallResponse;
|
||||
// switch (callResp.type) {
|
||||
// case AppCallResponseTypes.OK:
|
||||
// if (callResp.markdown) {
|
||||
// dispatch(postEphemeralCallResponseForCommandArgs(callResp, callResp.markdown, args));
|
||||
// }
|
||||
// return {data: {}};
|
||||
// case AppCallResponseTypes.FORM:
|
||||
// case AppCallResponseTypes.NAVIGATE:
|
||||
// return {data: {}};
|
||||
// default:
|
||||
// return createErrorMessage(intl.formatMessage({
|
||||
// id: 'apps.error.responses.unknown_type',
|
||||
// defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
// }, {
|
||||
// type: callResp.type,
|
||||
// }));
|
||||
// }
|
||||
// };
|
||||
|
||||
const filterEmDashForCommand = (command: string): string => {
|
||||
return command.replace(/\u2014/g, '--');
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {setLastServerVersionCheck} from '@actions/local/systems';
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getCurrentChannelId, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getCurrentChannelId} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {deleteV1Data} from '@utils/file';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
@@ -22,9 +21,6 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
||||
|
||||
if (!since) {
|
||||
registerDeviceToken(serverUrl);
|
||||
if (Object.keys(DatabaseManager.serverDatabases).length === 1) {
|
||||
await setLastServerVersionCheck(serverUrl, true);
|
||||
}
|
||||
}
|
||||
|
||||
// clear lastUnreadChannelId
|
||||
@@ -69,8 +65,7 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
||||
logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = meData?.user || (await getCurrentUser(database))!;
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
|
||||
|
||||
if (!since) {
|
||||
|
||||
@@ -145,7 +145,7 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
|
||||
|
||||
const confReq = await fetchConfigAndLicense(serverUrl);
|
||||
const prefData = await fetchMyPreferences(serverUrl, fetchOnly);
|
||||
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, confReq.config?.CollapsedThreads, confReq.config?.FeatureFlagCollapsedThreads));
|
||||
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, confReq.config));
|
||||
if (prefData.preferences) {
|
||||
const crtToggled = await getHasCRTChanged(database, prefData.preferences);
|
||||
if (crtToggled) {
|
||||
@@ -306,7 +306,7 @@ export async function entryInitialChannelId(database: Database, requestedChannel
|
||||
|
||||
export async function restDeferredAppEntryActions(
|
||||
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
|
||||
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
config: ClientConfig, license: ClientLicense, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
initialTeamId?: string, initialChannelId?: string) {
|
||||
// defer sidebar DM & GM profiles
|
||||
let channelsToFetchProfiles: Set<Channel>|undefined;
|
||||
@@ -325,7 +325,7 @@ export async function restDeferredAppEntryActions(
|
||||
fetchTeamsChannelsAndUnreadPosts(serverUrl, since, teamData.teams, teamData.memberships, initialTeamId);
|
||||
}
|
||||
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
||||
if (preferences && processIsCRTEnabled(preferences, config)) {
|
||||
if (initialTeamId) {
|
||||
await fetchNewThreads(serverUrl, initialTeamId, false);
|
||||
}
|
||||
@@ -348,7 +348,7 @@ export async function restDeferredAppEntryActions(
|
||||
|
||||
setTimeout(async () => {
|
||||
if (channelsToFetchProfiles?.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config, license);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId);
|
||||
}
|
||||
}, FETCH_MISSING_DM_TIMEOUT);
|
||||
@@ -454,7 +454,7 @@ const restSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
for (const myTeam of myTeams) {
|
||||
fetchMyChannelsForTeam(serverUrl, myTeam.id, false, 0, false, excludeDirect);
|
||||
excludeDirect = true;
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
||||
if (preferences && processIsCRTEnabled(preferences, config)) {
|
||||
fetchNewThreads(serverUrl, myTeam.id, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export async function deferredAppEntryGraphQLActions(
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
||||
if (preferences && processIsCRTEnabled(preferences, config)) {
|
||||
if (initialTeamId) {
|
||||
await fetchNewThreads(serverUrl, initialTeamId, false);
|
||||
}
|
||||
@@ -256,7 +256,7 @@ export const entry = async (serverUrl: string, teamId?: string, channelId?: stri
|
||||
|
||||
export async function deferredAppEntryActions(
|
||||
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
|
||||
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
config: ClientConfig, license: ClientLicense, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
initialTeamId?: string, initialChannelId?: string) {
|
||||
let result;
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {getDefaultThemeByAppearance} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getMyChannel} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getConfig, getCurrentTeamId, getLicense, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
@@ -136,8 +136,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
await operator.batchRecords(models);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
const {config, license} = await getCommonSystemValues(operator.database);
|
||||
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId);
|
||||
|
||||
|
||||
@@ -124,4 +124,3 @@ export const getRedirectLocation = async (serverUrl: string, link: string) => {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {fetchMyTeam} from '@actions/remote/team';
|
||||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getMyChannel, getChannelById} from '@queries/servers/channel';
|
||||
import {getCurrentTeamId, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {getCommonSystemValues, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
@@ -30,14 +30,14 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const system = await getCommonSystemValues(database);
|
||||
let teamId = notification.payload?.team_id;
|
||||
let isDirectChannel = false;
|
||||
|
||||
if (!teamId) {
|
||||
// If the notification payload does not have a teamId we assume is a DM/GM
|
||||
isDirectChannel = true;
|
||||
teamId = currentTeamId;
|
||||
teamId = system.currentTeamId;
|
||||
}
|
||||
|
||||
// To make the switch faster we determine if we already have the team & channel
|
||||
@@ -129,13 +129,13 @@ export const openNotification = async (serverUrl: string, notification: Notifica
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(rootId);
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const system = await getCommonSystemValues(database);
|
||||
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
let teamId = notification.payload?.team_id;
|
||||
|
||||
if (!teamId) {
|
||||
// If the notification payload does not have a teamId we assume is a DM/GM
|
||||
teamId = currentTeamId;
|
||||
teamId = system.currentTeamId;
|
||||
}
|
||||
|
||||
if (currentServerUrl !== serverUrl) {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General} from '@constants';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
export const isNPSEnabled = async (serverUrl: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const manifests = await client.getPluginsManifests();
|
||||
for (const v of manifests) {
|
||||
if (v.id === General.NPS_PLUGIN_ID) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const giveFeedbackAction = async (serverUrl: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const post = await client.npsGiveFeedbackAction();
|
||||
return {post};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
@@ -54,6 +54,7 @@ export const saveFavoriteChannel = async (serverUrl: string, channelId: string,
|
||||
}
|
||||
|
||||
try {
|
||||
// Todo: @shaz I think you'll need to add the category handler here so that the channel is added/removed from the favorites category
|
||||
const userId = await getCurrentUserId(operator.database);
|
||||
const favPref: PreferenceType = {
|
||||
category: CATEGORY_FAVORITE_CHANNEL,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {storeConfig} from '@actions/local/systems';
|
||||
import {Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPreferenceValue, getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
@@ -10,7 +9,7 @@ import NetworkManager from '@managers/network_manager';
|
||||
import {prepareCategories, prepareCategoryChannels} from '@queries/servers/categories';
|
||||
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
|
||||
import {prepareMyPreferences, queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, getCommonSystemValues} from '@queries/servers/system';
|
||||
import {prepareMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {isDMorGM, selectDefaultChannelForTeam} from '@utils/channel';
|
||||
@@ -102,15 +101,14 @@ export async function retryInitialTeamAndChannel(serverUrl: string) {
|
||||
|
||||
const models: Model[] = (await Promise.all([
|
||||
prepareMyPreferences(operator, prefData.preferences!),
|
||||
storeConfig(serverUrl, clData.config, true),
|
||||
...prepareMyTeams(operator, teamData.teams!, teamData.memberships!),
|
||||
...await prepareMyChannelsForTeam(operator, initialTeam.id, chData!.channels!, chData!.memberships!),
|
||||
prepareCategories(operator, chData!.categories!),
|
||||
prepareCategoryChannels(operator, chData!.categories!),
|
||||
|
||||
prepareCommonSystemValues(
|
||||
operator,
|
||||
{
|
||||
config: clData.config!,
|
||||
license: clData.license!,
|
||||
currentTeamId: initialTeam?.id,
|
||||
currentChannelId: initialChannel?.id,
|
||||
@@ -123,7 +121,7 @@ export async function retryInitialTeamAndChannel(serverUrl: string) {
|
||||
const directChannels = chData!.channels!.filter(isDMorGM);
|
||||
const channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
if (channelsToFetchProfiles.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(prefData.preferences || [], clData.config?.LockTeammateNameDisplay, clData.config?.TeammateNameDisplay, clData.license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(prefData.preferences || [], clData.config, clData.license);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), user.locale, teammateDisplayNameSetting, user.id);
|
||||
}
|
||||
|
||||
@@ -165,8 +163,7 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) {
|
||||
user_id: p.userId,
|
||||
value: p.value,
|
||||
}));
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
|
||||
// fetch channels / channel membership for initial team
|
||||
const chData = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
|
||||
@@ -203,7 +200,7 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) {
|
||||
const directChannels = chData!.channels!.filter(isDMorGM);
|
||||
const channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
if (channelsToFetchProfiles.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config, license);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), user.locale, teammateDisplayNameSetting, user.id);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import NetworkManager from '@managers/network_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {queryServerName} from '@queries/app/servers';
|
||||
import {getCurrentUserId, getExpiredSession, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCurrentUserId, getCommonSystemValues, getExpiredSession} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {logWarning, logError} from '@utils/log';
|
||||
@@ -35,10 +35,9 @@ export const completeLogin = async (serverUrl: string) => {
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const {config, license}: { config: Partial<ClientConfig>; license: Partial<ClientLicense> } = await getCommonSystemValues(database);
|
||||
|
||||
if (!Object.keys(config)?.length || !license || !Object.keys(license)?.length) {
|
||||
if (!Object.keys(config)?.length || !Object.keys(license)?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
|
||||
export async function fetchTermsOfService(serverUrl: string): Promise<{terms?: TermsOfService; error?: any}> {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const terms = await client.getTermsOfService();
|
||||
return {terms};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTermsOfServiceStatus(serverUrl: string, id: string, status: boolean): Promise<{resp?: {status: string}; error?: any}> {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await client.updateMyTermsOfServiceStatus(id, status);
|
||||
return {resp};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,9 @@ import {fetchPostThread} from '@actions/remote/post';
|
||||
import {General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getPostById} from '@queries/servers/post';
|
||||
import {getConfigValue, getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getCommonSystemValues, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getIsCRTEnabled, getNewestThreadInTeam, getThreadById} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
|
||||
@@ -57,20 +56,6 @@ export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string,
|
||||
|
||||
await switchToThread(serverUrl, rootId, isFromNotification);
|
||||
|
||||
if (await AppsManager.isAppsEnabled(serverUrl)) {
|
||||
// Getting the post again in case we didn't had it at the beginning
|
||||
const post = await getPostById(database, rootId);
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
|
||||
if (post) {
|
||||
if (currentChannelId === post?.channelId) {
|
||||
AppsManager.copyMainBindingsToThread(serverUrl, currentChannelId);
|
||||
} else {
|
||||
AppsManager.fetchBindings(serverUrl, post.channelId, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -104,8 +89,9 @@ export const fetchThreads = async (
|
||||
}
|
||||
|
||||
try {
|
||||
const version = await getConfigValue(database, 'Version');
|
||||
const data = await client.getThreads('me', teamId, before, after, perPage, deleted, unread, since, false, version);
|
||||
const {config} = await getCommonSystemValues(database);
|
||||
|
||||
const data = await client.getThreads('me', teamId, before, after, perPage, deleted, unread, since, false, config.Version);
|
||||
|
||||
const {threads} = data;
|
||||
|
||||
@@ -322,7 +308,7 @@ async function fetchBatchThreads(
|
||||
return {error: 'currentUser not found'};
|
||||
}
|
||||
|
||||
const version = await getConfigValue(operator.database, 'Version');
|
||||
const {config} = await getCommonSystemValues(operator.database);
|
||||
const data: Thread[] = [];
|
||||
|
||||
const fetchThreadsFunc = async (opts: FetchThreadsOptions) => {
|
||||
@@ -330,7 +316,7 @@ async function fetchBatchThreads(
|
||||
const {before, after, perPage = General.CRT_CHUNK_SIZE, deleted, unread, since} = opts;
|
||||
|
||||
page += 1;
|
||||
const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, version);
|
||||
const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, config.Version);
|
||||
if (threads.length) {
|
||||
// Mark all fetched threads as following
|
||||
for (const thread of threads) {
|
||||
|
||||
@@ -407,7 +407,7 @@ export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetc
|
||||
return {users: [], existingUsers};
|
||||
}
|
||||
const users = await client.getProfilesByIds([...new Set(usersToLoad)]);
|
||||
if (!fetchOnly && users.length) {
|
||||
if (!fetchOnly) {
|
||||
await operator.handleUsers({
|
||||
users,
|
||||
prepareRecordsOnly: false,
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchGroupsForChannel, fetchGroupsForMember, fetchGroupsForTeam} from '@actions/remote/groups';
|
||||
import {deleteGroupChannelById, deleteGroupMembershipById, deleteGroupTeamById} from '@app/queries/servers/group';
|
||||
import {generateGroupAssociationId} from '@app/utils/groups';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {deleteGroupChannelById, deleteGroupMembershipById, deleteGroupTeamById} from '@queries/servers/group';
|
||||
import {generateGroupAssociationId} from '@utils/groups';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
type WebsocketGroupMessage = WebSocketMessage<{
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
handleCallUserDisconnected,
|
||||
handleCallUserMuted,
|
||||
handleCallUserRaiseHand,
|
||||
handleCallUserReacted,
|
||||
handleCallUserUnmuted,
|
||||
handleCallUserUnraiseHand,
|
||||
handleCallUserVoiceOff,
|
||||
@@ -28,13 +27,12 @@ import {isSupportedServerCalls} from '@calls/utils';
|
||||
import {Events, Screens, WebsocketEvents} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import {getActiveServerUrl, queryActiveServer} from '@queries/app/servers';
|
||||
import {getCurrentChannel} from '@queries/servers/channel';
|
||||
import {
|
||||
getCommonSystemValues,
|
||||
getConfig,
|
||||
getCurrentUserId,
|
||||
getLicense,
|
||||
getWebSocketLastDisconnected,
|
||||
resetWebSocketLastDisconnected,
|
||||
setCurrentTeamAndChannelId,
|
||||
@@ -179,16 +177,14 @@ async function doReconnect(serverUrl: string) {
|
||||
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(database))!;
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchedToChannel ? initialChannelId : undefined);
|
||||
|
||||
if (isSupportedServerCalls(config?.Version)) {
|
||||
loadConfigAndCalls(serverUrl, currentUserId);
|
||||
}
|
||||
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchedToChannel ? initialChannelId : undefined);
|
||||
|
||||
AppsManager.refreshAppBindings(serverUrl);
|
||||
// https://mattermost.atlassian.net/browse/MM-41520
|
||||
}
|
||||
|
||||
export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
@@ -394,9 +390,6 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
case WebsocketEvents.CALLS_CALL_END:
|
||||
handleCallEnded(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_REACTED:
|
||||
handleCallUserReacted(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.GROUP_RECEIVED:
|
||||
handleGroupReceivedEvent(serverUrl, msg);
|
||||
|
||||
@@ -128,7 +128,10 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
|
||||
) {
|
||||
markAsViewed = true;
|
||||
markAsRead = false;
|
||||
} else if ((post.channel_id === currentChannelId)) {
|
||||
} else if ((post.channel_id === currentChannelId)) { // TODO: THREADS && !viewingGlobalThreads) {
|
||||
// Don't mark as read if we're in global threads screen
|
||||
// the currentChannelId still refers to previously viewed channel
|
||||
|
||||
const isChannelScreenMounted = NavigationStore.getNavigationComponents().includes(Screens.CHANNEL);
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {updateDmGmDisplayName} from '@actions/local/channel';
|
||||
import {storeConfig} from '@actions/local/systems';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
@@ -36,8 +35,10 @@ export async function handleConfigChangedEvent(serverUrl: string, msg: WebSocket
|
||||
|
||||
try {
|
||||
const config = msg.data.config;
|
||||
const systems: IdValue[] = [{id: SYSTEM_IDENTIFIERS.CONFIG, value: JSON.stringify(config)}];
|
||||
|
||||
const prevConfig = await getConfig(operator.database);
|
||||
await storeConfig(serverUrl, config);
|
||||
await operator.handleSystem({systems, prepareRecordsOnly: false});
|
||||
if (config?.LockTeammateNameDisplay && (prevConfig?.LockTeammateNameDisplay !== config.LockTeammateNameDisplay)) {
|
||||
updateDmGmDisplayName(serverUrl);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {queryChannelsByTypes, queryUserChannelsByTypes} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
@@ -84,14 +84,13 @@ export async function handleUserTypingEvent(serverUrl: string, msg: WebSocketMes
|
||||
return;
|
||||
}
|
||||
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const {config, license} = await getCommonSystemValues(database);
|
||||
|
||||
const {users, existingUsers} = await fetchUsersByIds(serverUrl, [msg.data.user_id]);
|
||||
const user = users?.[0] || existingUsers?.[0];
|
||||
|
||||
const namePreference = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(namePreference, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(namePreference, config, license);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const username = displayUsername(user, currentUser?.locale, teammateDisplayNameSetting);
|
||||
const data = {
|
||||
|
||||
@@ -184,8 +184,6 @@ query ${QueryNames.QUERY_ENTRY} {
|
||||
createAt
|
||||
expiresAt
|
||||
}
|
||||
termsOfServiceId
|
||||
termsOfServiceCreateAt
|
||||
}
|
||||
teamMembers(userId:"me") {
|
||||
deleteAt
|
||||
|
||||
@@ -205,16 +205,12 @@ export default class ClientBase {
|
||||
return `${this.urlVersion}/plugins`;
|
||||
}
|
||||
|
||||
getPluginRoute(id: string) {
|
||||
return `/plugins/${id}`;
|
||||
}
|
||||
|
||||
getAppsProxyRoute() {
|
||||
return this.getPluginRoute('com.mattermost.apps');
|
||||
return '/plugins/com.mattermost.apps';
|
||||
}
|
||||
|
||||
getCallsRoute() {
|
||||
return this.getPluginRoute(Calls.PluginId);
|
||||
return `/plugins/${Calls.PluginId}`;
|
||||
}
|
||||
|
||||
doFetch = async (url: string, options: ClientOptions, returnDataOnly = true) => {
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
import {ClientResponse, ClientResponseError, ProgressPromise, UploadRequestOptions} from '@mattermost/react-native-network-client';
|
||||
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
|
||||
export interface ClientFilesMix {
|
||||
getFileUrl: (fileId: string, timestamp: number) => string;
|
||||
getFileThumbnailUrl: (fileId: string, timestamp: number) => string;
|
||||
@@ -74,7 +72,7 @@ const ClientFiles = (superclass: any) => class extends superclass {
|
||||
channel_id: channelId,
|
||||
},
|
||||
},
|
||||
timeoutInterval: toMilliseconds({minutes: 3}),
|
||||
timeoutInterval: 3 * 60 * 1000, // 3 minutes
|
||||
};
|
||||
const promise = this.apiClient.upload(url, file.localPath, options) as ProgressPromise<ClientResponse>;
|
||||
promise.progress!(onProgress).then(onComplete).catch(onError);
|
||||
|
||||
@@ -15,7 +15,6 @@ import ClientFiles, {ClientFilesMix} from './files';
|
||||
import ClientGeneral, {ClientGeneralMix} from './general';
|
||||
import ClientGroups, {ClientGroupsMix} from './groups';
|
||||
import ClientIntegrations, {ClientIntegrationsMix} from './integrations';
|
||||
import ClientNPS, {ClientNPSMix} from './nps';
|
||||
import ClientPosts, {ClientPostsMix} from './posts';
|
||||
import ClientPreferences, {ClientPreferencesMix} from './preferences';
|
||||
import ClientTeams, {ClientTeamsMix} from './teams';
|
||||
@@ -41,8 +40,7 @@ interface Client extends ClientBase,
|
||||
ClientTosMix,
|
||||
ClientUsersMix,
|
||||
ClientCallsMix,
|
||||
ClientPluginsMix,
|
||||
ClientNPSMix
|
||||
ClientPluginsMix
|
||||
{}
|
||||
|
||||
class Client extends mix(ClientBase).with(
|
||||
@@ -62,7 +60,6 @@ class Client extends mix(ClientBase).with(
|
||||
ClientUsers,
|
||||
ClientCalls,
|
||||
ClientPlugins,
|
||||
ClientNPS,
|
||||
) {
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string) {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General} from '@constants';
|
||||
|
||||
export interface ClientNPSMix {
|
||||
npsGiveFeedbackAction: () => Promise<Post>;
|
||||
}
|
||||
|
||||
const ClientNPS = (superclass: any) => class extends superclass {
|
||||
npsGiveFeedbackAction = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getPluginRoute(General.NPS_PLUGIN_ID)}/api/v1/give_feedback`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientNPS;
|
||||
@@ -2,8 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export interface ClientTosMix {
|
||||
updateMyTermsOfServiceStatus: (termsOfServiceId: string, accepted: boolean) => Promise<{status: string}>;
|
||||
getTermsOfService: () => Promise<TermsOfService>;
|
||||
updateMyTermsOfServiceStatus: (termsOfServiceId: string, accepted: boolean) => Promise<any>;
|
||||
getTermsOfService: () => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientTos = (superclass: any) => class extends superclass {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {Platform} from 'react-native';
|
||||
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getConfig} from '@queries/servers/system';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {logError, logInfo, logWarning} from '@utils/log';
|
||||
|
||||
const MAX_WEBSOCKET_FAILS = 7;
|
||||
@@ -75,8 +75,8 @@ export default class WebSocketClient {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await getConfig(database);
|
||||
const connectionUrl = (config.WebsocketURL || this.serverUrl) + '/api/v4/websocket';
|
||||
const system = await getCommonSystemValues(database);
|
||||
const connectionUrl = (system.config.WebsocketURL || this.serverUrl) + '/api/v4/websocket';
|
||||
|
||||
if (this.connectingCallback) {
|
||||
this.connectingCallback();
|
||||
@@ -97,7 +97,7 @@ export default class WebSocketClient {
|
||||
|
||||
this.url = connectionUrl;
|
||||
|
||||
const reliableWebSockets = config.EnableReliableWebSockets === 'true';
|
||||
const reliableWebSockets = system.config.EnableReliableWebSockets === 'true';
|
||||
if (reliableWebSockets) {
|
||||
// Add connection id, and last_sequence_number to the query param.
|
||||
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import {dismissAnnouncement} from '@actions/local/systems';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import RemoveMarkdown from '@components/remove_markdown';
|
||||
import {ANNOUNCEMENT_BAR_HEIGHT} from '@constants/view';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import ExpandedAnnouncementBanner from './expanded_announcement_banner';
|
||||
|
||||
type Props = {
|
||||
bannerColor: string;
|
||||
bannerDismissed: boolean;
|
||||
bannerEnabled: boolean;
|
||||
bannerText?: string;
|
||||
bannerTextColor?: string;
|
||||
allowDismissal: boolean;
|
||||
}
|
||||
|
||||
const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
background: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
},
|
||||
bannerContainer: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 10,
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 8,
|
||||
borderRadius: 7,
|
||||
},
|
||||
wrapper: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
bannerTextContainer: {
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
marginRight: 5,
|
||||
textAlign: 'center',
|
||||
},
|
||||
bannerText: {
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const CLOSE_BUTTON_ID = 'announcement-close';
|
||||
|
||||
const BUTTON_HEIGHT = 48; // From /app/utils/buttonStyles.ts, lg button
|
||||
const TITLE_HEIGHT = 30 + 12; // typography 600 line height
|
||||
const MARGINS = 12 + 24 + 10; // (after title + after text + after content) from ./expanded_announcement_banner.tsx
|
||||
const TEXT_CONTAINER_HEIGHT = 150;
|
||||
const DISMISS_BUTTON_HEIGHT = BUTTON_HEIGHT + 10; // Top margin from ./expanded_announcement_banner.tsx
|
||||
|
||||
const SNAP_POINT_WITHOUT_DISMISS = TITLE_HEIGHT + BUTTON_HEIGHT + MARGINS + TEXT_CONTAINER_HEIGHT;
|
||||
|
||||
const AnnouncementBanner = ({
|
||||
bannerColor,
|
||||
bannerDismissed,
|
||||
bannerEnabled,
|
||||
bannerText = '',
|
||||
bannerTextColor = '#000',
|
||||
allowDismissal,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const height = useSharedValue(0);
|
||||
const theme = useTheme();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const style = getStyle(theme);
|
||||
|
||||
const renderContent = useCallback(() => (
|
||||
<ExpandedAnnouncementBanner
|
||||
allowDismissal={allowDismissal}
|
||||
bannerText={bannerText}
|
||||
/>
|
||||
), [allowDismissal, bannerText]);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
const title = intl.formatMessage({
|
||||
id: 'mobile.announcement_banner.title',
|
||||
defaultMessage: 'Announcement',
|
||||
});
|
||||
|
||||
let snapPoint = SNAP_POINT_WITHOUT_DISMISS;
|
||||
if (allowDismissal) {
|
||||
snapPoint += DISMISS_BUTTON_HEIGHT;
|
||||
}
|
||||
|
||||
bottomSheet({
|
||||
closeButtonId: CLOSE_BUTTON_ID,
|
||||
title,
|
||||
renderContent,
|
||||
snapPoints: [snapPoint, 10],
|
||||
theme,
|
||||
});
|
||||
}, [theme.sidebarHeaderTextColor, intl.locale, renderContent, allowDismissal]);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
dismissAnnouncement(serverUrl, bannerText);
|
||||
}, [serverUrl, bannerText]);
|
||||
|
||||
useEffect(() => {
|
||||
const showBanner = bannerEnabled && !bannerDismissed && Boolean(bannerText);
|
||||
setVisible(showBanner);
|
||||
}, [bannerDismissed, bannerEnabled, bannerText]);
|
||||
|
||||
useEffect(() => {
|
||||
height.value = withTiming(visible ? ANNOUNCEMENT_BAR_HEIGHT : 0, {
|
||||
duration: 200,
|
||||
});
|
||||
}, [visible]);
|
||||
|
||||
const bannerStyle = useAnimatedStyle(() => ({
|
||||
height: height.value,
|
||||
}));
|
||||
|
||||
const bannerTextContainerStyle = useMemo(() => [style.bannerTextContainer, {
|
||||
color: bannerTextColor,
|
||||
}], [style, bannerTextColor]);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[style.background, bannerStyle]}
|
||||
>
|
||||
<View
|
||||
style={[style.bannerContainer, {backgroundColor: bannerColor}]}
|
||||
>
|
||||
{visible &&
|
||||
<>
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
style={style.wrapper}
|
||||
>
|
||||
<Text
|
||||
style={bannerTextContainerStyle}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
<CompassIcon
|
||||
color={bannerTextColor}
|
||||
name='information-outline'
|
||||
size={18}
|
||||
/>
|
||||
{' '}
|
||||
<RemoveMarkdown
|
||||
value={bannerText}
|
||||
textStyle={style.bannerText}
|
||||
/>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{allowDismissal && (
|
||||
<TouchableOpacity
|
||||
onPress={handleDismiss}
|
||||
>
|
||||
<CompassIcon
|
||||
color={changeOpacity(bannerTextColor, 0.56)}
|
||||
name='close'
|
||||
size={18}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
</>
|
||||
}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnouncementBanner;
|
||||
@@ -1,136 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
import Button from 'react-native-button';
|
||||
import {ScrollView} from 'react-native-gesture-handler';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {dismissAnnouncement} from '@actions/local/systems';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Markdown from '@components/markdown';
|
||||
import {Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
allowDismissal: boolean;
|
||||
bannerText: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContainer: {
|
||||
flex: 1,
|
||||
marginTop: 12,
|
||||
marginBottom: 24,
|
||||
},
|
||||
baseTextStyle: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 100, 'Regular'),
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
dismissBottomSheet();
|
||||
};
|
||||
|
||||
const ExpandedAnnouncementBanner = ({
|
||||
allowDismissal,
|
||||
bannerText,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
const intl = useIntl();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const dismissBanner = useCallback(() => {
|
||||
dismissAnnouncement(serverUrl, bannerText);
|
||||
close();
|
||||
}, [bannerText]);
|
||||
|
||||
const buttonStyles = useMemo(() => {
|
||||
return {
|
||||
okay: {
|
||||
button: buttonBackgroundStyle(theme, 'lg', 'primary'),
|
||||
text: buttonTextStyle(theme, 'lg', 'primary'),
|
||||
},
|
||||
dismiss: {
|
||||
button: [{marginTop: 10}, buttonBackgroundStyle(theme, 'lg', 'link')],
|
||||
text: buttonTextStyle(theme, 'lg', 'link'),
|
||||
},
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
return [style.container, {marginBottom: insets.bottom + 10}];
|
||||
}, [style, insets.bottom]);
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
{!isTablet && (
|
||||
<Text style={style.title}>
|
||||
{intl.formatMessage({
|
||||
id: 'mobile.announcement_banner.title',
|
||||
defaultMessage: 'Announcement',
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
<ScrollView
|
||||
style={style.scrollContainer}
|
||||
>
|
||||
<Markdown
|
||||
baseTextStyle={style.baseTextStyle}
|
||||
blockStyles={getMarkdownBlockStyles(theme)}
|
||||
disableGallery={true}
|
||||
textStyles={getMarkdownTextStyles(theme)}
|
||||
value={bannerText}
|
||||
theme={theme}
|
||||
location={Screens.BOTTOM_SHEET}
|
||||
/>
|
||||
</ScrollView>
|
||||
<Button
|
||||
containerStyle={buttonStyles.okay.button}
|
||||
onPress={close}
|
||||
>
|
||||
<FormattedText
|
||||
id='announcment_banner.okay'
|
||||
defaultMessage={'Okay'}
|
||||
style={buttonStyles.okay.text}
|
||||
/>
|
||||
</Button>
|
||||
{allowDismissal && (
|
||||
<Button
|
||||
containerStyle={buttonStyles.dismiss.button}
|
||||
onPress={dismissBanner}
|
||||
>
|
||||
<FormattedText
|
||||
id='announcment_banner.dismiss'
|
||||
defaultMessage={'Dismiss announcement'}
|
||||
style={buttonStyles.dismiss.text}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpandedAnnouncementBanner;
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$, combineLatest} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfigBooleanValue, observeConfigValue, observeLastDismissedAnnouncement, observeLicense} from '@queries/servers/system';
|
||||
|
||||
import AnnouncementBanner from './announcement_banner';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const lastDismissed = observeLastDismissedAnnouncement(database);
|
||||
const bannerText = observeConfigValue(database, 'BannerText');
|
||||
const allowDismissal = observeConfigBooleanValue(database, 'AllowBannerDismissal');
|
||||
|
||||
const bannerDismissed = combineLatest([lastDismissed, bannerText, allowDismissal]).pipe(
|
||||
switchMap(([ld, bt, abd]) => of$(abd && (ld === bt))),
|
||||
);
|
||||
|
||||
const license = observeLicense(database);
|
||||
const enableBannerConfig = observeConfigBooleanValue(database, 'EnableBanner');
|
||||
const bannerEnabled = combineLatest([license, enableBannerConfig]).pipe(
|
||||
switchMap(([lcs, cfg]) => of$(cfg && lcs?.IsLicensed === 'true')),
|
||||
);
|
||||
return {
|
||||
bannerColor: observeConfigValue(database, 'BannerColor'),
|
||||
bannerEnabled,
|
||||
bannerText,
|
||||
bannerTextColor: observeConfigValue(database, 'BannerTextColor'),
|
||||
bannerDismissed,
|
||||
allowDismissal,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(AnnouncementBanner));
|
||||
@@ -189,7 +189,6 @@ const Autocomplete = ({
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
isAppsEnabled={isAppsEnabled}
|
||||
/>
|
||||
}
|
||||
{/* {(isSearch && enableDateSuggestion) &&
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
|
||||
import Autocomplete from './autocomplete';
|
||||
|
||||
type OwnProps = {
|
||||
serverUrl?: string;
|
||||
}
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables(['serverUrl'], ({serverUrl}: OwnProps) => ({
|
||||
isAppsEnabled: serverUrl ? AppsManager.observeIsAppsEnabled(serverUrl) : of$(false),
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
isAppsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
|
||||
}));
|
||||
|
||||
export default enhanced(Autocomplete);
|
||||
export default withDatabase(enhanced(Autocomplete));
|
||||
|
||||
@@ -7,9 +7,9 @@ import {IntlShape} from 'react-intl';
|
||||
import {doAppFetchForm, doAppLookup} from '@actions/remote/apps';
|
||||
import {fetchChannelById, fetchChannelByName, searchChannels} from '@actions/remote/channel';
|
||||
import {fetchUsersByIds, fetchUsersByUsernames, searchUsers} from '@actions/remote/user';
|
||||
import {AppBindingLocations, AppCallResponseTypes, AppFieldTypes, COMMAND_SUGGESTION_ERROR} from '@constants/apps';
|
||||
import {AppCallResponseTypes, AppFieldTypes, COMMAND_SUGGESTION_ERROR} from '@constants/apps';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import {getChannelById, getChannelByName} from '@queries/servers/channel';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getUserById, queryUsersByUsername} from '@queries/servers/user';
|
||||
@@ -173,7 +173,7 @@ export class ParsedCommand {
|
||||
}
|
||||
|
||||
case ParseState.EndCommand: {
|
||||
const binding = bindings.find(this.findBindings, this);
|
||||
const binding = bindings.find(this.findBindings);
|
||||
if (!binding) {
|
||||
// gone as far as we could, this token doesn't match a sub-command.
|
||||
// return the state from the last matching binding
|
||||
@@ -856,13 +856,15 @@ export class AppCommandParser {
|
||||
private teamID: string;
|
||||
private rootPostID?: string;
|
||||
private intl: IntlShape;
|
||||
private theme: Theme;
|
||||
|
||||
constructor(serverUrl: string, intl: IntlShape, channelID: string, teamID = '', rootPostID = '') {
|
||||
constructor(serverUrl: string, intl: IntlShape, channelID: string, teamID = '', rootPostID = '', theme: Theme) {
|
||||
this.serverUrl = serverUrl;
|
||||
this.channelID = channelID;
|
||||
this.rootPostID = rootPostID;
|
||||
this.teamID = teamID;
|
||||
this.intl = intl;
|
||||
this.theme = theme;
|
||||
|
||||
// We are making the assumption the database is always present at this level.
|
||||
// This assumption may not be correct. Please review.
|
||||
@@ -984,9 +986,6 @@ export class AppCommandParser {
|
||||
break;
|
||||
}
|
||||
user = res.users[0] || res.existingUsers[0];
|
||||
if (!user) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
parsed.values[f.name] = user.username;
|
||||
break;
|
||||
@@ -1001,9 +1000,6 @@ export class AppCommandParser {
|
||||
break;
|
||||
}
|
||||
channel = res.channel;
|
||||
if (!channel) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
parsed.values[f.name] = channel.name;
|
||||
break;
|
||||
@@ -1026,6 +1022,7 @@ export class AppCommandParser {
|
||||
const result: AutocompleteSuggestion[] = [];
|
||||
|
||||
const bindings = this.getCommandBindings();
|
||||
|
||||
for (const binding of bindings) {
|
||||
let base = binding.label;
|
||||
if (!base) {
|
||||
@@ -1182,21 +1179,14 @@ export class AppCommandParser {
|
||||
|
||||
const errors: {[key: string]: string} = {};
|
||||
await Promise.all(parsed.resolvedForm.fields.map(async (f) => {
|
||||
const fieldValue = values[f.name];
|
||||
if (!fieldValue) {
|
||||
if (!values[f.name]) {
|
||||
return;
|
||||
}
|
||||
switch (f.type) {
|
||||
case AppFieldTypes.DYNAMIC_SELECT:
|
||||
if (f.multiselect) {
|
||||
let commandValues: string[] = [];
|
||||
if (Array.isArray(fieldValue)) {
|
||||
commandValues = fieldValue as string[];
|
||||
} else {
|
||||
commandValues = [fieldValue] as string[];
|
||||
}
|
||||
|
||||
if (f.multiselect && Array.isArray(values[f.name])) {
|
||||
const options: AppSelectOption[] = [];
|
||||
const commandValues = values[f.name] as string[];
|
||||
for (const value of commandValues) {
|
||||
if (options.find((o) => o.value === value)) {
|
||||
errors[f.name] = this.intl.formatMessage({
|
||||
@@ -1212,7 +1202,7 @@ export class AppCommandParser {
|
||||
break;
|
||||
}
|
||||
|
||||
values[f.name] = {label: fieldValue, value: fieldValue};
|
||||
values[f.name] = {label: values[f.name], value: values[f.name]};
|
||||
break;
|
||||
case AppFieldTypes.STATIC_SELECT: {
|
||||
const getOption = (value: string) => {
|
||||
@@ -1230,15 +1220,9 @@ export class AppCommandParser {
|
||||
values[f.name] = undefined;
|
||||
};
|
||||
|
||||
if (f.multiselect) {
|
||||
let commandValues: string[] = [];
|
||||
if (Array.isArray(fieldValue)) {
|
||||
commandValues = fieldValue as string[];
|
||||
} else {
|
||||
commandValues = [fieldValue] as string[];
|
||||
}
|
||||
|
||||
if (f.multiselect && Array.isArray(values[f.name])) {
|
||||
const options: AppSelectOption[] = [];
|
||||
const commandValues = values[f.name] as string[];
|
||||
for (const value of commandValues) {
|
||||
const option = getOption(value);
|
||||
if (!option) {
|
||||
@@ -1260,9 +1244,9 @@ export class AppCommandParser {
|
||||
break;
|
||||
}
|
||||
|
||||
const option = getOption(fieldValue);
|
||||
const option = getOption(values[f.name]);
|
||||
if (!option) {
|
||||
setOptionError(fieldValue);
|
||||
setOptionError(values[f.name]);
|
||||
return;
|
||||
}
|
||||
values[f.name] = option;
|
||||
@@ -1291,15 +1275,9 @@ export class AppCommandParser {
|
||||
});
|
||||
};
|
||||
|
||||
if (f.multiselect) {
|
||||
let commandValues: string[] = [];
|
||||
if (Array.isArray(fieldValue)) {
|
||||
commandValues = fieldValue as string[];
|
||||
} else {
|
||||
commandValues = [fieldValue] as string[];
|
||||
}
|
||||
|
||||
if (f.multiselect && Array.isArray(values[f.name])) {
|
||||
const options: AppSelectOption[] = [];
|
||||
const commandValues = values[f.name] as string[];
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const value of commandValues) {
|
||||
let userName = value;
|
||||
@@ -1363,15 +1341,9 @@ export class AppCommandParser {
|
||||
});
|
||||
};
|
||||
|
||||
if (f.multiselect) {
|
||||
let commandValues: string[] = [];
|
||||
if (Array.isArray(fieldValue)) {
|
||||
commandValues = fieldValue as string[];
|
||||
} else {
|
||||
commandValues = [fieldValue] as string[];
|
||||
}
|
||||
|
||||
if (f.multiselect && Array.isArray(values[f.name])) {
|
||||
const options: AppSelectOption[] = [];
|
||||
const commandValues = values[f.name] as string[];
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const value of commandValues) {
|
||||
let channelName = value;
|
||||
@@ -1463,7 +1435,11 @@ export class AppCommandParser {
|
||||
// getCommandBindings returns the commands in the redux store.
|
||||
// They are grouped by app id since each app has one base command
|
||||
private getCommandBindings = (): AppBinding[] => {
|
||||
return AppsManager.getBindings(this.serverUrl, AppBindingLocations.COMMAND, Boolean(this.rootPostID));
|
||||
const manager = IntegrationsManager.getManager(this.serverUrl);
|
||||
if (this.rootPostID) {
|
||||
return manager.getRHSCommandBindings();
|
||||
}
|
||||
return manager.getCommandBindings();
|
||||
};
|
||||
|
||||
// getChannel gets the channel in which the user is typing the command
|
||||
@@ -1562,9 +1538,10 @@ export class AppCommandParser {
|
||||
};
|
||||
|
||||
public getSubmittableForm = async (location: string, binding: AppBinding): Promise<{form?: AppForm; error?: string} | undefined> => {
|
||||
const manager = IntegrationsManager.getManager(this.serverUrl);
|
||||
const rootID = this.rootPostID || '';
|
||||
const key = `${this.channelID}-${rootID}-${location}`;
|
||||
const submittableForm = AppsManager.getCommandForm(this.serverUrl, key, Boolean(this.rootPostID));
|
||||
const submittableForm = this.rootPostID ? manager.getAppRHSCommandForm(key) : manager.getAppCommandForm(key);
|
||||
if (submittableForm) {
|
||||
return {form: submittableForm};
|
||||
}
|
||||
@@ -1578,7 +1555,11 @@ export class AppCommandParser {
|
||||
const context = await this.getAppContext(binding);
|
||||
const fetched = await this.fetchSubmittableForm(binding.form.source, context);
|
||||
if (fetched?.form) {
|
||||
AppsManager.setCommandForm(this.serverUrl, key, fetched.form, Boolean(this.rootPostID));
|
||||
if (this.rootPostID) {
|
||||
manager.setAppRHSCommandForm(key, fetched.form);
|
||||
} else {
|
||||
manager.setAppCommandForm(key, fetched.form);
|
||||
}
|
||||
}
|
||||
return fetched;
|
||||
};
|
||||
@@ -1715,13 +1696,7 @@ export class AppCommandParser {
|
||||
prefix = '';
|
||||
}
|
||||
|
||||
const applicable = parsed.resolvedForm.fields.filter((field) => (
|
||||
field.label &&
|
||||
field.label.toLowerCase().startsWith(parsed.incomplete.toLowerCase()) &&
|
||||
!parsed.values[field.name] &&
|
||||
!field.readonly &&
|
||||
field.type !== AppFieldTypes.MARKDOWN
|
||||
));
|
||||
const applicable = parsed.resolvedForm.fields.filter((field) => field.label && field.label.toLowerCase().startsWith(parsed.incomplete.toLowerCase()) && !parsed.values[field.name]);
|
||||
if (applicable) {
|
||||
return applicable.map((f) => {
|
||||
return {
|
||||
|
||||
@@ -10,11 +10,15 @@ import AtMentionItem from '@components/autocomplete/at_mention_item';
|
||||
import ChannelMentionItem from '@components/autocomplete/channel_mention_item';
|
||||
import {COMMAND_SUGGESTION_CHANNEL, COMMAND_SUGGESTION_USER} from '@constants/apps';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import analytics from '@managers/analytics';
|
||||
|
||||
import {AppCommandParser, ExtendedAutocompleteSuggestion} from '../app_command_parser/app_command_parser';
|
||||
import SlashSuggestionItem from '../slash_suggestion_item';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export type Props = {
|
||||
currentTeamId: string;
|
||||
isSearch?: boolean;
|
||||
@@ -28,20 +32,7 @@ export type Props = {
|
||||
listStyle: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
const keyExtractor = (item: ExtendedAutocompleteSuggestion): string => {
|
||||
switch (item.type) {
|
||||
case COMMAND_SUGGESTION_USER: {
|
||||
const user = item.item as UserProfile;
|
||||
return user.id;
|
||||
}
|
||||
case COMMAND_SUGGESTION_CHANNEL: {
|
||||
const channel = item.item as Channel;
|
||||
return channel.id;
|
||||
}
|
||||
default:
|
||||
return item.Suggestion;
|
||||
}
|
||||
};
|
||||
const keyExtractor = (item: ExtendedAutocompleteSuggestion): string => item.Suggestion + item.type + item.item;
|
||||
|
||||
const emptySuggestonList: AutocompleteSuggestion[] = [];
|
||||
|
||||
@@ -57,8 +48,9 @@ const AppSlashSuggestion = ({
|
||||
listStyle,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId));
|
||||
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme));
|
||||
const [dataSource, setDataSource] = useState<AutocompleteSuggestion[]>(emptySuggestonList);
|
||||
const active = isAppsEnabled && Boolean(dataSource.length);
|
||||
const mounted = useRef(false);
|
||||
@@ -106,34 +98,28 @@ const AppSlashSuggestion = ({
|
||||
|
||||
const renderItem = useCallback(({item}: {item: ExtendedAutocompleteSuggestion}) => {
|
||||
switch (item.type) {
|
||||
case COMMAND_SUGGESTION_USER: {
|
||||
const user = item.item as UserProfile | undefined;
|
||||
if (!user) {
|
||||
case COMMAND_SUGGESTION_USER:
|
||||
if (!item.item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AtMentionItem
|
||||
user={user}
|
||||
user={item.item as UserProfile | UserModel}
|
||||
onPress={completeIgnoringSuggestion(item.Complete)}
|
||||
testID='autocomplete.slash_suggestion.at_mention_item'
|
||||
/>
|
||||
);
|
||||
}
|
||||
case COMMAND_SUGGESTION_CHANNEL: {
|
||||
const channel = item.item as Channel | undefined;
|
||||
if (!channel) {
|
||||
case COMMAND_SUGGESTION_CHANNEL:
|
||||
if (!item.item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChannelMentionItem
|
||||
channel={channel}
|
||||
channel={item.item as Channel | ChannelModel}
|
||||
onPress={completeIgnoringSuggestion(item.Complete)}
|
||||
testID='autocomplete.slash_suggestion.channel_mention_item'
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<SlashSuggestionItem
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||
import {observeConfigBooleanValue, observeCurrentTeamId} from '@queries/servers/system';
|
||||
|
||||
import SlashSuggestion from './slash_suggestion';
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
isAppsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(SlashSuggestion));
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
|
||||
import {fetchSuggestions} from '@actions/remote/command';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import analytics from '@managers/analytics';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
|
||||
@@ -77,8 +78,9 @@ const SlashSuggestion = ({
|
||||
listStyle,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId));
|
||||
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme));
|
||||
const mounted = useRef(false);
|
||||
const [noResultsTerm, setNoResultsTerm] = useState<string|null>(null);
|
||||
|
||||
|
||||
@@ -33,12 +33,12 @@ type AutoCompleteSelectorProps = {
|
||||
getDynamicOptions?: (userInput?: string) => Promise<DialogOption[]>;
|
||||
helpText?: string;
|
||||
label?: string;
|
||||
onSelected?: (value: SelectedDialogOption) => void;
|
||||
onSelected?: (value: string | string[]) => void;
|
||||
optional?: boolean;
|
||||
options?: DialogOption[];
|
||||
options?: PostActionOption[];
|
||||
placeholder?: string;
|
||||
roundedBorders?: boolean;
|
||||
selected?: SelectedDialogValue;
|
||||
selected?: string | string[];
|
||||
showRequiredAsterisk?: boolean;
|
||||
teammateNameDisplay: string;
|
||||
isMultiselect?: boolean;
|
||||
@@ -89,11 +89,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
async function getItemName(serverUrl: string, selected: string, teammateNameDisplay: string, intl: IntlShape, dataSource?: string, options?: DialogOption[]): Promise<string> {
|
||||
if (!selected) {
|
||||
return '';
|
||||
}
|
||||
|
||||
async function getItemName(serverUrl: string, selected: string, teammateNameDisplay: string, intl: IntlShape, dataSource?: string, options?: PostActionOption[]) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
|
||||
switch (dataSource) {
|
||||
@@ -101,7 +97,6 @@ async function getItemName(serverUrl: string, selected: string, teammateNameDisp
|
||||
if (!database) {
|
||||
return intl.formatMessage({id: 'channel_loader.someone', defaultMessage: 'Someone'});
|
||||
}
|
||||
|
||||
const user = await getUserById(database, selected);
|
||||
return displayUsername(user, intl.locale, teammateNameDisplay, true);
|
||||
}
|
||||
@@ -109,17 +104,15 @@ async function getItemName(serverUrl: string, selected: string, teammateNameDisp
|
||||
if (!database) {
|
||||
return intl.formatMessage({id: 'autocomplete_selector.unknown_channel', defaultMessage: 'Unknown channel'});
|
||||
}
|
||||
|
||||
const channel = await getChannelById(database, selected);
|
||||
return channel?.displayName || intl.formatMessage({id: 'autocomplete_selector.unknown_channel', defaultMessage: 'Unknown channel'});
|
||||
}
|
||||
default:
|
||||
return options?.find((o) => o.value === selected)?.text || selected;
|
||||
}
|
||||
|
||||
const option = options?.find((opt) => opt.value === selected);
|
||||
return option?.text || '';
|
||||
}
|
||||
|
||||
function getTextAndValueFromSelectedItem(item: Selection, teammateNameDisplay: string, locale: string, dataSource?: string) {
|
||||
function getTextAndValueFromSelectedItem(item: DialogOption | Channel | UserProfile, teammateNameDisplay: string, locale: string, dataSource?: string) {
|
||||
if (dataSource === ViewConstants.DATA_SOURCE_USERS) {
|
||||
const user = item as UserProfile;
|
||||
return {text: displayUsername(user, locale, teammateNameDisplay), value: user.id};
|
||||
@@ -127,7 +120,8 @@ function getTextAndValueFromSelectedItem(item: Selection, teammateNameDisplay: s
|
||||
const channel = item as Channel;
|
||||
return {text: channel.display_name, value: channel.id};
|
||||
}
|
||||
return item as DialogOption;
|
||||
const option = item as DialogOption;
|
||||
return option;
|
||||
}
|
||||
|
||||
function AutoCompleteSelector({
|
||||
@@ -143,28 +137,34 @@ function AutoCompleteSelector({
|
||||
|
||||
const goToSelectorScreen = useCallback(preventDoubleTap(() => {
|
||||
const screen = Screens.INTEGRATION_SELECTOR;
|
||||
goToScreen(screen, title, {dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect, teammateNameDisplay});
|
||||
goToScreen(screen, title, {dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect});
|
||||
}), [dataSource, options, getDynamicOptions]);
|
||||
|
||||
const handleSelect = useCallback((newSelection?: Selection) => {
|
||||
if (!newSelection) {
|
||||
const handleSelect = useCallback((item?: Selection) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(newSelection)) {
|
||||
const selectedOption = getTextAndValueFromSelectedItem(newSelection, teammateNameDisplay, intl.locale, dataSource);
|
||||
setItemText(selectedOption.text);
|
||||
if (!Array.isArray(item)) {
|
||||
const {text: selectedText, value: selectedValue} = getTextAndValueFromSelectedItem(item, teammateNameDisplay, intl.locale, dataSource);
|
||||
setItemText(selectedText);
|
||||
|
||||
if (onSelected) {
|
||||
onSelected(selectedOption);
|
||||
onSelected(selectedValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOptions = newSelection.map((option) => getTextAndValueFromSelectedItem(option, teammateNameDisplay, intl.locale, dataSource));
|
||||
setItemText(selectedOptions.map((option) => option.text).join(', '));
|
||||
const allSelectedTexts = [];
|
||||
const allSelectedValues = [];
|
||||
for (const i of item) {
|
||||
const {text: selectedText, value: selectedValue} = getTextAndValueFromSelectedItem(i, teammateNameDisplay, intl.locale, dataSource);
|
||||
allSelectedTexts.push(selectedText);
|
||||
allSelectedValues.push(selectedValue);
|
||||
}
|
||||
setItemText(allSelectedTexts.join(', '));
|
||||
if (onSelected) {
|
||||
onSelected(selectedOptions);
|
||||
onSelected(allSelectedValues);
|
||||
}
|
||||
}, [teammateNameDisplay, intl, dataSource]);
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native';
|
||||
import RNButton from 'react-native-button';
|
||||
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
backgroundStyle?: StyleProp<ViewStyle>;
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
size?: ButtonSize;
|
||||
emphasis?: ButtonEmphasis;
|
||||
buttonType?: ButtonType;
|
||||
buttonState?: ButtonState;
|
||||
testID?: string;
|
||||
onPress: () => void;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
theme,
|
||||
backgroundStyle,
|
||||
textStyle,
|
||||
size,
|
||||
emphasis,
|
||||
buttonType,
|
||||
buttonState,
|
||||
onPress,
|
||||
text,
|
||||
testID,
|
||||
}: Props) => {
|
||||
const bgStyle = useMemo(() => [
|
||||
buttonBackgroundStyle(theme, size, emphasis, buttonType, buttonState),
|
||||
backgroundStyle,
|
||||
], [theme, backgroundStyle, size, emphasis, buttonType, buttonState]);
|
||||
|
||||
const txtStyle = useMemo(() => [
|
||||
buttonTextStyle(theme, size, emphasis, buttonType),
|
||||
textStyle,
|
||||
], [theme, textStyle, size, emphasis, buttonType]);
|
||||
|
||||
return (
|
||||
<RNButton
|
||||
containerStyle={bgStyle}
|
||||
onPress={onPress}
|
||||
testID={testID}
|
||||
>
|
||||
<Text
|
||||
style={txtStyle}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</RNButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
@@ -7,7 +7,6 @@ import {useIntl} from 'react-intl';
|
||||
|
||||
import OptionItem from '@components/option_item';
|
||||
import SlideUpPanelItem from '@components/slide_up_panel_item';
|
||||
import {CHANNEL_INFO} from '@constants/screens';
|
||||
import {SNACK_BAR_TYPE} from '@constants/snack_bar';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
@@ -27,7 +26,7 @@ const CopyChannelLinkOption = ({channelName, teamName, showAsLabel, testID}: Pro
|
||||
const onCopyLink = useCallback(async () => {
|
||||
Clipboard.setString(`${serverUrl}/${teamName}/channels/${channelName}`);
|
||||
await dismissBottomSheet();
|
||||
showSnackBar({barType: SNACK_BAR_TYPE.LINK_COPIED, sourceScreen: CHANNEL_INFO});
|
||||
showSnackBar({barType: SNACK_BAR_TYPE.LINK_COPIED});
|
||||
}, [channelName, teamName, serverUrl]);
|
||||
|
||||
if (showAsLabel) {
|
||||
|
||||
@@ -103,7 +103,7 @@ const ChannelIcon = ({
|
||||
let unreadGroup;
|
||||
let mutedStyle;
|
||||
|
||||
if (isUnread && !isMuted) {
|
||||
if (isUnread) {
|
||||
unreadIcon = styles.iconUnread;
|
||||
unreadGroupBox = styles.groupBoxUnread;
|
||||
unreadGroup = styles.groupUnread;
|
||||
@@ -116,7 +116,7 @@ const ChannelIcon = ({
|
||||
}
|
||||
|
||||
if (isInfo) {
|
||||
activeIcon = isUnread && !isMuted ? styles.iconInfoUnread : styles.iconInfo;
|
||||
activeIcon = isUnread ? styles.iconInfoUnread : styles.iconInfo;
|
||||
activeGroupBox = styles.groupBoxInfo;
|
||||
activeGroup = isUnread ? styles.groupInfoUnread : styles.groupInfo;
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ const ChannelListItem = ({
|
||||
}, [channel.id]);
|
||||
|
||||
const textStyles = useMemo(() => [
|
||||
isBolded && !isMuted ? textStyle.bold : textStyle.regular,
|
||||
isBolded ? textStyle.bold : textStyle.regular,
|
||||
styles.text,
|
||||
isBolded && styles.highlight,
|
||||
isActive && isTablet && !isInfo ? styles.textActive : null,
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeConfig} from '@queries/servers/system';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
import {getUserCustomStatus, isCustomStatusExpired} from '@utils/user';
|
||||
|
||||
@@ -21,8 +21,9 @@ type HeaderInputProps = {
|
||||
const enhanced = withObservables(
|
||||
['userId'],
|
||||
({userId, database}: WithDatabaseArgs & HeaderInputProps) => {
|
||||
const config = observeConfig(database);
|
||||
const user = observeUser(database, userId);
|
||||
const isCustomStatusEnabled = observeConfigBooleanValue(database, 'EnableCustomUserStatuses');
|
||||
const isCustomStatusEnabled = config.pipe(map((cfg) => cfg?.EnableCustomUserStatuses === 'true'));
|
||||
const customStatus = user.pipe(switchMap((u) => (u?.isBot ? of$(undefined) : of$(getUserCustomStatus(u)))));
|
||||
const customStatusExpired = user.pipe(switchMap((u) => (u?.isBot ? of$(false) : of$(isCustomStatusExpired(u)))));
|
||||
|
||||
|
||||
@@ -1,470 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/channel_list_row should be selected 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"paddingVertical": 9,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.channel"
|
||||
>
|
||||
<Icon
|
||||
name="circle-multiple-outline"
|
||||
size={20}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.56)",
|
||||
"padding": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flexDirection": "column",
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.channel.display_name"
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.64)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
My purpose
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Icon
|
||||
color="#1c58d9"
|
||||
name="check-circle"
|
||||
size={28}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`components/channel_list_row should match snapshot with delete_at filled in 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"paddingVertical": 9,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.channel"
|
||||
>
|
||||
<Icon
|
||||
name="archive-outline"
|
||||
size={20}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.56)",
|
||||
"padding": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flexDirection": "column",
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.channel.display_name"
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`components/channel_list_row should match snapshot with open channel icon 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"paddingVertical": 9,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.channel"
|
||||
>
|
||||
<Icon
|
||||
name="circle-multiple-outline"
|
||||
size={20}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.56)",
|
||||
"padding": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flexDirection": "column",
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.channel.display_name"
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`components/channel_list_row should match snapshot with private channel icon 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"paddingVertical": 9,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.channel"
|
||||
>
|
||||
<Icon
|
||||
name="circle-multiple-outline"
|
||||
size={20}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.56)",
|
||||
"padding": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flexDirection": "column",
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.channel.display_name"
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`components/channel_list_row should match snapshot with purpose filled in 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"paddingVertical": 9,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.channel"
|
||||
>
|
||||
<Icon
|
||||
name="circle-multiple-outline"
|
||||
size={20}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.56)",
|
||||
"padding": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flexDirection": "column",
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.channel.display_name"
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.64)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
My purpose
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`components/channel_list_row should match snapshot with shared filled in 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"paddingVertical": 9,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.channel"
|
||||
>
|
||||
<Icon
|
||||
name="circle-multiple-outline"
|
||||
size={20}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.56)",
|
||||
"padding": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flexDirection": "column",
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.channel.display_name"
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
@@ -1,151 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Database from '@nozbe/watermelondb/Database';
|
||||
import React from 'react';
|
||||
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import ChannelListRow from '.';
|
||||
|
||||
describe('components/channel_list_row', () => {
|
||||
let database: Database;
|
||||
const channel: Channel = {
|
||||
id: '1',
|
||||
create_at: 1111,
|
||||
update_at: 1111,
|
||||
delete_at: 0,
|
||||
team_id: 'my team',
|
||||
type: 'O',
|
||||
display_name: 'channel',
|
||||
name: 'channel',
|
||||
header: 'channel',
|
||||
purpose: '',
|
||||
last_post_at: 1,
|
||||
total_msg_count: 1,
|
||||
extra_update_at: 1,
|
||||
creator_id: '1',
|
||||
scheme_id: null,
|
||||
group_constrained: null,
|
||||
shared: true,
|
||||
};
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase();
|
||||
database = server.database;
|
||||
});
|
||||
|
||||
it('should match snapshot with open channel icon', () => {
|
||||
const wrapper = renderWithEverything(
|
||||
<ChannelListRow
|
||||
channel={channel}
|
||||
selected={false}
|
||||
selectable={false}
|
||||
testID='ChannelListRow'
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with private channel icon', () => {
|
||||
channel.type = 'P';
|
||||
|
||||
const wrapper = renderWithEverything(
|
||||
<ChannelListRow
|
||||
channel={channel}
|
||||
selected={false}
|
||||
selectable={false}
|
||||
testID='ChannelListRow'
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with delete_at filled in', () => {
|
||||
channel.delete_at = 1111;
|
||||
channel.shared = false;
|
||||
channel.type = 'O';
|
||||
|
||||
const wrapper = renderWithEverything(
|
||||
<ChannelListRow
|
||||
channel={channel}
|
||||
testID='ChannelListRow'
|
||||
selectable={false}
|
||||
selected={false}
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with shared filled in', () => {
|
||||
channel.delete_at = 0;
|
||||
channel.shared = true;
|
||||
channel.type = 'O';
|
||||
|
||||
const wrapper = renderWithEverything(
|
||||
<ChannelListRow
|
||||
channel={channel}
|
||||
testID='ChannelListRow'
|
||||
selectable={false}
|
||||
selected={false}
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with purpose filled in', () => {
|
||||
channel.purpose = 'My purpose';
|
||||
|
||||
const wrapper = renderWithEverything(
|
||||
<ChannelListRow
|
||||
channel={channel}
|
||||
testID='ChannelListRow'
|
||||
selectable={false}
|
||||
selected={false}
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should be selected', () => {
|
||||
const wrapper = renderWithEverything(
|
||||
<ChannelListRow
|
||||
channel={channel}
|
||||
testID='ChannelListRow'
|
||||
selectable={true}
|
||||
selected={true}
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,7 @@ type FileProps = {
|
||||
galleryIdentifier: string;
|
||||
index: number;
|
||||
inViewPort: boolean;
|
||||
isSingleImage?: boolean;
|
||||
isSingleImage: boolean;
|
||||
nonVisibleImagesCount: number;
|
||||
onPress: (index: number) => void;
|
||||
publicLinkEnabled: boolean;
|
||||
|
||||
@@ -6,7 +6,7 @@ import withObservables from '@nozbe/with-observables';
|
||||
import {combineLatest, of as of$, from as from$} from 'rxjs';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfigBooleanValue, observeLicense} from '@queries/servers/system';
|
||||
import {observeConfig, observeLicense} from '@queries/servers/system';
|
||||
import {fileExists} from '@utils/file';
|
||||
|
||||
import Files from './files';
|
||||
@@ -36,8 +36,14 @@ const filesLocalPathValidation = async (files: FileModel[], authorId: string) =>
|
||||
};
|
||||
|
||||
const enhance = withObservables(['post'], ({database, post}: EnhanceProps) => {
|
||||
const enableMobileFileDownload = observeConfigBooleanValue(database, 'EnableMobileFileDownload');
|
||||
const publicLinkEnabled = observeConfigBooleanValue(database, 'EnablePublicLink');
|
||||
const config = observeConfig(database);
|
||||
const enableMobileFileDownload = config.pipe(
|
||||
switchMap((cfg) => of$(cfg?.EnableMobileFileDownload !== 'false')),
|
||||
);
|
||||
|
||||
const publicLinkEnabled = config.pipe(
|
||||
switchMap((cfg) => of$(cfg?.EnablePublicLink !== 'false')),
|
||||
);
|
||||
|
||||
const complianceDisabled = observeLicense(database).pipe(
|
||||
switchMap((lcs) => of$(lcs?.IsLicensed === 'false' || lcs?.Compliance === 'false')),
|
||||
|
||||
@@ -45,9 +45,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
zIndex: 10,
|
||||
maxWidth: 315,
|
||||
},
|
||||
readOnly: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelBg, 0.16),
|
||||
},
|
||||
smallLabel: {
|
||||
fontSize: 10,
|
||||
},
|
||||
@@ -194,9 +191,6 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
||||
|
||||
const combinedTextInputStyle = useMemo(() => {
|
||||
const res: StyleProp<TextStyle> = [styles.textInput];
|
||||
if (!editable) {
|
||||
res.push(styles.readOnly);
|
||||
}
|
||||
res.push({
|
||||
borderWidth: focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH,
|
||||
height: DEFAULT_INPUT_HEIGHT + ((focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH) * 2),
|
||||
@@ -215,7 +209,7 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
||||
}
|
||||
|
||||
return res;
|
||||
}, [styles, theme, shouldShowError, focused, textInputStyle, focusedLabel, multiline, editable]);
|
||||
}, [styles, theme, shouldShowError, focused, textInputStyle, focusedLabel, multiline]);
|
||||
|
||||
const textAnimatedTextStyle = useAnimatedStyle(() => {
|
||||
const inputText = placeholder || value;
|
||||
|
||||
@@ -5,8 +5,6 @@ import moment from 'moment-timezone';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Text, TextProps} from 'react-native';
|
||||
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
|
||||
type FormattedRelativeTimeProps = TextProps & {
|
||||
timezone?: UserTimezone | string;
|
||||
value: number | string | Date;
|
||||
@@ -26,10 +24,7 @@ const FormattedRelativeTime = ({timezone, value, updateIntervalInSeconds, ...pro
|
||||
const [formattedTime, setFormattedTime] = useState(getFormattedRelativeTime);
|
||||
useEffect(() => {
|
||||
if (updateIntervalInSeconds) {
|
||||
const interval = setInterval(
|
||||
() => setFormattedTime(getFormattedRelativeTime()),
|
||||
toMilliseconds({seconds: updateIntervalInSeconds}),
|
||||
);
|
||||
const interval = setInterval(() => setFormattedTime(getFormattedRelativeTime()), updateIntervalInSeconds * 1000);
|
||||
return function cleanup() {
|
||||
return clearInterval(interval);
|
||||
};
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import * as React from 'react';
|
||||
import Svg, {
|
||||
Path,
|
||||
Mask,
|
||||
G,
|
||||
Rect,
|
||||
} from 'react-native-svg';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
function ReviewAppIllustration({theme}: Props) {
|
||||
return (
|
||||
<Svg
|
||||
width={222}
|
||||
height={205}
|
||||
viewBox='0 0 222 205'
|
||||
fill='none'
|
||||
>
|
||||
<Path
|
||||
d='M166.305 43.157c0-3.319 2.79-6.01 6.232-6.01h43.231c3.442 0 6.232 2.691 6.232 6.01v41.69c0 3.32-2.79 6.01-6.232 6.01h-43.231c-3.442 0-6.232-2.69-6.232-6.01v-41.69Z'
|
||||
fill={theme.centerChannelColor}
|
||||
fillOpacity={0.08}
|
||||
/>
|
||||
<Path
|
||||
d='M194.309 76.927a.835.835 0 0 0-.517 0l-11.006 4.04a.914.914 0 0 1-.75-.118.7.7 0 0 1-.302-.618l.6-11.294a.854.854 0 0 0-.171-.496l-7.394-8.828a.652.652 0 0 1-.157-.313.63.63 0 0 1 .03-.346.913.913 0 0 1 .469-.496l11.435-2.968c.09 0 .178-.024.255-.067a.496.496 0 0 0 .184-.181l6.447-9.483a.775.775 0 0 1 .28-.24.804.804 0 0 1 1.009.24l6.447 9.483a.525.525 0 0 0 .171.224c.078.059.17.096.267.108l11.352 2.884c.124.04.238.105.334.19a.89.89 0 0 1 .223.305.64.64 0 0 1-.127.66l-7.394 8.828a.813.813 0 0 0-.175.495l.604 11.295a.65.65 0 0 1-.083.355.695.695 0 0 1-.263.263.86.86 0 0 1-.688.122L194.3 76.93l.009-.004Z'
|
||||
fill='#FFBC1F'
|
||||
/>
|
||||
<Rect
|
||||
x={95.794}
|
||||
y={168.252}
|
||||
width={19.722}
|
||||
height={3.26}
|
||||
rx={1.63}
|
||||
fill='#8D93A5'
|
||||
/>
|
||||
<Path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M144.9 178.534c-2.206 2.177-5.351 3.846-8.8 3.846H75.773c-3.449 0-6.593-1.669-8.799-3.846-2.21-2.182-3.863-5.252-3.863-8.573V35.039c0-3.322 1.65-6.392 3.86-8.574 2.205-2.178 5.349-3.845 8.797-3.845H136.1c3.448 0 6.593 1.666 8.799 3.844 2.212 2.182 3.863 5.252 3.863 8.575v134.922c0 3.321-1.652 6.391-3.862 8.573Zm-8.8.586c4.641 0 9.281-4.585 9.281-9.159V35.039c0-4.58-4.64-9.159-9.281-9.159H75.768c-4.64 0-9.276 4.58-9.276 9.16V169.96c0 4.574 4.64 9.159 9.281 9.159H136.1Z'
|
||||
fill='#363A45'
|
||||
/>
|
||||
<Path
|
||||
d='M145.381 169.961c0 4.574-4.64 9.159-9.281 9.159H75.773c-4.64 0-9.28-4.585-9.28-9.159V35.039c0-4.58 4.634-9.159 9.275-9.159H136.1c4.641 0 9.281 4.58 9.281 9.16V169.96Z'
|
||||
fill='#3F4350'
|
||||
/>
|
||||
<Path
|
||||
d='M112.699 33.758c0 .451-.296.817-.671.817H99.851c-.37 0-.676-.377-.676-.817 0-.44.307-.814.676-.814h12.157c.374 0 .691.366.691.814Z'
|
||||
fill='#8D93A5'
|
||||
/>
|
||||
<Path
|
||||
fill={theme.centerChannelBg}
|
||||
d='M69.873 42.726H142v120.092H69.873z'
|
||||
/>
|
||||
<Mask
|
||||
id='a'
|
||||
maskUnits='userSpaceOnUse'
|
||||
x={69}
|
||||
y={43}
|
||||
width={134}
|
||||
height={120}
|
||||
>
|
||||
<Path
|
||||
fill='#FFFFFF'
|
||||
d='M69.716 43.157h132.421v119.812H69.716z'
|
||||
/>
|
||||
</Mask>
|
||||
<G mask='url(#a)'>
|
||||
<Path
|
||||
d='m89.041 85.762.504-14.438-1.145-5.718c2.096-5.199 5.176-4.835 7.066-5 4.306-.376 7.18-2.65 9.161-.64 1.077 1.203 2.291 6.115 2.485 9.934.138 2.02.871 3.742-.687 4.757a21.35 21.35 0 0 1-3.355 1.413l.572 10.807-14.6-1.115Z'
|
||||
fill='#AD831F'
|
||||
/>
|
||||
<Path
|
||||
d='M105.784 65.33a21.555 21.555 0 0 1 3.882 3.312c.355 1.512-3.962 2.285-3.962 2.285l.08-5.597Z'
|
||||
fill='#AD831F'
|
||||
/>
|
||||
<Path
|
||||
d='M98.489 76.644c1.527.161 3.072-.024 4.512-.54.538-.21-.996.728-1.546.916-.707.35-1.498.51-2.29.463-.55-.088-1.26-.883-.676-.839Z'
|
||||
fill='#7A5600'
|
||||
/>
|
||||
<Path
|
||||
d='M110.433 58.243a1.927 1.927 0 0 0-.966-.99 2.036 2.036 0 0 0-1.404-.113c-4.066.65-3.298-2.76-9.288-1.789-4.168.674-8.566 0-12.253 2.043a7.734 7.734 0 0 0-3.619 3.465 3.458 3.458 0 0 0-.21 2.424 3.58 3.58 0 0 0 1.447 1.992c.252.143.55.265.676.519.08.264.056.547-.069.794-.515 1.49-.973 3.312.183 4.416.642.618 1.718.905 1.97 1.744.137.463 0 .96.057 1.446.24 1.28 2.29 1.49 3.264.585a5.392 5.392 0 0 0 1.271-3.643c.103-1.159.137-2.494-.756-3.311-.412-.376-.996-.596-1.214-1.104a1.45 1.45 0 0 1 .046-.93c.118-.298.33-.552.607-.726a3.237 3.237 0 0 1 1.24-.505 3.29 3.29 0 0 1 1.346.036c.44.105.853.299 1.209.57.356.272.647.614.855 1.003.332.728.435 1.689 1.145 2.042.927.442 1.98-.552 2.107-1.534a16.322 16.322 0 0 0-.31-2.959c0-.993.527-2.207 1.558-2.207.34.017.677.084.996.198 2.023.55 4.157.603 6.207.155a6.497 6.497 0 0 0 2.817-1.104c.402-.284.716-.669.908-1.112.191-.444.254-.93.18-1.405Z'
|
||||
fill='#4A2407'
|
||||
/>
|
||||
<Path
|
||||
d='m102.577 64.403.058-.11c-3.058-.266-6.872-.497-9.483-.464-2.61.033-3.813.386-5.176 2.65-.229.385.436.661.676.275.27-.62.715-1.154 1.284-1.544a3.846 3.846 0 0 1 1.934-.664c1.328-.099 2.703 0 4.02 0 1.683 0 4.294.232 6.47.43.068-.198.137-.385.217-.573Z'
|
||||
fill={theme.centerChannelBg}
|
||||
/>
|
||||
<Path
|
||||
d='M102.978 66.555a2.144 2.144 0 0 1-.751-.944 2.064 2.064 0 0 1-.108-1.186c.161-1.16 1.054-1.998 1.981-1.877.499.137.922.456 1.181.889s.332.945.205 1.43c-.16 1.103-1.053 1.998-1.981 1.876a1.311 1.311 0 0 1-.527-.188Zm1.374-3.311a.687.687 0 0 0-.32-.11c-.63-.089-1.237.54-1.363 1.401-.126.861.286 1.634.916 1.722.63.089 1.225-.552 1.351-1.413.078-.299.064-.613-.04-.905a1.595 1.595 0 0 0-.544-.74v.045Z'
|
||||
fill={theme.centerChannelBg}
|
||||
/>
|
||||
<Path
|
||||
d='M97.344 84.824c29.774.22 29.694-2.473 32.683 31.382 2.989 33.854 3.035 35.08-26.408 36.945-19.628 1.248-31.217 8.831-32.236-1.567-1.03-10.597-1.683-10.288-3.436-28.292-3.973-39.837-3.298-38.711 29.397-38.468Z'
|
||||
fill='#AD831F'
|
||||
/>
|
||||
<Path
|
||||
d='M110.72 109.848c8.497 9.294 27.69 16.48 42.52 16.48a24.612 24.612 0 0 0 6.275-.773 21.401 21.401 0 0 0 3.653-1.313c12.208-5.751 15.838-23.501 18.495-29.594 3.275-7.495-.126-2.837 2.084-7.12.79-1.534 2.737-2.528 3.367-3.311.904-1.104-.825-2.594-7.352 1.666-10.307 6.624-9.162 19.969-19.308 21.371a15.88 15.88 0 0 1-3.836 0 25.19 25.19 0 0 1-3.378-.585c-18.105-4.305-19.64-20.796-37.459-21.47 0-.044-13.57 15.288-5.061 24.649ZM17.629 97.452c-.344.927 1.145 5.519 1.546 9.724.985 10.134 4.214 25.687 19.216 31.625a18.309 18.309 0 0 0 5.966 1.215c1.3.055 2.604-.004 3.894-.177 9.16-1.104 19.227-7.539 24.22-17.573 6.87-13.853 9.665-37.354 9.665-37.354-6.619 0-22.903-1.225-28.824 23.986a13.288 13.288 0 0 1-2.394 5.456 13.886 13.886 0 0 1-4.58 3.949 10.907 10.907 0 0 1-3.642 1.104 9.984 9.984 0 0 1-5.085-.698 9.604 9.604 0 0 1-3.996-3.11c-8.886-11.469-9.986-23.81-16.387-25.024-6.402-1.215 2.073 2.362.4 6.877Z'
|
||||
fill='#AD831F'
|
||||
/>
|
||||
<Path
|
||||
d='M115.781 85.155c17.819.673 19.354 17.165 37.459 21.47 1.889.483 3.839.713 5.794.684.836 5.883 1.443 11.8 2.027 17.728a24.098 24.098 0 0 1-7.821 1.247c-14.83 0-34.023-7.186-42.52-16.491-8.498-9.306 5.061-24.638 5.061-24.638ZM44.804 118.932c3.825-1.247 7.26-4.647 8.508-10.034 5.875-25.211 22.205-23.997 28.813-23.986 0 0-2.76 23.501-9.654 37.354-5.566 11.171-17.36 17.827-27.324 17.761a69.32 69.32 0 0 0-.343-21.095Z'
|
||||
fill='#1E325C'
|
||||
/>
|
||||
<Path
|
||||
d='M131.355 135.92c17.361 49.055 18.415 69.2 16.8 86.099-1.615 16.9-14.406 34.286-31.091 47.013-3.516 2.406-7.913 4.106-5.131 10.663 4.043 9.537-6.596-2.992-9.253-9.228-2.656-6.237 25.515-17.088 19.01-50.39-6.504-33.303-23.957-46.924-34.87-70.491-10.914-23.567 44.535-13.666 44.535-13.666Z'
|
||||
fill={theme.buttonBg}
|
||||
/>
|
||||
<Path
|
||||
d='M131.355 135.92c17.361 49.055 18.415 69.2 16.8 86.099-1.615 16.9-14.406 34.286-31.091 47.013-3.516 2.406-7.913 4.106-5.131 10.663 4.043 9.537-6.596-2.992-9.253-9.228-2.656-6.237 25.515-17.088 19.01-50.39-6.504-33.303-23.957-46.924-34.87-70.491-10.914-23.567 44.535-13.666 44.535-13.666Z'
|
||||
fill='#000'
|
||||
fillOpacity={0.16}
|
||||
/>
|
||||
<Path
|
||||
d='M111.086 139.188c2.107 54.971-3.355 80.26-28.057 98.042-24.701 17.783-43.848 18.423-44.593 27.386-.744 8.964-2.977 6.91-3.847-2.285-1.145-12.484-6.55-3.212 22.159-24.56 28.71-21.348 15.15-77.599 13.742-94.267-1.409-16.668 40.596-4.316 40.596-4.316Z'
|
||||
fill={theme.buttonBg}
|
||||
/>
|
||||
<Path
|
||||
d='M38.081 255.466c14.407-6.844 29.385-14.472 36.073-29.418 7.444-16.634 4.272-35.322 3.802-52.785-.275-10.531 0-21.072.71-31.581 0-.474.802-.474.767 0-.607 9.659-.927 19.328-.755 29.009.171 9.681 1.145 19.361 1.145 29.064-.057 8.4-1.008 16.922-4.157 24.814-2.898 7.177-7.71 13.491-13.948 18.302-6.997 5.519-15.14 9.416-23.19 13.246-.504.199-.893-.442-.447-.651Z'
|
||||
fill='#1E325C'
|
||||
/>
|
||||
<Path
|
||||
d='M89.099 84.769a6.29 6.29 0 0 0 3.126 2.55c4.707 1.865 10.696 1.865 13.33-2.473 21.643 0 21.838.927 24.518 31.36 1.351 15.299 2.096 23.931-.195 28.976-11.944 4.978-25.675 6.623-38.191 8.963-5.451 1.015-10.925 1.876-16.41 2.638-2.222-.519-3.54-2.009-3.848-5.199-1.03-10.597-1.684-10.288-3.436-28.292-3.653-36.14-3.435-38.567 21.106-38.523Z'
|
||||
fill={theme.centerChannelBg}
|
||||
/>
|
||||
<Path
|
||||
d='M89.099 84.769a6.29 6.29 0 0 0 3.126 2.55c4.707 1.865 10.696 1.865 13.33-2.473 21.643 0 21.838.927 24.518 31.36 1.351 15.299 2.096 23.931-.195 28.976-11.944 4.978-25.675 6.623-38.191 8.963-5.451 1.015-10.925 1.876-16.41 2.638-2.222-.519-3.54-2.009-3.848-5.199-1.03-10.597-1.684-10.288-3.436-28.292-3.653-36.14-3.435-38.567 21.106-38.523Z'
|
||||
fill={theme.centerChannelColor}
|
||||
fillOpacity={0.08}
|
||||
/>
|
||||
<Path
|
||||
d='M118.198 142.775a81.05 81.05 0 0 1-7.662-18.412 215.276 215.276 0 0 1-6.252-39.528c22.903 0 25.193-.806 27.976 30.212.138 1.501-1.672 4.194-1.145 5.729 4.432 11.987 20.304 24.284 18.998 27.044-1.82 3.951-8.772 8.168-17.521 11.48a77.847 77.847 0 0 1-6.985 1.368 104.902 104.902 0 0 0-7.409-17.893ZM67.764 145.546c-.653 7.34-1.511 13.621-.973 19.24 5.027-.806 10.077-1.314 15.15-1.656a100.117 100.117 0 0 0 9.826-35.511c.664-7.34 2.714-35.532 2.21-39.738-.32-2.825-3.974-.441-4.718-3.135-24.69 0-27.335 1.767-23.716 37.983.171 1.811 2.702 4.107 2.782 5.818.206 5.672.019 11.351-.56 16.999Z'
|
||||
fill='#1E325C'
|
||||
/>
|
||||
<Path
|
||||
d='M105.864 83.422c1.695 1.314 1.386.95.229 3.013-1.157 2.065-1.741 3.93-2.691 1.424-.951-2.505-1.077-2.108.16-3.598 1.237-1.49 1.443-1.512 2.302-.84ZM91.687 83.841c3.836 1.623 2.428 1.248 3.16 3.687 1.592 5.243-.63 2.462-6.069-.563-3.699-2.064-3.264-2.384-1.97-3.642 1.294-1.259.905-1.204 4.673.518 3.767 1.722-3.642-1.59.206 0ZM156.618 107.21a214.295 214.295 0 0 0 2.897 18.301 21.401 21.401 0 0 0 3.653-1.313 203.658 203.658 0 0 1-2.714-16.999 15.88 15.88 0 0 1-3.836.011Z'
|
||||
fill={theme.buttonBg}
|
||||
/>
|
||||
</G>
|
||||
<Path
|
||||
d='M25.705 125.411c0-3.319 2.79-6.01 6.232-6.01h18.305c3.442 0 6.232 2.691 6.232 6.01v17.652c0 3.319-2.79 6.01-6.232 6.01H31.937c-3.442 0-6.232-2.691-6.232-6.01v-17.652Z'
|
||||
fill={theme.centerChannelColor}
|
||||
fillOpacity={0.08}
|
||||
/>
|
||||
<Path
|
||||
d='M31.28 134.233s.33-3.798 2.412-5.711c2.081-1.914 4.073-3.181 8.275-3.428 4.202-.247 6.836 3.841 7.243 4.57.408.729 1.617 2.892 1.617 4.569 0 1.676.013 5.199-2.478 6.763-2.492 1.564-4.889 2.476-6.608 2.389-1.72-.086-6.066-.433-8.014-3.042-1.947-2.608-2.447-3.442-2.447-6.11Z'
|
||||
fill='#FFBC1F'
|
||||
/>
|
||||
<Path
|
||||
d='M41.394 127.066a3.612 3.612 0 0 1 2.646.289.125.125 0 0 0 .145-.017.115.115 0 0 0 .022-.14c-.37-.652-1.336-1.841-2.928-.329a.114.114 0 0 0-.037.067.119.119 0 0 0 .073.125c.025.01.053.011.079.005Z'
|
||||
fill='#FFD470'
|
||||
/>
|
||||
<Path
|
||||
d='M37.474 141.656c-1.019-1.004-2.376-2.281-2.24-6.648.136-4.368 1.223-5.914 2.001-7.075.419-.63 1.895-1.933 3.703-2.753-3.519.384-5.35 1.593-7.257 3.342-2.091 1.911-2.411 5.709-2.411 5.709 0 2.668.51 3.504 2.457 6.11 1.948 2.607 6.292 2.956 8.014 3.042-.002 0-3.248-.717-4.267-1.727Z'
|
||||
fill='#CC8F00'
|
||||
/>
|
||||
<Path
|
||||
d='M42.017 140.642s5.438.099 6.392-4.787a.64.64 0 0 0-.118-.519.681.681 0 0 0-.21-.183.72.72 0 0 0-.268-.087 54.746 54.746 0 0 0-12.165.177.698.698 0 0 0-.462.267.65.65 0 0 0-.116.506c.264 1.363 1.465 4.307 6.947 4.626Z'
|
||||
fill='#6F370B'
|
||||
/>
|
||||
<Path
|
||||
d='M42.607 140.287s-3.322.239-4.709-1.096l.168-.121c.179-.102.37-.181.57-.237a4.687 4.687 0 0 1 1.068-.165c.209-.01.418-.014.638 0 .22.002.44.033.652.091.21.065.412.152.603.257.285.144.536.343.736.586.157.199.252.436.274.685Z'
|
||||
fill='#C43133'
|
||||
/>
|
||||
<Path
|
||||
d='M36.961 135.389s4.755-.283 10.074-.174c0 0-.272 1.022-5.183 1.022-1.109 0-4.686.413-4.89-.848Z'
|
||||
fill='#fff'
|
||||
/>
|
||||
<Path
|
||||
d='M39.256 131.17c.052 1.169-.418 2.137-1.063 2.163-.644.026-1.198-.9-1.255-2.068-.056-1.169.419-2.135 1.063-2.161.644-.026 1.205.896 1.255 2.066ZM46.498 131.176c.046 1.022-.37 1.87-.931 1.894-.56.024-1.046-.787-1.094-1.807-.048-1.02.37-1.87.93-1.894.561-.024 1.049.785 1.095 1.807Z'
|
||||
fill='#6F370B'
|
||||
/>
|
||||
<Path
|
||||
d='M0 66.444c0-3.32 2.79-6.01 6.232-6.01h36.22c3.442 0 6.232 2.69 6.232 6.01v34.929c0 3.319-2.79 6.01-6.231 6.01H6.232c-3.442 0-6.232-2.691-6.232-6.01v-34.93Z'
|
||||
fill={theme.centerChannelColor}
|
||||
fillOpacity={0.08}
|
||||
/>
|
||||
<Path
|
||||
d='M34.592 95.28s-3.856 1.454-4.48 1.671c-.623.217-2.508.983-2.956 1.126-.448.144-3.102 1.126-4.118 1.126-1.017 0-4.608-.4-4.608-.4l-2.133-.906-.754-1.126-.493-.719-1.647-1.416v-2.259l.262-.581-1.086-1.377-.345-2.034.748-1.634.19-.689-1.237-2.395.71-1.597s1.874-1.233 2.136-1.307a13.832 13.832 0 0 1 2.543 0c1.978.082 3.959.063 5.934-.056 1.444-.127 1.034-.902 1.034-.902l-1.475-3.108-.561-3.448s-.149-1.597.151-2.395c.3-.799 1.61-1.962 2.919-1.888 1.31.073.6 1.56.6 1.56l1.123 1.925s.748.668.861 1.052c.114.384.562 2.322.9 3.341.338 1.02 1.533 3.124 2.919 4.01 1.385.885 4.007 2.322 4.455 3.084.318.61.557 1.256.71 1.924l.345 2.339-.262 2.252-.758 3.124-1.627 1.704Z'
|
||||
fill='#FFBC1F'
|
||||
/>
|
||||
<Path
|
||||
d='M25.88 80.546s-3.704-2.65-3.445-9.689c0 0-1.496 5.56 1.199 9.803-.01-.004 1.485.247 2.247-.114ZM15.612 80.71s-2.499.732-2.647 2.279c-.148 1.546 0 2.566.948 3.244 0 0-2.995 3.34.344 5.516 0 0-2.047 3.725 1.847 4.26 0 0-1.278 3.097 5.9 2.903a16.222 16.222 0 0 0 7.478-2.131c1.034-.578 3.394-1.257 4.135-1.788.741-.531 2.809-2.275 3.046-5.713 0-1.985 0-3.846-.475-5.346 0 0 1.347 1.337 1.347 4.297s-.723 5.42-1.423 6.458c-.7 1.04-2.068 2.105-3.37 2.339-1.303.234-2.757.217-3.69.628-.935.411-3.447 1.38-5.442 1.574-2.118.127-4.244.03-6.34-.29-.849-.195-2.572-1.26-2.472-2.674 0 0-3.494-1.26-1.599-4.343 0 0-1.67-.822-1.67-3.12a2.91 2.91 0 0 1 .34-1.285c.212-.398.514-.745.882-1.014a3.355 3.355 0 0 1-1.25-1.306 3.227 3.227 0 0 1-.373-1.745c.166-2.158 2.437-2.967 4.484-2.743Z'
|
||||
fill='#CC8F00'
|
||||
/>
|
||||
<Path
|
||||
d='M27.838 73.31a4.725 4.725 0 0 1-2.007-1.786 4.521 4.521 0 0 1-.674-2.558c0-.361.917-.401 1.068 0 .152.4 1.523 3.782 1.613 4.343Z'
|
||||
fill='#FFD470'
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReviewAppIllustration;
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import * as React from 'react';
|
||||
import Svg, {
|
||||
Path,
|
||||
Ellipse,
|
||||
} from 'react-native-svg';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
function ShareFeedbackIllustration({theme}: Props) {
|
||||
return (
|
||||
<Svg
|
||||
width={172}
|
||||
height={172}
|
||||
viewBox='0 0 172 172'
|
||||
fill='none'
|
||||
>
|
||||
<Path
|
||||
d='M39.982 33h92.003a12.865 12.865 0 0 1 9.092 3.722 12.805 12.805 0 0 1 3.791 9.047v58.35a12.801 12.801 0 0 1-3.791 9.047 12.866 12.866 0 0 1-9.092 3.722h-13.577v21.841l-20.367-21.841H40.015a12.859 12.859 0 0 1-9.091-3.722 12.81 12.81 0 0 1-3.792-9.047v-58.35a12.799 12.799 0 0 1 3.78-9.035A12.852 12.852 0 0 1 39.982 33Z'
|
||||
fill='#FFBC1F'
|
||||
/>
|
||||
<Path
|
||||
d='M98.04 116.888H40.016a12.857 12.857 0 0 1-9.091-3.722 12.81 12.81 0 0 1-3.792-9.047V68.695s4.052 32.757 4.78 35.64c.727 2.884 2.172 7.198 9.015 7.913 6.843.716 57.114 4.64 57.114 4.64Z'
|
||||
fill='#CC8F00'
|
||||
/>
|
||||
<Path
|
||||
d='M117.408 66.603a8.3 8.3 0 0 0-4.604 1.393 8.255 8.255 0 0 0-1.256 12.725 8.302 8.302 0 0 0 12.752-1.253 8.263 8.263 0 0 0 .768-7.762 8.255 8.255 0 0 0-4.486-4.477 8.295 8.295 0 0 0-3.174-.626ZM85.983 66.603a8.3 8.3 0 0 0-4.605 1.393 8.275 8.275 0 0 0-3.052 3.712 8.255 8.255 0 0 0 1.797 9.013 8.304 8.304 0 0 0 9.032 1.793 8.285 8.285 0 0 0 3.72-3.046 8.258 8.258 0 0 0 1.396-4.595 8.243 8.243 0 0 0-2.424-5.851 8.277 8.277 0 0 0-5.864-2.42ZM54.592 66.603a8.3 8.3 0 0 0-4.607 1.388 8.274 8.274 0 0 0-3.057 3.71 8.254 8.254 0 0 0 1.79 9.017 8.294 8.294 0 0 0 9.032 1.797 8.284 8.284 0 0 0 3.722-3.046 8.258 8.258 0 0 0 1.397-4.596 8.246 8.246 0 0 0-2.42-5.847 8.278 8.278 0 0 0-5.857-2.423Z'
|
||||
fill={theme.centerChannelBg}
|
||||
/>
|
||||
<Path
|
||||
d='M135.32 57.433a25.992 25.992 0 0 0-4.65-9.077 26.044 26.044 0 0 0-7.788-6.597.902.902 0 0 1-.474-.994.897.897 0 0 1 .844-.708c5.8-.347 17.51.889 13.838 17.289a.912.912 0 0 1-1.77.087Z'
|
||||
fill='#FFD470'
|
||||
/>
|
||||
<Ellipse
|
||||
cx={86}
|
||||
cy={148.925}
|
||||
rx={52.528}
|
||||
ry={4.075}
|
||||
fill='#000'
|
||||
fillOpacity={0.08}
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShareFeedbackIllustration;
|
||||
@@ -2,13 +2,13 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, Text, TextStyle} from 'react-native';
|
||||
import {Text, TextStyle} from 'react-native';
|
||||
|
||||
import {popToRoot, dismissAllModals} from '@screens/navigation';
|
||||
|
||||
type HashtagProps = {
|
||||
hashtag: string;
|
||||
linkStyle: StyleProp<TextStyle>;
|
||||
linkStyle: TextStyle;
|
||||
};
|
||||
|
||||
const Hashtag = ({hashtag, linkStyle}: HashtagProps) => {
|
||||
|
||||
@@ -4,16 +4,19 @@
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React from 'react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeConfig} from '@queries/servers/system';
|
||||
|
||||
import Markdown from './markdown';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const enableLatex = observeConfigBooleanValue(database, 'EnableLatex');
|
||||
const enableInlineLatex = observeConfigBooleanValue(database, 'EnableInlineLatex');
|
||||
const config = observeConfig(database);
|
||||
const enableLatex = config.pipe(switchMap((c) => of$(c?.EnableLatex === 'true')));
|
||||
const enableInlineLatex = config.pipe(switchMap((c) => of$(c?.EnableInlineLatex === 'true')));
|
||||
|
||||
return {
|
||||
enableLatex,
|
||||
|
||||
@@ -284,16 +284,10 @@ const Markdown = ({
|
||||
return renderText({context, literal: `#${hashtag}`});
|
||||
}
|
||||
|
||||
const linkStyle = [textStyles.link];
|
||||
const headingIndex = context.findIndex((c) => c.includes('heading'));
|
||||
if (headingIndex > -1) {
|
||||
linkStyle.push(textStyles[context[headingIndex]]);
|
||||
}
|
||||
|
||||
return (
|
||||
<Hashtag
|
||||
hashtag={hashtag}
|
||||
linkStyle={linkStyle}
|
||||
linkStyle={textStyles.link}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,16 +3,23 @@
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfigValue} from '@queries/servers/system';
|
||||
import {observeConfig} from '@queries/servers/system';
|
||||
|
||||
import MarkdownLink from './markdown_link';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const experimentalNormalizeMarkdownLinks = observeConfigValue(database, 'ExperimentalNormalizeMarkdownLinks');
|
||||
const siteURL = observeConfigValue(database, 'SiteURL');
|
||||
const config = observeConfig(database);
|
||||
const experimentalNormalizeMarkdownLinks = config.pipe(
|
||||
switchMap((cfg) => of$(cfg?.ExperimentalNormalizeMarkdownLinks)),
|
||||
);
|
||||
const siteURL = config.pipe(
|
||||
switchMap((cfg) => of$(cfg?.SiteURL)),
|
||||
);
|
||||
|
||||
return {
|
||||
experimentalNormalizeMarkdownLinks,
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
import React, {useMemo} from 'react';
|
||||
import {Platform, Text, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import ViewConstants from '@constants/view';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
@@ -26,18 +24,18 @@ type Props = {
|
||||
defaultHeight: number;
|
||||
hasSearch: boolean;
|
||||
isLargeTitle: boolean;
|
||||
heightOffset: number;
|
||||
largeHeight: number;
|
||||
leftComponent?: React.ReactElement;
|
||||
onBackPress?: () => void;
|
||||
onTitlePress?: () => void;
|
||||
rightButtons?: HeaderRightButton[];
|
||||
scrollValue?: Animated.SharedValue<number>;
|
||||
lockValue?: Animated.SharedValue<number | null>;
|
||||
showBackButton?: boolean;
|
||||
subtitle?: string;
|
||||
subtitleCompanion?: React.ReactElement;
|
||||
theme: Theme;
|
||||
title?: string;
|
||||
top: number;
|
||||
}
|
||||
|
||||
const hitSlop = {top: 20, bottom: 20, left: 20, right: 20};
|
||||
@@ -129,21 +127,20 @@ const Header = ({
|
||||
defaultHeight,
|
||||
hasSearch,
|
||||
isLargeTitle,
|
||||
heightOffset,
|
||||
largeHeight,
|
||||
leftComponent,
|
||||
onBackPress,
|
||||
onTitlePress,
|
||||
rightButtons,
|
||||
scrollValue,
|
||||
lockValue,
|
||||
showBackButton = true,
|
||||
subtitle,
|
||||
subtitleCompanion,
|
||||
theme,
|
||||
title,
|
||||
top,
|
||||
}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const opacity = useAnimatedStyle(() => {
|
||||
if (!isLargeTitle) {
|
||||
@@ -154,7 +151,8 @@ const Header = ({
|
||||
return {opacity: 0};
|
||||
}
|
||||
|
||||
const barHeight = heightOffset - ViewConstants.LARGE_HEADER_TITLE_HEIGHT;
|
||||
const largeTitleLabelHeight = 60;
|
||||
const barHeight = (largeHeight - defaultHeight) - largeTitleLabelHeight;
|
||||
const val = (scrollValue?.value ?? 0);
|
||||
const showDuration = 200;
|
||||
const hideDuration = 50;
|
||||
@@ -163,15 +161,11 @@ const Header = ({
|
||||
return {
|
||||
opacity: withTiming(opacityValue, {duration}),
|
||||
};
|
||||
}, [heightOffset, isLargeTitle, hasSearch]);
|
||||
}, [defaultHeight, largeHeight, isLargeTitle, hasSearch]);
|
||||
|
||||
const containerAnimatedStyle = useAnimatedStyle(() => ({
|
||||
height: defaultHeight,
|
||||
paddingTop: insets.top,
|
||||
}), [defaultHeight, lockValue]);
|
||||
|
||||
const containerStyle = useMemo(() => (
|
||||
[styles.container, containerAnimatedStyle]), [styles, containerAnimatedStyle]);
|
||||
const containerStyle = useMemo(() => {
|
||||
return [styles.container, {height: defaultHeight + top, paddingTop: top}];
|
||||
}, [defaultHeight, theme]);
|
||||
|
||||
const additionalTitleStyle = useMemo(() => ({
|
||||
marginLeft: Platform.select({android: showBackButton && !leftComponent ? 20 : 0}),
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {forwardRef} from 'react';
|
||||
import Animated, {useAnimatedStyle, useDerivedValue} from 'react-native-reanimated';
|
||||
import React from 'react';
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {SEARCH_INPUT_HEIGHT, SEARCH_INPUT_MARGIN} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useHeaderHeight, {MAX_OVERSCROLL} from '@hooks/header';
|
||||
import {clamp} from '@utils/gallery';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import Header, {HeaderRightButton} from './header';
|
||||
import NavigationHeaderLargeTitle from './large';
|
||||
import NavigationSearch from './search';
|
||||
|
||||
import type {SearchProps, SearchRef} from '@components/search';
|
||||
import type {SearchProps} from '@components/search';
|
||||
|
||||
type Props = SearchProps & {
|
||||
hasSearch?: boolean;
|
||||
@@ -24,7 +23,6 @@ type Props = SearchProps & {
|
||||
onTitlePress?: () => void;
|
||||
rightButtons?: HeaderRightButton[];
|
||||
scrollValue?: Animated.SharedValue<number>;
|
||||
lockValue?: Animated.SharedValue<number | null>;
|
||||
hideHeader?: () => void;
|
||||
showBackButton?: boolean;
|
||||
subtitle?: string;
|
||||
@@ -41,7 +39,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const NavigationHeader = forwardRef<SearchRef, Props>(({
|
||||
const NavigationHeader = ({
|
||||
hasSearch = false,
|
||||
isLargeTitle = false,
|
||||
leftComponent,
|
||||
@@ -49,46 +47,29 @@ const NavigationHeader = forwardRef<SearchRef, Props>(({
|
||||
onTitlePress,
|
||||
rightButtons,
|
||||
scrollValue,
|
||||
lockValue,
|
||||
showBackButton,
|
||||
subtitle,
|
||||
subtitleCompanion,
|
||||
title = '',
|
||||
hideHeader,
|
||||
...searchProps
|
||||
}: Props, ref) => {
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const {largeHeight, defaultHeight, headerOffset} = useHeaderHeight();
|
||||
const {largeHeight, defaultHeight} = useHeaderHeight();
|
||||
const containerHeight = useAnimatedStyle(() => {
|
||||
const minHeight = defaultHeight;
|
||||
const minHeight = defaultHeight + insets.top;
|
||||
const value = -(scrollValue?.value || 0);
|
||||
const calculatedHeight = (isLargeTitle ? largeHeight : defaultHeight) + value;
|
||||
const height = lockValue?.value ? lockValue.value : calculatedHeight;
|
||||
const height = ((isLargeTitle ? largeHeight : defaultHeight)) + value + insets.top;
|
||||
return {
|
||||
height: Math.max(height, minHeight),
|
||||
minHeight,
|
||||
maxHeight: largeHeight + MAX_OVERSCROLL,
|
||||
maxHeight: largeHeight + insets.top + MAX_OVERSCROLL,
|
||||
};
|
||||
});
|
||||
|
||||
const minScrollValue = useDerivedValue(() => scrollValue?.value || 0, [scrollValue]);
|
||||
|
||||
const translateY = useDerivedValue(() => (
|
||||
lockValue?.value ? -lockValue.value : Math.min(-minScrollValue.value, headerOffset)
|
||||
), [lockValue, minScrollValue, headerOffset]);
|
||||
|
||||
const searchTopStyle = useAnimatedStyle(() => {
|
||||
const margin = clamp(-minScrollValue.value, -headerOffset, headerOffset);
|
||||
const marginTop = (lockValue?.value ? -lockValue?.value : margin) - SEARCH_INPUT_HEIGHT - SEARCH_INPUT_MARGIN;
|
||||
return {marginTop};
|
||||
}, [lockValue, headerOffset, scrollValue]);
|
||||
|
||||
const heightOffset = useDerivedValue(() => (
|
||||
lockValue?.value ? lockValue.value : headerOffset
|
||||
), [lockValue, headerOffset]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.View style={[styles.container, containerHeight]}>
|
||||
@@ -96,43 +77,45 @@ const NavigationHeader = forwardRef<SearchRef, Props>(({
|
||||
defaultHeight={defaultHeight}
|
||||
hasSearch={hasSearch}
|
||||
isLargeTitle={isLargeTitle}
|
||||
heightOffset={heightOffset.value}
|
||||
largeHeight={largeHeight}
|
||||
leftComponent={leftComponent}
|
||||
onBackPress={onBackPress}
|
||||
onTitlePress={onTitlePress}
|
||||
rightButtons={rightButtons}
|
||||
lockValue={lockValue}
|
||||
scrollValue={scrollValue}
|
||||
showBackButton={showBackButton}
|
||||
subtitle={subtitle}
|
||||
subtitleCompanion={subtitleCompanion}
|
||||
theme={theme}
|
||||
title={title}
|
||||
top={insets.top}
|
||||
/>
|
||||
{isLargeTitle &&
|
||||
<NavigationHeaderLargeTitle
|
||||
heightOffset={heightOffset.value}
|
||||
defaultHeight={defaultHeight}
|
||||
hasSearch={hasSearch}
|
||||
largeHeight={largeHeight}
|
||||
scrollValue={scrollValue}
|
||||
subtitle={subtitle}
|
||||
theme={theme}
|
||||
title={title}
|
||||
translateY={translateY}
|
||||
/>
|
||||
}
|
||||
{hasSearch &&
|
||||
<NavigationSearch
|
||||
{...searchProps}
|
||||
hideHeader={hideHeader}
|
||||
theme={theme}
|
||||
topStyle={searchTopStyle}
|
||||
ref={ref}
|
||||
/>
|
||||
<NavigationSearch
|
||||
{...searchProps}
|
||||
defaultHeight={defaultHeight}
|
||||
largeHeight={largeHeight}
|
||||
scrollValue={scrollValue}
|
||||
hideHeader={hideHeader}
|
||||
theme={theme}
|
||||
top={0}
|
||||
/>
|
||||
}
|
||||
</Animated.View>
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
NavigationHeader.displayName = 'NavHeader';
|
||||
export default NavigationHeader;
|
||||
|
||||
|
||||
@@ -9,12 +9,13 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
heightOffset: number;
|
||||
defaultHeight: number;
|
||||
hasSearch: boolean;
|
||||
largeHeight: number;
|
||||
scrollValue?: Animated.SharedValue<number>;
|
||||
subtitle?: string;
|
||||
theme: Theme;
|
||||
title: string;
|
||||
translateY: Animated.DerivedValue<number>;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
@@ -33,29 +34,33 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
}));
|
||||
|
||||
const NavigationHeaderLargeTitle = ({
|
||||
heightOffset,
|
||||
defaultHeight,
|
||||
largeHeight,
|
||||
hasSearch,
|
||||
scrollValue,
|
||||
subtitle,
|
||||
theme,
|
||||
title,
|
||||
translateY,
|
||||
}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const transform = useAnimatedStyle(() => (
|
||||
{transform: [{translateY: translateY.value}]}
|
||||
), [translateY?.value]);
|
||||
const transform = useAnimatedStyle(() => {
|
||||
const value = scrollValue?.value || 0;
|
||||
return {
|
||||
transform: [{translateY: Math.min(-value, largeHeight - defaultHeight)}],
|
||||
};
|
||||
});
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
return [{height: heightOffset}, styles.container];
|
||||
}, [heightOffset, theme]);
|
||||
return [{height: largeHeight - defaultHeight}, styles.container];
|
||||
}, [defaultHeight, largeHeight, theme]);
|
||||
|
||||
return (
|
||||
<Animated.View style={[containerStyle, transform]}>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={[styles.heading]}
|
||||
style={styles.heading}
|
||||
testID='navigation.large_header.title'
|
||||
>
|
||||
{title}
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {forwardRef, useCallback, useEffect, useMemo} from 'react';
|
||||
import {DeviceEventEmitter, Keyboard, NativeSyntheticEvent, Platform, TextInputFocusEventData, ViewStyle} from 'react-native';
|
||||
import Animated, {AnimatedStyleProp} from 'react-native-reanimated';
|
||||
import React, {useCallback, useEffect, useMemo} from 'react';
|
||||
import {DeviceEventEmitter, Keyboard, NativeSyntheticEvent, Platform, TextInputFocusEventData} from 'react-native';
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
|
||||
|
||||
import Search, {SearchProps, SearchRef} from '@components/search';
|
||||
import Search, {SearchProps} from '@components/search';
|
||||
import {Events} from '@constants';
|
||||
import {HEADER_SEARCH_HEIGHT} from '@constants/view';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = SearchProps & {
|
||||
topStyle: AnimatedStyleProp<ViewStyle>;
|
||||
defaultHeight: number;
|
||||
largeHeight: number;
|
||||
scrollValue?: Animated.SharedValue<number>;
|
||||
hideHeader?: () => void;
|
||||
theme: Theme;
|
||||
top: number;
|
||||
}
|
||||
|
||||
const INITIAL_TOP = -45;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
height: HEADER_SEARCH_HEIGHT,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 20,
|
||||
width: '100%',
|
||||
zIndex: 10,
|
||||
top: INITIAL_TOP,
|
||||
},
|
||||
inputContainerStyle: {
|
||||
backgroundColor: changeOpacity(theme.sidebarText, 0.12),
|
||||
@@ -31,12 +40,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const NavigationSearch = forwardRef<SearchRef, Props>(({
|
||||
const NavigationSearch = ({
|
||||
defaultHeight,
|
||||
largeHeight,
|
||||
scrollValue,
|
||||
hideHeader,
|
||||
theme,
|
||||
topStyle,
|
||||
...searchProps
|
||||
}: Props, ref) => {
|
||||
}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const cancelButtonProps: SearchProps['cancelButtonProps'] = useMemo(() => ({
|
||||
@@ -47,35 +58,38 @@ const NavigationSearch = forwardRef<SearchRef, Props>(({
|
||||
color: theme.sidebarText,
|
||||
}), [theme]);
|
||||
|
||||
const searchTop = useAnimatedStyle(() => {
|
||||
const value = scrollValue?.value || 0;
|
||||
const min = (largeHeight - defaultHeight);
|
||||
return {marginTop: Math.min(-Math.min((value), min), min)};
|
||||
}, [largeHeight, defaultHeight]);
|
||||
|
||||
const onFocus = useCallback((e: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
||||
hideHeader?.();
|
||||
searchProps.onFocus?.(e);
|
||||
}, [hideHeader, searchProps.onFocus]);
|
||||
|
||||
const showEmitter = useCallback(() => {
|
||||
if (Platform.OS === 'android') {
|
||||
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const hideEmitter = useCallback(() => {
|
||||
if (Platform.OS === 'android') {
|
||||
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const show = Keyboard.addListener('keyboardDidShow', showEmitter);
|
||||
const hide = Keyboard.addListener('keyboardDidHide', hideEmitter);
|
||||
const show = Keyboard.addListener('keyboardDidShow', () => {
|
||||
if (Platform.OS === 'android') {
|
||||
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, false);
|
||||
}
|
||||
});
|
||||
|
||||
const hide = Keyboard.addListener('keyboardDidHide', () => {
|
||||
if (Platform.OS === 'android') {
|
||||
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
hide.remove();
|
||||
show.remove();
|
||||
};
|
||||
}, [hideEmitter, showEmitter]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, topStyle]}>
|
||||
<Animated.View style={[styles.container, searchTop]}>
|
||||
<Search
|
||||
{...searchProps}
|
||||
cancelButtonProps={cancelButtonProps}
|
||||
@@ -86,13 +100,10 @@ const NavigationSearch = forwardRef<SearchRef, Props>(({
|
||||
placeholderTextColor={changeOpacity(theme.sidebarText, Platform.select({android: 0.56, default: 0.72}))}
|
||||
searchIconColor={theme.sidebarText}
|
||||
selectionColor={theme.sidebarText}
|
||||
ref={ref}
|
||||
testID='navigation.header.search_bar'
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
NavigationSearch.displayName = 'NavSearch';
|
||||
export default NavigationSearch;
|
||||
|
||||
|
||||
@@ -23,13 +23,11 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 32,
|
||||
height: '100%',
|
||||
alignItems: 'center' as const,
|
||||
justifyContent: 'center' as const,
|
||||
},
|
||||
result: {
|
||||
textAlign: 'center',
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 400, 'SemiBold'),
|
||||
},
|
||||
|
||||
@@ -10,7 +10,6 @@ import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import OptionIcon from './option_icon';
|
||||
import RadioItem, {RadioItemProps} from './radio_item';
|
||||
|
||||
const TouchableOptionTypes = {
|
||||
@@ -104,7 +103,6 @@ export type OptionItemProps = {
|
||||
description?: string;
|
||||
destructive?: boolean;
|
||||
icon?: string;
|
||||
iconColor?: string;
|
||||
info?: string;
|
||||
inline?: boolean;
|
||||
label: string;
|
||||
@@ -126,7 +124,6 @@ const OptionItem = ({
|
||||
description,
|
||||
destructive,
|
||||
icon,
|
||||
iconColor,
|
||||
info,
|
||||
inline = false,
|
||||
label,
|
||||
@@ -214,7 +211,6 @@ const OptionItem = ({
|
||||
onPress={onRemove}
|
||||
style={[styles.iconContainer]}
|
||||
type='opacity'
|
||||
testID={`${testID}.remove.button`}
|
||||
>
|
||||
<CompassIcon
|
||||
name={'close'}
|
||||
@@ -239,10 +235,10 @@ const OptionItem = ({
|
||||
<View style={styles.labelContainer}>
|
||||
{Boolean(icon) && (
|
||||
<View style={styles.iconContainer}>
|
||||
<OptionIcon
|
||||
icon={icon!}
|
||||
iconColor={iconColor}
|
||||
destructive={destructive}
|
||||
<CompassIcon
|
||||
name={icon!}
|
||||
size={24}
|
||||
color={destructive ? theme.dndIndicator : changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {isValidUrl} from '@utils/url';
|
||||
|
||||
type OptionIconProps = {
|
||||
icon: string;
|
||||
iconColor?: string;
|
||||
destructive?: boolean;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme(() => {
|
||||
return {
|
||||
icon: {
|
||||
fontSize: 24,
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const OptionIcon = ({icon, iconColor, destructive}: OptionIconProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const [failedToLoadImage, setFailedToLoadImage] = useState(false);
|
||||
const onErrorLoadingIcon = useCallback(() => {
|
||||
setFailedToLoadImage(true);
|
||||
}, []);
|
||||
|
||||
const iconAsSource = useMemo(() => {
|
||||
return {uri: icon};
|
||||
}, [icon]);
|
||||
|
||||
if (isValidUrl(icon) && !failedToLoadImage) {
|
||||
return (
|
||||
<FastImage
|
||||
source={iconAsSource}
|
||||
style={styles.icon}
|
||||
onError={onErrorLoadingIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const iconName = failedToLoadImage ? 'power-plugin-outline' : icon;
|
||||
return (
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
size={24}
|
||||
color={iconColor || (destructive ? theme.dndIndicator : changeOpacity(theme.centerChannelColor, 0.64))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptionIcon;
|
||||
@@ -16,14 +16,13 @@ type Props = {
|
||||
channelId: string;
|
||||
cursorPosition: number;
|
||||
rootId?: string;
|
||||
canShowPostPriority?: boolean;
|
||||
files?: FileInfo[];
|
||||
maxFileCount: number;
|
||||
maxFileSize: number;
|
||||
maxFileCount: number;
|
||||
canUploadFiles: boolean;
|
||||
updateCursorPosition: React.Dispatch<React.SetStateAction<number>>;
|
||||
updateCursorPosition: (cursorPosition: number) => void;
|
||||
updatePostInputTop: (top: number) => void;
|
||||
updateValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
updateValue: (value: string) => void;
|
||||
value: string;
|
||||
setIsFocused: (isFocused: boolean) => void;
|
||||
}
|
||||
@@ -41,10 +40,9 @@ export default function DraftHandler(props: Props) {
|
||||
channelId,
|
||||
cursorPosition,
|
||||
rootId = '',
|
||||
canShowPostPriority,
|
||||
files,
|
||||
maxFileCount,
|
||||
maxFileSize,
|
||||
maxFileCount,
|
||||
canUploadFiles,
|
||||
updateCursorPosition,
|
||||
updatePostInputTop,
|
||||
@@ -108,7 +106,7 @@ export default function DraftHandler(props: Props) {
|
||||
}
|
||||
|
||||
newUploadError(null);
|
||||
}, [intl, newUploadError, maxFileSize, serverUrl, files?.length, channelId, rootId]);
|
||||
}, [intl, newUploadError, maxFileCount, maxFileSize, serverUrl, files?.length, channelId, rootId]);
|
||||
|
||||
// This effect mainly handles keeping clean the uploadErrorHandlers, and
|
||||
// reinstantiate them on component mount and file retry.
|
||||
@@ -137,7 +135,6 @@ export default function DraftHandler(props: Props) {
|
||||
testID={testID}
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
canShowPostPriority={canShowPostPriority}
|
||||
|
||||
// From draft handler
|
||||
cursorPosition={cursorPosition}
|
||||
|
||||
@@ -4,23 +4,42 @@
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React from 'react';
|
||||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {DEFAULT_SERVER_MAX_FILE_SIZE} from '@constants/post_draft';
|
||||
import {observeCanUploadFiles, observeConfigIntValue, observeMaxFileCount} from '@queries/servers/system';
|
||||
import {observeConfig, observeLicense} from '@queries/servers/system';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
|
||||
import DraftHandler from './draft_handler';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const canUploadFiles = observeCanUploadFiles(database);
|
||||
const maxFileSize = observeConfigIntValue(database, 'MaxFileSize', DEFAULT_SERVER_MAX_FILE_SIZE);
|
||||
const maxFileCount = observeMaxFileCount(database);
|
||||
const config = observeConfig(database);
|
||||
|
||||
const license = observeLicense(database);
|
||||
|
||||
const canUploadFiles = combineLatest([config, license]).pipe(
|
||||
switchMap(([c, l]) => of$(
|
||||
c?.EnableFileAttachments === 'true' ||
|
||||
(l?.IsLicensed !== 'true' && l?.Compliance !== 'true' && c?.EnableMobileFileUpload === 'true'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const maxFileSize = config.pipe(
|
||||
switchMap((cfg) => of$(parseInt(cfg?.MaxFileSize || '0', 10) || DEFAULT_SERVER_MAX_FILE_SIZE)),
|
||||
);
|
||||
|
||||
const maxFileCount = config.pipe(
|
||||
switchMap((cfg) => of$(isMinimumServerVersion(cfg?.Version || '', 6, 0) ? 10 : 5)),
|
||||
);
|
||||
|
||||
return {
|
||||
maxFileSize,
|
||||
canUploadFiles,
|
||||
maxFileCount,
|
||||
canUploadFiles,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {PasteInputRef} from '@mattermost/react-native-paste-input';
|
||||
import React, {useCallback, useRef} from 'react';
|
||||
import React, {useCallback} from 'react';
|
||||
import {LayoutChangeEvent, Platform, ScrollView, View} from 'react-native';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import PostPriorityLabel from '@components/post_priority/post_priority_label';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
@@ -21,14 +19,9 @@ type Props = {
|
||||
channelId: string;
|
||||
rootId?: string;
|
||||
currentUserId: string;
|
||||
canShowPostPriority?: boolean;
|
||||
|
||||
// Post Props
|
||||
postProps: Post['props'];
|
||||
updatePostProps: (postProps: Post['props']) => void;
|
||||
|
||||
// Cursor Position Handler
|
||||
updateCursorPosition: React.Dispatch<React.SetStateAction<number>>;
|
||||
updateCursorPosition: (pos: number) => void;
|
||||
cursorPosition: number;
|
||||
|
||||
// Send Handler
|
||||
@@ -40,7 +33,7 @@ type Props = {
|
||||
files: FileInfo[];
|
||||
value: string;
|
||||
uploadFileError: React.ReactNode;
|
||||
updateValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
updateValue: (value: string) => void;
|
||||
addFiles: (files: FileInfo[]) => void;
|
||||
updatePostInputTop: (top: number) => void;
|
||||
setIsFocused: (isFocused: boolean) => void;
|
||||
@@ -83,13 +76,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
},
|
||||
postPriorityLabel: {
|
||||
marginLeft: 12,
|
||||
marginTop: Platform.select({
|
||||
ios: 3,
|
||||
android: 10,
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -97,7 +83,6 @@ export default function DraftInput({
|
||||
testID,
|
||||
channelId,
|
||||
currentUserId,
|
||||
canShowPostPriority,
|
||||
files,
|
||||
maxMessageLength,
|
||||
rootId = '',
|
||||
@@ -110,8 +95,6 @@ export default function DraftInput({
|
||||
updateCursorPosition,
|
||||
cursorPosition,
|
||||
updatePostInputTop,
|
||||
postProps,
|
||||
updatePostProps,
|
||||
setIsFocused,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
@@ -120,11 +103,6 @@ export default function DraftInput({
|
||||
updatePostInputTop(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const inputRef = useRef<PasteInputRef>();
|
||||
const focus = useCallback(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Render
|
||||
const postInputTestID = `${testID}.post.input`;
|
||||
const quickActionsTestID = `${testID}.quick_actions`;
|
||||
@@ -155,11 +133,6 @@ export default function DraftInput({
|
||||
overScrollMode={'never'}
|
||||
disableScrollViewPanResponder={true}
|
||||
>
|
||||
{Boolean(postProps.priority) && (
|
||||
<View style={style.postPriorityLabel}>
|
||||
<PostPriorityLabel label={postProps.priority}/>
|
||||
</View>
|
||||
)}
|
||||
<PostInput
|
||||
testID={postInputTestID}
|
||||
channelId={channelId}
|
||||
@@ -171,7 +144,6 @@ export default function DraftInput({
|
||||
value={value}
|
||||
addFiles={addFiles}
|
||||
sendMessage={sendMessage}
|
||||
inputRef={inputRef}
|
||||
setIsFocused={setIsFocused}
|
||||
/>
|
||||
<Uploads
|
||||
@@ -188,10 +160,6 @@ export default function DraftInput({
|
||||
addFiles={addFiles}
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
postProps={postProps}
|
||||
updatePostProps={updatePostProps}
|
||||
canShowPostPriority={canShowPostPriority}
|
||||
focus={focus}
|
||||
/>
|
||||
<SendAction
|
||||
testID={sendActionTestID}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import {View as ViewConstants} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete';
|
||||
import {useIsTablet, useKeyboardHeight} from '@hooks/device';
|
||||
import {useDefaultHeaderHeight} from '@hooks/header';
|
||||
@@ -34,7 +33,6 @@ type Props = {
|
||||
keyboardTracker: RefObject<KeyboardTrackingViewRef>;
|
||||
containerHeight: number;
|
||||
isChannelScreen: boolean;
|
||||
canShowPostPriority?: boolean;
|
||||
}
|
||||
|
||||
const {KEYBOARD_TRACKING_OFFSET} = ViewConstants;
|
||||
@@ -55,7 +53,6 @@ function PostDraft({
|
||||
keyboardTracker,
|
||||
containerHeight,
|
||||
isChannelScreen,
|
||||
canShowPostPriority,
|
||||
}: Props) {
|
||||
const [value, setValue] = useState(message);
|
||||
const [cursorPosition, setCursorPosition] = useState(message.length);
|
||||
@@ -65,7 +62,6 @@ function PostDraft({
|
||||
const keyboardHeight = useKeyboardHeight(keyboardTracker);
|
||||
const insets = useSafeAreaInsets();
|
||||
const headerHeight = useDefaultHeaderHeight();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
// Update draft in case we switch channels or threads
|
||||
useEffect(() => {
|
||||
@@ -79,7 +75,7 @@ function PostDraft({
|
||||
ios: (keyboardHeight ? keyboardHeight - keyboardAdjustment : (postInputTop + insetsAdjustment)),
|
||||
default: postInputTop + insetsAdjustment,
|
||||
});
|
||||
const autocompleteAvailableSpace = containerHeight - autocompletePosition - (isChannelScreen ? headerHeight : 0);
|
||||
const autocompleteAvailableSpace = containerHeight - autocompletePosition - (isChannelScreen ? headerHeight + insets.top : 0);
|
||||
|
||||
const [animatedAutocompletePosition, animatedAutocompleteAvailableSpace] = useAutocompleteDefaultAnimatedValues(autocompletePosition, autocompleteAvailableSpace);
|
||||
|
||||
@@ -111,7 +107,6 @@ function PostDraft({
|
||||
cursorPosition={cursorPosition}
|
||||
files={files}
|
||||
rootId={rootId}
|
||||
canShowPostPriority={canShowPostPriority}
|
||||
updateCursorPosition={setCursorPosition}
|
||||
updatePostInputTop={setPostInputTop}
|
||||
updateValue={setValue}
|
||||
@@ -132,7 +127,6 @@ function PostDraft({
|
||||
hasFilesAttached={Boolean(files?.length)}
|
||||
inPost={true}
|
||||
availableSpace={animatedAutocompleteAvailableSpace}
|
||||
serverUrl={serverUrl}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {of as of$} from 'rxjs';
|
||||
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observeConfigBooleanValue, observeConfigIntValue} from '@queries/servers/system';
|
||||
import {observeConfig} from '@queries/servers/system';
|
||||
|
||||
import PostInput from './post_input';
|
||||
|
||||
@@ -20,9 +20,18 @@ type OwnProps = {
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({database, channelId}: WithDatabaseArgs & OwnProps) => {
|
||||
const timeBetweenUserTypingUpdatesMilliseconds = observeConfigIntValue(database, 'TimeBetweenUserTypingUpdatesMilliseconds');
|
||||
const enableUserTypingMessage = observeConfigBooleanValue(database, 'EnableUserTypingMessages');
|
||||
const maxNotificationsPerChannel = observeConfigIntValue(database, 'MaxNotificationsPerChannel');
|
||||
const config = observeConfig(database);
|
||||
const timeBetweenUserTypingUpdatesMilliseconds = config.pipe(
|
||||
switchMap((cfg) => of$(parseInt(cfg?.TimeBetweenUserTypingUpdatesMilliseconds || '0', 10))),
|
||||
);
|
||||
|
||||
const enableUserTypingMessage = config.pipe(
|
||||
switchMap((cfg) => of$(cfg?.EnableUserTypingMessages === 'true')),
|
||||
);
|
||||
|
||||
const maxNotificationsPerChannel = config.pipe(
|
||||
switchMap((cfg) => of$(parseInt(cfg?.MaxNotificationsPerChannel || '0', 10))),
|
||||
);
|
||||
|
||||
const channel = observeChannel(database, channelId);
|
||||
|
||||
|
||||
@@ -34,12 +34,11 @@ type Props = {
|
||||
enableUserTypingMessage: boolean;
|
||||
membersInChannel: number;
|
||||
value: string;
|
||||
updateValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
updateValue: (value: string) => void;
|
||||
addFiles: (files: ExtractedFileInfo[]) => void;
|
||||
cursorPosition: number;
|
||||
updateCursorPosition: React.Dispatch<React.SetStateAction<number>>;
|
||||
updateCursorPosition: (pos: number) => void;
|
||||
sendMessage: () => void;
|
||||
inputRef: React.MutableRefObject<PasteInputRef | undefined>;
|
||||
setIsFocused: (isFocused: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -110,7 +109,6 @@ export default function PostInput({
|
||||
cursorPosition,
|
||||
updateCursorPosition,
|
||||
sendMessage,
|
||||
inputRef,
|
||||
setIsFocused,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
@@ -121,7 +119,7 @@ export default function PostInput({
|
||||
const managedConfig = useManagedConfig<ManagedConfig>();
|
||||
|
||||
const lastTypingEventSent = useRef(0);
|
||||
|
||||
const input = useRef<PasteInputRef>();
|
||||
const lastNativeValue = useRef('');
|
||||
const previousAppState = useRef(AppState.currentState);
|
||||
|
||||
@@ -135,7 +133,7 @@ export default function PostInput({
|
||||
}, [maxHeight, style.input]);
|
||||
|
||||
const blur = () => {
|
||||
inputRef.current?.blur();
|
||||
input.current?.blur();
|
||||
};
|
||||
|
||||
const handleAndroidKeyboard = () => {
|
||||
@@ -240,12 +238,12 @@ export default function PostInput({
|
||||
sendMessage();
|
||||
break;
|
||||
case 'shift-enter':
|
||||
updateValue((v) => v.substring(0, cursorPosition) + '\n' + v.substring(cursorPosition));
|
||||
updateCursorPosition((pos) => pos + 1);
|
||||
updateValue(value.substring(0, cursorPosition) + '\n' + value.substring(cursorPosition));
|
||||
updateCursorPosition(cursorPosition + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [sendMessage, updateValue, cursorPosition, isTablet]);
|
||||
}, [sendMessage, updateValue, value, cursorPosition, isTablet]);
|
||||
|
||||
const onAppStateChange = useCallback((appState: AppStateStatus) => {
|
||||
if (appState !== 'active' && previousAppState.current === 'active') {
|
||||
@@ -281,7 +279,7 @@ export default function PostInput({
|
||||
const draft = value ? `${value} ${text} ` : `${text} `;
|
||||
updateValue(draft);
|
||||
updateCursorPosition(draft.length);
|
||||
inputRef.current?.focus();
|
||||
input.current?.focus();
|
||||
}
|
||||
});
|
||||
return () => listener.remove();
|
||||
@@ -290,7 +288,7 @@ export default function PostInput({
|
||||
useEffect(() => {
|
||||
if (value !== lastNativeValue.current) {
|
||||
// May change when we implement Fabric
|
||||
inputRef.current?.setNativeProps({
|
||||
input.current?.setNativeProps({
|
||||
text: value,
|
||||
});
|
||||
lastNativeValue.current = value;
|
||||
@@ -308,7 +306,7 @@ export default function PostInput({
|
||||
<PasteableTextInput
|
||||
allowFontScaling={true}
|
||||
testID={testID}
|
||||
ref={inputRef}
|
||||
ref={input}
|
||||
disableCopyPaste={disableCopyAndPaste}
|
||||
style={pasteInputStyle}
|
||||
onChangeText={handleTextChange}
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function FileQuickAction({
|
||||
>
|
||||
<CompassIcon
|
||||
color={color}
|
||||
name='paperclip'
|
||||
name='file-generic-outline'
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
|
||||
@@ -4,20 +4,34 @@
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React from 'react';
|
||||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeCanUploadFiles, observeIsPostPriorityEnabled, observeMaxFileCount} from '@queries/servers/system';
|
||||
import {observeConfig, observeLicense} from '@queries/servers/system';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
|
||||
import QuickActions from './quick_actions';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const canUploadFiles = observeCanUploadFiles(database);
|
||||
const maxFileCount = observeMaxFileCount(database);
|
||||
const config = observeConfig(database);
|
||||
const license = observeLicense(database);
|
||||
|
||||
const canUploadFiles = combineLatest([config, license]).pipe(
|
||||
switchMap(([c, l]) => of$(
|
||||
c?.EnableFileAttachments === 'true' ||
|
||||
(l?.IsLicensed !== 'true' && l?.Compliance !== 'true' && c?.EnableMobileFileUpload === 'true'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const maxFileCount = config.pipe(
|
||||
switchMap((cfg) => of$(isMinimumServerVersion(cfg?.Version || '', 6, 0) ? 10 : 5)),
|
||||
);
|
||||
|
||||
return {
|
||||
canUploadFiles,
|
||||
isPostPriorityEnabled: observeIsPostPriorityEnabled(database),
|
||||
maxFileCount,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -13,8 +13,8 @@ type Props = {
|
||||
testID?: string;
|
||||
disabled?: boolean;
|
||||
inputType: 'at' | 'slash';
|
||||
updateValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
focus: () => void;
|
||||
onTextChange: (value: string) => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
@@ -34,19 +34,18 @@ export default function InputQuickAction({
|
||||
testID,
|
||||
disabled,
|
||||
inputType,
|
||||
updateValue,
|
||||
focus,
|
||||
onTextChange,
|
||||
value,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const onPress = useCallback(() => {
|
||||
updateValue((v) => {
|
||||
if (inputType === 'at') {
|
||||
return `${v}@`;
|
||||
}
|
||||
return '/';
|
||||
});
|
||||
focus();
|
||||
}, [inputType]);
|
||||
let newValue = '/';
|
||||
if (inputType === 'at') {
|
||||
newValue = `${value}@`;
|
||||
}
|
||||
|
||||
onTextChange(newValue);
|
||||
}, [value, inputType]);
|
||||
|
||||
const actionTestID = disabled ?
|
||||
`${testID}.disabled` :
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import PostPriorityPicker, {PostPriorityData} from '@components/post_priority/post_priority_picker';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {ICON_SIZE} from '@constants/post_draft';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {bottomSheet, dismissBottomSheet} from '@screens/navigation';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
testID?: string;
|
||||
postProps: Post['props'];
|
||||
updatePostProps: (postProps: Post['props']) => void;
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
icon: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default function PostPriorityAction({
|
||||
testID,
|
||||
postProps,
|
||||
updatePostProps,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
|
||||
const handlePostPriorityPicker = useCallback((postPriorityData: PostPriorityData) => {
|
||||
updatePostProps((oldPostProps: Post['props']) => ({
|
||||
...oldPostProps,
|
||||
...postPriorityData,
|
||||
}));
|
||||
dismissBottomSheet();
|
||||
}, [updatePostProps]);
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
return (
|
||||
<PostPriorityPicker
|
||||
data={{
|
||||
priority: postProps?.priority || '',
|
||||
}}
|
||||
onSubmit={handlePostPriorityPicker}
|
||||
/>
|
||||
);
|
||||
}, [handlePostPriorityPicker, postProps]);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
bottomSheet({
|
||||
title: intl.formatMessage({id: 'post_priority.picker.title', defaultMessage: 'Message priority'}),
|
||||
renderContent,
|
||||
snapPoints: [275, 10],
|
||||
theme,
|
||||
closeButtonId: 'post-priority-close-id',
|
||||
});
|
||||
}, [intl, renderContent, theme]);
|
||||
|
||||
const iconName = 'alert-circle-outline';
|
||||
const iconColor = changeOpacity(theme.centerChannelColor, 0.64);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
testID={testID}
|
||||
onPress={onPress}
|
||||
style={style.icon}
|
||||
type={'opacity'}
|
||||
>
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
color={iconColor}
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
@@ -8,23 +8,17 @@ import CameraAction from './camera_quick_action';
|
||||
import FileAction from './file_quick_action';
|
||||
import ImageAction from './image_quick_action';
|
||||
import InputAction from './input_quick_action';
|
||||
import PostPriorityAction from './post_priority_action';
|
||||
|
||||
type Props = {
|
||||
testID?: string;
|
||||
canUploadFiles: boolean;
|
||||
fileCount: number;
|
||||
isPostPriorityEnabled: boolean;
|
||||
canShowPostPriority?: boolean;
|
||||
maxFileCount: number;
|
||||
|
||||
// Draft Handler
|
||||
value: string;
|
||||
updateValue: (value: string) => void;
|
||||
addFiles: (file: FileInfo[]) => void;
|
||||
postProps: Post['props'];
|
||||
updatePostProps: (postProps: Post['props']) => void;
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
@@ -50,14 +44,9 @@ export default function QuickActions({
|
||||
canUploadFiles,
|
||||
value,
|
||||
fileCount,
|
||||
isPostPriorityEnabled,
|
||||
canShowPostPriority,
|
||||
maxFileCount,
|
||||
updateValue,
|
||||
addFiles,
|
||||
postProps,
|
||||
updatePostProps,
|
||||
focus,
|
||||
}: Props) {
|
||||
const atDisabled = value[value.length - 1] === '@';
|
||||
const slashDisabled = value.length > 0;
|
||||
@@ -67,7 +56,6 @@ export default function QuickActions({
|
||||
const fileActionTestID = `${testID}.file_action`;
|
||||
const imageActionTestID = `${testID}.image_action`;
|
||||
const cameraActionTestID = `${testID}.camera_action`;
|
||||
const postPriorityActionTestID = `${testID}.post_priority_action`;
|
||||
|
||||
const uploadProps = {
|
||||
disabled: !canUploadFiles,
|
||||
@@ -86,15 +74,15 @@ export default function QuickActions({
|
||||
testID={atInputActionTestID}
|
||||
disabled={atDisabled}
|
||||
inputType='at'
|
||||
updateValue={updateValue}
|
||||
focus={focus}
|
||||
onTextChange={updateValue}
|
||||
value={value}
|
||||
/>
|
||||
<InputAction
|
||||
testID={slashInputActionTestID}
|
||||
disabled={slashDisabled}
|
||||
inputType='slash'
|
||||
updateValue={updateValue}
|
||||
focus={focus}
|
||||
onTextChange={updateValue}
|
||||
value={''} // Only enabled when value == ''
|
||||
/>
|
||||
<FileAction
|
||||
testID={fileActionTestID}
|
||||
@@ -108,13 +96,6 @@ export default function QuickActions({
|
||||
testID={cameraActionTestID}
|
||||
{...uploadProps}
|
||||
/>
|
||||
{isPostPriorityEnabled && canShowPostPriority && (
|
||||
<PostPriorityAction
|
||||
testID={postPriorityActionTestID}
|
||||
postProps={postProps}
|
||||
updatePostProps={updatePostProps}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
||||
import {observeChannel, observeCurrentChannel} from '@queries/servers/channel';
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {observePermissionForChannel} from '@queries/servers/role';
|
||||
import {observeConfigBooleanValue, observeConfigIntValue, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeConfig, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
|
||||
import SendHandler from './send_handler';
|
||||
@@ -42,9 +42,16 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
|
||||
switchMap((u) => of$(u?.status === General.OUT_OF_OFFICE)),
|
||||
);
|
||||
|
||||
const enableConfirmNotificationsToChannel = observeConfigBooleanValue(database, 'EnableConfirmNotificationsToChannel');
|
||||
const isTimezoneEnabled = observeConfigBooleanValue(database, 'ExperimentalTimezone');
|
||||
const maxMessageLength = observeConfigIntValue(database, 'MaxPostSize', MAX_MESSAGE_LENGTH_FALLBACK);
|
||||
const config = observeConfig(database);
|
||||
const enableConfirmNotificationsToChannel = config.pipe(
|
||||
switchMap((cfg) => of$(Boolean(cfg?.EnableConfirmNotificationsToChannel === 'true'))),
|
||||
);
|
||||
const isTimezoneEnabled = config.pipe(
|
||||
switchMap((cfg) => of$(Boolean(cfg?.ExperimentalTimezone === 'true'))),
|
||||
);
|
||||
const maxMessageLength = config.pipe(
|
||||
switchMap((cfg) => of$(parseInt(cfg?.MaxPostSize || '0', 10) || MAX_MESSAGE_LENGTH_FALLBACK)),
|
||||
);
|
||||
|
||||
const useChannelMentions = combineLatest([channel, currentUser]).pipe(
|
||||
switchMap(([c, u]) => {
|
||||
|
||||
@@ -29,7 +29,6 @@ type Props = {
|
||||
testID?: string;
|
||||
channelId: string;
|
||||
rootId: string;
|
||||
canShowPostPriority?: boolean;
|
||||
setIsFocused: (isFocused: boolean) => void;
|
||||
|
||||
// From database
|
||||
@@ -47,8 +46,8 @@ type Props = {
|
||||
value: string;
|
||||
files: FileInfo[];
|
||||
clearDraft: () => void;
|
||||
updateValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
updateCursorPosition: React.Dispatch<React.SetStateAction<number>>;
|
||||
updateValue: (message: string) => void;
|
||||
updateCursorPosition: (cursorPosition: number) => void;
|
||||
updatePostInputTop: (top: number) => void;
|
||||
addFiles: (file: FileInfo[]) => void;
|
||||
uploadFileError: React.ReactNode;
|
||||
@@ -65,7 +64,6 @@ export default function SendHandler({
|
||||
membersCount = 0,
|
||||
cursorPosition,
|
||||
rootId,
|
||||
canShowPostPriority,
|
||||
useChannelMentions,
|
||||
userIsOutOfOffice,
|
||||
customEmojis,
|
||||
@@ -84,8 +82,6 @@ export default function SendHandler({
|
||||
const [channelTimezoneCount, setChannelTimezoneCount] = useState(0);
|
||||
const [sendingMessage, setSendingMessage] = useState(false);
|
||||
|
||||
const [postProps, setPostProps] = useState<Post['props']>({});
|
||||
|
||||
const canSend = useCallback(() => {
|
||||
if (sendingMessage) {
|
||||
return false;
|
||||
@@ -118,19 +114,14 @@ export default function SendHandler({
|
||||
channel_id: channelId,
|
||||
root_id: rootId,
|
||||
message: value,
|
||||
} as Post;
|
||||
|
||||
if (Object.keys(postProps).length) {
|
||||
post.props = postProps;
|
||||
}
|
||||
};
|
||||
|
||||
createPost(serverUrl, post, postFiles);
|
||||
|
||||
clearDraft();
|
||||
setSendingMessage(false);
|
||||
setPostProps({});
|
||||
DeviceEventEmitter.emit(Events.POST_LIST_SCROLL_TO_BOTTOM, rootId ? Screens.THREAD : Screens.CHANNEL);
|
||||
}, [files, currentUserId, channelId, rootId, value, clearDraft, postProps]);
|
||||
}, [files, currentUserId, channelId, rootId, value, clearDraft]);
|
||||
|
||||
const showSendToAllOrChannelOrHereAlert = useCallback((calculatedMembersCount: number, atHere: boolean) => {
|
||||
const notifyAllMessage = DraftUtils.buildChannelWideMentionMessage(intl, calculatedMembersCount, Boolean(isTimezoneEnabled), channelTimezoneCount, atHere);
|
||||
@@ -217,6 +208,11 @@ export default function SendHandler({
|
||||
|
||||
clearDraft();
|
||||
|
||||
// TODO Apps related https://mattermost.atlassian.net/browse/MM-41233
|
||||
// if (data?.form) {
|
||||
// showAppForm(data.form, data.call, theme);
|
||||
// }
|
||||
|
||||
if (data?.goto_location && !value.startsWith('/leave')) {
|
||||
handleGotoLocation(serverUrl, intl, data.goto_location);
|
||||
}
|
||||
@@ -285,7 +281,6 @@ export default function SendHandler({
|
||||
channelId={channelId}
|
||||
currentUserId={currentUserId}
|
||||
rootId={rootId}
|
||||
canShowPostPriority={canShowPostPriority}
|
||||
cursorPosition={cursorPosition}
|
||||
updateCursorPosition={updateCursorPosition}
|
||||
value={value}
|
||||
@@ -297,8 +292,6 @@ export default function SendHandler({
|
||||
canSend={canSend()}
|
||||
maxMessageLength={maxMessageLength}
|
||||
updatePostInputTop={updatePostInputTop}
|
||||
postProps={postProps}
|
||||
updatePostProps={setPostProps}
|
||||
setIsFocused={setIsFocused}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {ActivityIndicator, DeviceEventEmitter, View, ViewToken} from 'react-native';
|
||||
import {ActivityIndicator, DeviceEventEmitter, Platform, View, ViewToken} from 'react-native';
|
||||
import Animated, {interpolate, useAnimatedStyle, useSharedValue, withSpring} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {resetMessageCount} from '@actions/local/channel';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -37,7 +36,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const HIDDEN_TOP = -60;
|
||||
const SHOWN_TOP = 5;
|
||||
const SHOWN_TOP = Platform.select({ios: 50, default: 5});
|
||||
const MIN_INPUT = 0;
|
||||
const MAX_INPUT = 1;
|
||||
|
||||
@@ -113,7 +112,6 @@ const MoreMessages = ({
|
||||
}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
const insets = useSafeAreaInsets();
|
||||
const pressed = useRef(false);
|
||||
const resetting = useRef(false);
|
||||
const initialScroll = useRef(false);
|
||||
@@ -122,11 +120,9 @@ const MoreMessages = ({
|
||||
const underlayColor = useMemo(() => `hsl(${hexToHue(theme.buttonBg)}, 50%, 38%)`, [theme]);
|
||||
const top = useSharedValue(0);
|
||||
const adjustedShownTop = SHOWN_TOP + (currentCallBarVisible ? CURRENT_CALL_BAR_HEIGHT : 0) + (joinCallBannerVisible ? JOIN_CALL_BAR_HEIGHT : 0);
|
||||
const adjustTop = isTablet || (isCRTEnabled && rootId);
|
||||
const shownTop = adjustTop ? SHOWN_TOP : adjustedShownTop;
|
||||
const shownTop = isTablet || (isCRTEnabled && rootId) ? 5 : adjustedShownTop;
|
||||
const BARS_FACTOR = Math.abs((1) / (HIDDEN_TOP - SHOWN_TOP));
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{
|
||||
translateY: withSpring(interpolate(
|
||||
@@ -140,13 +136,13 @@ const MoreMessages = ({
|
||||
[
|
||||
HIDDEN_TOP,
|
||||
HIDDEN_TOP,
|
||||
shownTop + (adjustTop ? 0 : insets.top),
|
||||
shownTop + (adjustTop ? 0 : insets.top),
|
||||
shownTop,
|
||||
shownTop,
|
||||
],
|
||||
Animated.Extrapolate.CLAMP,
|
||||
), {damping: 15}),
|
||||
}],
|
||||
}), [shownTop, insets.top, adjustTop]);
|
||||
}), [isTablet, shownTop]);
|
||||
|
||||
// Due to the implementation differences "unreadCount" gets updated for a channel on reset but not for a thread.
|
||||
// So we maintain a localUnreadCount to hide the indicator when the count is reset.
|
||||
|
||||
@@ -20,7 +20,7 @@ import type PostModel from '@typings/database/models/servers/post';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type AvatarProps = {
|
||||
author?: UserModel;
|
||||
author: UserModel;
|
||||
enablePostIconOverride?: boolean;
|
||||
isAutoReponse: boolean;
|
||||
location: string;
|
||||
@@ -91,9 +91,6 @@ const Avatar = ({author, enablePostIconOverride, isAutoReponse, location, post}:
|
||||
}
|
||||
|
||||
const openUserProfile = preventDoubleTap(() => {
|
||||
if (!author) {
|
||||
return;
|
||||
}
|
||||
const screen = Screens.USER_PROFILE;
|
||||
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
||||
const closeButtonId = 'close-user-profile';
|
||||
@@ -115,8 +112,8 @@ const Avatar = ({author, enablePostIconOverride, isAutoReponse, location, post}:
|
||||
author={author}
|
||||
size={ViewConstant.PROFILE_PICTURE_SIZE}
|
||||
iconSize={24}
|
||||
showStatus={!isAutoReponse || author?.isBot}
|
||||
testID={`post_avatar.${author?.id}.profile_picture`}
|
||||
showStatus={!isAutoReponse || author.isBot}
|
||||
testID={`post_avatar.${author.id}.profile_picture`}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import enhance from '@nozbe/with-observables';
|
||||
|
||||
import {observePostAuthor} from '@queries/servers/post';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
|
||||
import Avatar from './avatar';
|
||||
@@ -16,7 +15,7 @@ const withPost = enhance(['post'], ({database, post}: {post: PostModel} & WithDa
|
||||
const enablePostIconOverride = observeConfigBooleanValue(database, 'EnablePostIconOverride');
|
||||
|
||||
return {
|
||||
author: observePostAuthor(database, post),
|
||||
author: post.author.observe(),
|
||||
enablePostIconOverride,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,11 +9,9 @@ import Button from 'react-native-button';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
import {handleBindingClick, postEphemeralCallResponseForPost} from '@actions/remote/apps';
|
||||
import {handleGotoLocation} from '@actions/remote/command';
|
||||
import {AppBindingLocations, AppCallResponseTypes} from '@constants/apps';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||
import {showAppForm} from '@screens/navigation';
|
||||
import {createCallContext} from '@utils/apps';
|
||||
import {getStatusColors} from '@utils/message_attachment_colors';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
@@ -99,14 +97,7 @@ const ButtonBinding = ({currentTeamId, binding, post, teamID, theme}: Props) =>
|
||||
}
|
||||
return;
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
if (callResp.navigate_to_url) {
|
||||
handleGotoLocation(serverUrl, intl, callResp.navigate_to_url);
|
||||
}
|
||||
return;
|
||||
case AppCallResponseTypes.FORM:
|
||||
if (callResp.form) {
|
||||
showAppForm(callResp.form, context);
|
||||
}
|
||||
return;
|
||||
default: {
|
||||
const errorMessage = intl.formatMessage({
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
import {postEphemeralCallResponseForPost} from '@actions/remote/apps';
|
||||
import {handleBindingClick, postEphemeralCallResponseForPost} from '@actions/remote/apps';
|
||||
import AutocompleteSelector from '@components/autocomplete_selector';
|
||||
import {AppBindingLocations, AppCallResponseTypes} from '@constants/apps';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useAppBinding} from '@hooks/apps';
|
||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||
import {createCallContext} from '@utils/apps';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
@@ -26,46 +28,63 @@ type Props = {
|
||||
|
||||
const MenuBinding = ({binding, currentTeamId, post, teamID}: Props) => {
|
||||
const [selected, setSelected] = useState<string>();
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const onCallResponse = useCallback((callResp: AppCallResponse, message: string) => {
|
||||
postEphemeralCallResponseForPost(serverUrl, callResp, message, post);
|
||||
}, [serverUrl, post]);
|
||||
|
||||
const context = useMemo(() => ({
|
||||
channel_id: post.channelId,
|
||||
team_id: teamID || currentTeamId,
|
||||
post_id: post.id,
|
||||
root_id: post.rootId || post.id,
|
||||
}), [post, teamID, currentTeamId]);
|
||||
|
||||
const config = useMemo(() => ({
|
||||
onSuccess: onCallResponse,
|
||||
onError: onCallResponse,
|
||||
}), [onCallResponse]);
|
||||
|
||||
const handleBindingSubmit = useAppBinding(context, config);
|
||||
|
||||
const onSelect = useCallback(async (picked: SelectedDialogOption) => {
|
||||
if (!picked || Array.isArray(picked)) {
|
||||
const onSelect = useCallback(async (picked?: string | string[]) => {
|
||||
if (!picked || Array.isArray(picked)) { // We are sure AutocompleteSelector only returns one, since it is not multiselect.
|
||||
return;
|
||||
}
|
||||
setSelected(picked.value);
|
||||
setSelected(picked);
|
||||
|
||||
const bind = binding.bindings?.find((b) => b.location === picked.value);
|
||||
const bind = binding.bindings?.find((b) => b.location === picked);
|
||||
if (!bind) {
|
||||
logDebug('Trying to select element not present in binding.');
|
||||
return;
|
||||
}
|
||||
|
||||
const finish = await handleBindingSubmit(bind);
|
||||
finish();
|
||||
}, [handleBindingSubmit, binding.bindings]);
|
||||
const context = createCallContext(
|
||||
bind.app_id,
|
||||
AppBindingLocations.IN_POST + bind.location,
|
||||
post.channelId,
|
||||
teamID || currentTeamId,
|
||||
post.id,
|
||||
);
|
||||
|
||||
const options = useMemo(() => binding.bindings?.map<PostActionOption>((b: AppBinding) => ({
|
||||
text: b.label,
|
||||
value: b.location || '',
|
||||
})), [binding.bindings]);
|
||||
const res = await handleBindingClick(serverUrl, bind, context, intl);
|
||||
if (res.error) {
|
||||
const errorResponse = res.error;
|
||||
const errorMessage = errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error occurred.',
|
||||
});
|
||||
postEphemeralCallResponseForPost(serverUrl, errorResponse, errorMessage, post);
|
||||
return;
|
||||
}
|
||||
|
||||
const callResp = res.data!;
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.text) {
|
||||
postEphemeralCallResponseForPost(serverUrl, callResp, callResp.text, post);
|
||||
}
|
||||
return;
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
case AppCallResponseTypes.FORM:
|
||||
return;
|
||||
default: {
|
||||
const errorMessage = intl.formatMessage({
|
||||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {
|
||||
type: callResp.type,
|
||||
});
|
||||
postEphemeralCallResponseForPost(serverUrl, callResp, errorMessage, post);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const options = binding.bindings?.map<PostActionOption>((b: AppBinding) => ({text: b.label, value: b.location || ''}));
|
||||
|
||||
return (
|
||||
<AutocompleteSelector
|
||||
|
||||
@@ -31,7 +31,7 @@ const contentType: Record<string, string> = {
|
||||
|
||||
const Content = ({isReplyPost, layoutWidth, location, post, theme}: ContentProps) => {
|
||||
let type: string | undefined = post.metadata?.embeds?.[0].type;
|
||||
if (!type && post.props?.app_bindings?.length) {
|
||||
if (!type && post.props?.attachments?.length) {
|
||||
type = contentType.app_bindings;
|
||||
}
|
||||
|
||||
|
||||