Compare commits

..

2 Commits

Author SHA1 Message Date
Michael Kochell
70f51889e5 try gitpod with vnc 2022-10-26 18:47:58 -04:00
Michael Kochell
11242def6a add .gitpod.yml 2022-10-25 22:21:47 -04:00
439 changed files with 20570 additions and 30454 deletions

View File

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

@@ -0,0 +1,159 @@
FROM gitpod/workspace-full-vnc
ENV CYPRESS_CACHE_FOLDER=/workspace/.cypress-cache
# Install Cypress dependencies.
RUN sudo apt-get update \
&& sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
libgtk2.0-0 \
libgtk-3-0 \
libnotify-dev \
libgconf-2-4 \
libnss3 \
libxss1 \
libasound2 \
libxtst6 \
xauth \
xvfb \
&& sudo rm -rf /var/lib/apt/lists/*
RUN mkdir -p /workspace/persist/.cache/go-build
ENV GOCACHE=/workspace/persist/.cache/go-build
ENV MM_SERVICESETTINGS_ENABLEDEVELOPER=true
# Copied from https://github.com/react-native-community/docker-android/blob/master/Dockerfile
LABEL Description="This image provides a base Android development environment for React Native, and may be used to run tests."
ENV DEBIAN_FRONTEND=noninteractive
# set default build arguments
# https://developer.android.com/studio#command-tools
ARG SDK_VERSION=commandlinetools-linux-8512546_latest.zip
ARG ANDROID_BUILD_VERSION=31
ARG ANDROID_TOOLS_VERSION=31.0.0
ARG BUCK_VERSION=2022.05.05.01
# Buck doesn't support versions beyond NDK 21
# Therefore we need to diverge the NDK version and set NDK_HOME
# for Buck to pick it up correctly.
ARG NDK_VERSION_BUCK=21.4.7075529
ARG NDK_VERSION_GRADLE=23.1.7779620
ARG NODE_VERSION=14.x
ARG WATCHMAN_VERSION=4.9.0
ARG CMAKE_VERSION=3.18.1
# set default environment variables, please don't remove old env for compatibilty issue
ENV ADB_INSTALL_TIMEOUT=10
ENV ANDROID_HOME=/opt/android
ENV ANDROID_SDK_ROOT=${ANDROID_HOME}
ENV ANDROID_NDK_BUCK=${ANDROID_HOME}/ndk/$NDK_VERSION_BUCK
ENV ANDROID_NDK_GRADLE=${ANDROID_HOME}/ndk/$NDK_VERSION_GRADLE
# this is needed for Buck to be able to recognize NDK 21
ENV NDK_HOME=${ANDROID_HOME}/ndk/$NDK_VERSION_BUCK
ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
ENV CMAKE_BIN_PATH=${ANDROID_HOME}/cmake/$CMAKE_VERSION/bin
ENV PATH=${CMAKE_BIN_PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/emulator:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:/opt/buck/bin/:${PATH}
# Install system dependencies
RUN apt update -qq && apt install -qq -y --no-install-recommends \
apt-transport-https \
curl \
file \
gcc \
git \
g++ \
gnupg2 \
libc++1-10 \
libgl1 \
libtcmalloc-minimal4 \
make \
openjdk-11-jdk-headless \
openssh-client \
patch \
python3 \
python3-distutils \
rsync \
ruby \
ruby-dev \
tzdata \
unzip \
sudo \
ninja-build \
zip \
# Dev libraries requested by Hermes
libicu-dev \
# Emulator & video bridge dependencies
libc6 \
libdbus-1-3 \
libfontconfig1 \
libgcc1 \
libpulse0 \
libtinfo5 \
libx11-6 \
libxcb1 \
libxdamage1 \
libnss3 \
libxcomposite1 \
libxcursor1 \
libxi6 \
libxext6 \
libxfixes3 \
zlib1g \
libgl1 \
pulseaudio \
socat \
&& gem install bundler \
&& rm -rf /var/lib/apt/lists/*;
# install nodejs and yarn packages from nodesource
RUN curl -sL https://deb.nodesource.com/setup_${NODE_VERSION} | bash - \
&& apt-get update -qq \
&& apt-get install -qq -y --no-install-recommends nodejs \
&& npm i -g yarn \
&& rm -rf /var/lib/apt/lists/*
# download and install buck using the java11 pex from Jitpack
RUN curl -L https://jitpack.io/com/github/facebook/buck/v${BUCK_VERSION}/buck-v${BUCK_VERSION}-java11.pex -o /tmp/buck.pex \
&& mv /tmp/buck.pex /usr/local/bin/buck \
&& chmod +x /usr/local/bin/buck
# Full reference at https://dl.google.com/android/repository/repository2-1.xml
# download and unpack android
# workaround buck clang version detection by symlinking
RUN curl -sS https://dl.google.com/android/repository/${SDK_VERSION} -o /tmp/sdk.zip \
&& mkdir -p ${ANDROID_HOME}/cmdline-tools \
&& unzip -q -d ${ANDROID_HOME}/cmdline-tools /tmp/sdk.zip \
&& mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest \
&& rm /tmp/sdk.zip \
&& yes | sdkmanager --licenses \
&& yes | sdkmanager "platform-tools" \
"emulator" \
"platforms;android-$ANDROID_BUILD_VERSION" \
"build-tools;$ANDROID_TOOLS_VERSION" \
"cmake;$CMAKE_VERSION" \
"system-images;android-21;google_apis;armeabi-v7a" \
"ndk;$NDK_VERSION_BUCK" \
"ndk;$NDK_VERSION_GRADLE" \
&& rm -rf ${ANDROID_HOME}/.android \
&& chmod 777 -R /opt/android \
&& ln -s ${ANDROID_NDK_BUCK}/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/9.0.9 ${ANDROID_NDK_BUCK}/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/9.0.8
# Copied from https://github.com/gengjiawen/ci-sample/blob/master/.gitpod.Dockerfile
# FROM reactnativecommunity/react-native-android
### Gitpod user ###
# '-l': see https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user
RUN useradd -l -u 33333 -G sudo -md /home/gitpod -s /bin/bash -p gitpod gitpod \
# passwordless sudo for users in the 'sudo' group
&& sed -i.bkp -e 's/%sudo\s\+ALL=(ALL\(:ALL\)\?)\s\+ALL/%sudo ALL=NOPASSWD:ALL/g' /etc/sudoers
# Install custom tools, runtimes, etc.
# For example "bastet", a command-line tetris clone:
# RUN brew install bastet
#
# More information: https://www.gitpod.io/docs/config-docker/

6
.gitpod.yml Normal file
View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 730 KiB

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 730 KiB

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 730 KiB

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -124,4 +124,3 @@ export const getRedirectLocation = async (serverUrl: string, link: string) => {
return {error};
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -184,8 +184,6 @@ query ${QueryNames.QUERY_ENTRY} {
createAt
expiresAt
}
termsOfServiceId
termsOfServiceCreateAt
}
teamMembers(userId:"me") {
deleteAt

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -189,7 +189,6 @@ const Autocomplete = ({
nestedScrollEnabled={nestedScrollEnabled}
channelId={channelId}
rootId={rootId}
isAppsEnabled={isAppsEnabled}
/>
}
{/* {(isSearch && enableDateSuggestion) &&

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ type FileProps = {
galleryIdentifier: string;
index: number;
inViewPort: boolean;
isSingleImage?: boolean;
isSingleImage: boolean;
nonVisibleImagesCount: number;
onPress: (index: number) => void;
publicLinkEnabled: boolean;

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -63,7 +63,7 @@ export default function FileQuickAction({
>
<CompassIcon
color={color}
name='paperclip'
name='file-generic-outline'
size={ICON_SIZE}
/>
</TouchableWithFeedback>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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