forked from Ivasoft/mattermost-mobile
Compare commits
94 Commits
release-1.
...
v1.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d743e6454 | ||
|
|
8a7dabfadd | ||
|
|
c1b2667d6b | ||
|
|
4eff598665 | ||
|
|
0be1500055 | ||
|
|
c0c91ebf71 | ||
|
|
071c0f94f0 | ||
|
|
5e1e155d07 | ||
|
|
75ec18c0f4 | ||
|
|
d19e269dec | ||
|
|
bbd65b292f | ||
|
|
f0ce1fdc2b | ||
|
|
05259326f6 | ||
|
|
1787a04f6e | ||
|
|
76ed62b153 | ||
|
|
a717ec1757 | ||
|
|
2d7b3a5685 | ||
|
|
8f461acd50 | ||
|
|
dd318c2a10 | ||
|
|
c71389c98c | ||
|
|
30fb178f8b | ||
|
|
3f66128e3e | ||
|
|
c010cbc74b | ||
|
|
8ba96ecc70 | ||
|
|
b76469c867 | ||
|
|
c4e6d072da | ||
|
|
83dad65f1f | ||
|
|
82f4526704 | ||
|
|
2e4cdc8309 | ||
|
|
278fbe3ca9 | ||
|
|
ceaf153875 | ||
|
|
1ed3efeb40 | ||
|
|
8a72af7969 | ||
|
|
773590b83d | ||
|
|
a5e735233a | ||
|
|
ec1c66c9ec | ||
|
|
e1fd773850 | ||
|
|
09ed38ca98 | ||
|
|
c70d563a88 | ||
|
|
c44d6d49e9 | ||
|
|
251b91d7c0 | ||
|
|
72fce0cb87 | ||
|
|
a1f3c2c6ef | ||
|
|
afbcc2a203 | ||
|
|
94dec5e443 | ||
|
|
f6e23a9e0c | ||
|
|
fe1fc259ad | ||
|
|
e480dcfacb | ||
|
|
62026e375a | ||
|
|
9c22be4465 | ||
|
|
b5b948e58f | ||
|
|
5d9b8a7e06 | ||
|
|
2b495f4c51 | ||
|
|
49a284f46d | ||
|
|
61a33b3025 | ||
|
|
01d7d08b25 | ||
|
|
4bd364df42 | ||
|
|
ef808262d7 | ||
|
|
95f8e72c11 | ||
|
|
3ea3fe7e2f | ||
|
|
2e5f7fd60c | ||
|
|
cb98efc6da | ||
|
|
09a49302f2 | ||
|
|
68fe3653a8 | ||
|
|
2cf74c07cb | ||
|
|
a70688fe44 | ||
|
|
b8f4757300 | ||
|
|
b3ec19fe17 | ||
|
|
faa3a55b3d | ||
|
|
dff9628b6e | ||
|
|
82d9995f2b | ||
|
|
112fd06dfd | ||
|
|
92541025ef | ||
|
|
d927642cb9 | ||
|
|
696b2e7c5e | ||
|
|
bfac3546c9 | ||
|
|
e170bc1177 | ||
|
|
b41777a7e0 | ||
|
|
783dc66b2a | ||
|
|
1c093f70a4 | ||
|
|
63bcdc42fb | ||
|
|
793aea617c | ||
|
|
ed3e822b63 | ||
|
|
7bbe6ccd9f | ||
|
|
bc159153d6 | ||
|
|
167b91d7fd | ||
|
|
87b3e3e724 | ||
|
|
cb1e286e4f | ||
|
|
fb2f711359 | ||
|
|
07ff66726c | ||
|
|
2bb69a9b7e | ||
|
|
a25423eb4b | ||
|
|
dfb0557de6 | ||
|
|
093484eab3 |
52
Makefile
52
Makefile
@@ -1,8 +1,9 @@
|
||||
.PHONY: pre-run clean
|
||||
.PHONY: pre-run pre-build clean
|
||||
.PHONY: check-style
|
||||
.PHONY: start stop
|
||||
.PHONY: run run-ios run-android
|
||||
.PHONY: build-ios build-android unsigned-ios unsigned-android
|
||||
.PHONY: build build-ios build-android unsigned-ios unsigned-android
|
||||
.PHONY: build-pr can-build-pr prepare-pr
|
||||
.PHONY: test help
|
||||
|
||||
POD := $(shell which pod 2> /dev/null)
|
||||
@@ -19,6 +20,15 @@ node_modules: package.json
|
||||
@echo Getting Javascript dependencies
|
||||
@npm install
|
||||
|
||||
npm-ci: package.json
|
||||
@if ! [ $(shell which npm 2> /dev/null) ]; then \
|
||||
echo "npm is not installed https://npmjs.com"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@echo Getting Javascript dependencies
|
||||
@npm ci
|
||||
|
||||
.podinstall:
|
||||
ifeq ($(OS), Darwin)
|
||||
ifdef POD
|
||||
@@ -43,6 +53,8 @@ dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
|
||||
|
||||
pre-run: | node_modules .podinstall dist/assets ## Installs dependencies and assets
|
||||
|
||||
pre-build: | npm-ci .podinstall dist/assets ## Install dependencies and assets before building
|
||||
|
||||
check-style: node_modules ## Runs eslint
|
||||
@echo Checking for style guide compliance
|
||||
@npm run check
|
||||
@@ -60,9 +72,6 @@ clean: ## Cleans dependencies, previous builds and temp files
|
||||
@echo Cleanup finished
|
||||
|
||||
post-install:
|
||||
@# Need to copy custom ImagePickerModule.java that implements correct permission checks for android
|
||||
@rm node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
|
||||
@cp ./native_modules/ImagePickerModule.java node_modules/react-native-image-picker/android/src/main/java/com/imagepicker
|
||||
@# Need to copy custom RNDocumentPicker.m that implements direct access to the document picker in iOS
|
||||
@cp ./native_modules/RNDocumentPicker.m node_modules/react-native-document-picker/ios/RNDocumentPicker/RNDocumentPicker.m
|
||||
|
||||
@@ -170,7 +179,17 @@ run-android: | check-device-android pre-run prepare-android-build ## Runs the ap
|
||||
fi; \
|
||||
fi
|
||||
|
||||
build-ios: | pre-run check-style ## Creates an iOS build
|
||||
build: | stop pre-build check-style ## Builds the app for Android & iOS
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
fi
|
||||
@echo "Building App"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build
|
||||
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
|
||||
|
||||
|
||||
build-ios: | stop pre-build check-style ## Builds the iOS app
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
@@ -179,7 +198,7 @@ build-ios: | pre-run check-style ## Creates an iOS build
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
|
||||
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
|
||||
|
||||
build-android: | pre-run check-style prepare-android-build ## Creates an Android build
|
||||
build-android: | stop pre-build check-style prepare-android-build ## Build the Android app
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
@@ -188,7 +207,7 @@ build-android: | pre-run check-style prepare-android-build ## Creates an Android
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
|
||||
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
|
||||
|
||||
unsigned-ios: pre-run check-style
|
||||
unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS app
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
@@ -202,7 +221,7 @@ unsigned-ios: pre-run check-style
|
||||
@rm -rf build-ios/
|
||||
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
|
||||
|
||||
unsigned-android: pre-run check-style prepare-android-build
|
||||
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
@@ -215,6 +234,21 @@ unsigned-android: pre-run check-style prepare-android-build
|
||||
test: | pre-run check-style ## Runs tests
|
||||
@npm test
|
||||
|
||||
build-pr: | can-build-pr stop pre-build check-style ## Build a PR from the mattermost-mobile repo
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
fi
|
||||
@echo "Building App from PR ${PR_ID}"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build_pr pr:PR-${PR_ID}
|
||||
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
|
||||
|
||||
can-build-pr:
|
||||
@if [ -z ${PR_ID} ]; then \
|
||||
echo a PR number needs to be specified; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
@@ -1361,7 +1361,7 @@ SOFTWARE.
|
||||
|
||||
## react-native-drawer-layout
|
||||
|
||||
This product contains 'react-native-drawer-layout' by Brent Vatne.
|
||||
This product contains a modified version of 'react-native-drawer-layout' by Brent Vatne.
|
||||
|
||||
A platform-agnostic drawer layout. Pure JavaScript implementation on iOS and native implementation on Android. Why? Because the drawer layout is a useful component regardless of the platform! And if you can use it without changing any code, that's perfect
|
||||
|
||||
|
||||
@@ -113,8 +113,8 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 144
|
||||
versionName "1.13.0"
|
||||
versionCode 158
|
||||
versionName "1.14.0"
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
@@ -214,11 +214,11 @@ dependencies {
|
||||
implementation project(':react-native-recyclerview-list')
|
||||
|
||||
// For animated GIF support
|
||||
implementation 'com.facebook.fresco:animated-base-support:1.3.0'
|
||||
implementation 'com.facebook.fresco:fresco:1.10.0'
|
||||
implementation 'com.facebook.fresco:animated-gif:1.10.0'
|
||||
// For WebP support, including animated WebP
|
||||
implementation 'com.facebook.fresco:animated-gif:1.3.0'
|
||||
implementation 'com.facebook.fresco:animated-webp:1.3.0'
|
||||
implementation 'com.facebook.fresco:webpsupport:1.3.0'
|
||||
implementation 'com.facebook.fresco:animated-webp:1.10.0'
|
||||
implementation 'com.facebook.fresco:webpsupport:1.10.0'
|
||||
}
|
||||
|
||||
// Run this once to be able to run the application with BUCK
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
>
|
||||
<meta-data android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
|
||||
BIN
android/app/src/main/assets/fonts/Roboto-Black.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Roboto-Black.ttf
Executable file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Roboto-BlackItalic.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Roboto-BlackItalic.ttf
Executable file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Roboto-Bold.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Roboto-Bold.ttf
Executable file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Roboto-BoldItalic.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Roboto-BoldItalic.ttf
Executable file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Roboto-Italic.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Roboto-Italic.ttf
Executable file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Roboto-Light.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Roboto-Light.ttf
Executable file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Roboto-LightItalic.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Roboto-LightItalic.ttf
Executable file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Roboto-Medium.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Roboto-Medium.ttf
Executable file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Roboto-MediumItalic.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Roboto-MediumItalic.ttf
Executable file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Roboto-Regular.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Roboto-Regular.ttf
Executable file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Roboto-Thin.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Roboto-Thin.ttf
Executable file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Roboto-ThinItalic.ttf
Executable file
BIN
android/app/src/main/assets/fonts/Roboto-ThinItalic.ttf
Executable file
Binary file not shown.
@@ -10,6 +10,7 @@ import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.ArraySet;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
@@ -69,9 +70,13 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
|
||||
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
|
||||
LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
if (managedConfig!= null && managedConfig.size() > 0 && activity != null) {
|
||||
if (managedConfig != null && managedConfig.size() > 0 && activity != null) {
|
||||
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
|
||||
}
|
||||
|
||||
if (activity != null) {
|
||||
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
11
android/app/src/main/res/xml/network_security_config.xml
Normal file
11
android/app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config>
|
||||
<trust-anchors>
|
||||
<!-- Trust preinstalled CAs -->
|
||||
<certificates src="system" />
|
||||
<!-- Additionally trust user added CAs -->
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {PostTypes} from 'mattermost-redux/action_types';
|
||||
import {doPostAction} from 'mattermost-redux/actions/posts';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
@@ -50,3 +51,20 @@ export function setMenuActionSelector(dataSource, onSelect, options) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function selectAttachmentMenuAction(postId, actionId, displayText, value) {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ViewTypes.SUBMIT_ATTACHMENT_MENU_ACTION,
|
||||
postId,
|
||||
data: {
|
||||
[actionId]: {
|
||||
displayText,
|
||||
value,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
dispatch(doPostAction(postId, actionId, value));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function handleSearchDraftChanged(text) {
|
||||
@@ -11,3 +13,25 @@ export function handleSearchDraftChanged(text) {
|
||||
}, getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function showSearchModal(navigator, initialValue = '') {
|
||||
return (dispatch, getState) => {
|
||||
const theme = getTheme(getState());
|
||||
|
||||
const options = {
|
||||
screen: 'Search',
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
overrideBackPress: true,
|
||||
passProps: {
|
||||
initialValue,
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
};
|
||||
|
||||
navigator.showModal(options);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
|
||||
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
|
||||
import {getMyTeams} from 'mattermost-redux/actions/teams';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {selectFirstAvailableTeam} from 'app/utils/teams';
|
||||
|
||||
import {setChannelDisplayName} from './channel';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
export function handleTeamChange(teamId, selectChannel = true) {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -46,21 +48,30 @@ export function selectDefaultTeam() {
|
||||
const {teams: allTeams, myMembers} = state.entities.teams;
|
||||
const teams = Object.keys(myMembers).map((key) => allTeams[key]);
|
||||
|
||||
let defaultTeam;
|
||||
if (ExperimentalPrimaryTeam) {
|
||||
defaultTeam = teams.find((t) => t.name === ExperimentalPrimaryTeam.toLowerCase());
|
||||
}
|
||||
|
||||
if (!defaultTeam) {
|
||||
defaultTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
|
||||
}
|
||||
let defaultTeam = selectFirstAvailableTeam(teams, ExperimentalPrimaryTeam);
|
||||
|
||||
if (defaultTeam) {
|
||||
handleTeamChange(defaultTeam.id)(dispatch, getState);
|
||||
dispatch(handleTeamChange(defaultTeam.id));
|
||||
} else if (state.requests.teams.getTeams.status === RequestStatus.FAILURE || state.requests.teams.getMyTeams.status === RequestStatus.FAILURE) {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_ERROR_TEAMS);
|
||||
} else {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
|
||||
// If for some reason we reached this point cause of a failure in rehydration or something
|
||||
// lets fetch the teams one more time to make sure the user does not belong to any team
|
||||
const {data, error} = await dispatch(getMyTeams());
|
||||
if (error) {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_ERROR_TEAMS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
defaultTeam = selectFirstAvailableTeam(data, ExperimentalPrimaryTeam);
|
||||
}
|
||||
|
||||
if (defaultTeam) {
|
||||
dispatch(handleTeamChange(defaultTeam.id));
|
||||
} else {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
31
app/app.js
31
app/app.js
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable global-require*/
|
||||
import {AsyncStorage, Linking, NativeModules, Platform} from 'react-native';
|
||||
import {AsyncStorage, Linking, NativeModules, Platform, Text} from 'react-native';
|
||||
import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain';
|
||||
|
||||
import {loadMe} from 'mattermost-redux/actions/users';
|
||||
@@ -62,10 +62,37 @@ export default class App {
|
||||
// Usage deeplinking
|
||||
Linking.addEventListener('url', this.handleDeepLink);
|
||||
|
||||
this.setFontFamily();
|
||||
this.getStartupThemes();
|
||||
this.getAppCredentials();
|
||||
}
|
||||
|
||||
setFontFamily = () => {
|
||||
// Set a global font for Android
|
||||
if (Platform.OS === 'android') {
|
||||
const defaultFontFamily = {
|
||||
style: {
|
||||
fontFamily: 'Roboto',
|
||||
},
|
||||
};
|
||||
const TextRender = Text.render;
|
||||
const initialDefaultProps = Text.defaultProps;
|
||||
Text.defaultProps = {
|
||||
...initialDefaultProps,
|
||||
...defaultFontFamily,
|
||||
};
|
||||
Text.render = function render(props, ...args) {
|
||||
const oldProps = props;
|
||||
let newProps = {...props, style: [defaultFontFamily.style, props.style]};
|
||||
try {
|
||||
return Reflect.apply(TextRender, this, [newProps, ...args]);
|
||||
} finally {
|
||||
newProps = oldProps;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
getTranslations = () => {
|
||||
if (this.translations) {
|
||||
return this.translations;
|
||||
@@ -115,6 +142,8 @@ export default class App {
|
||||
this.waitForRehydration = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.waitForRehydration = false;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import AnnouncementBanner from './announcement_banner.js';
|
||||
|
||||
jest.useFakeTimers();
|
||||
@@ -16,7 +18,7 @@ describe('AnnouncementBanner', () => {
|
||||
bannerText: 'Banner Text',
|
||||
bannerTextColor: '#fff',
|
||||
navigator: {},
|
||||
theme: {},
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import {DocumentPicker} from 'react-native-document-picker';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
@@ -27,8 +29,10 @@ export default class AttachmentButton extends PureComponent {
|
||||
children: PropTypes.node,
|
||||
fileCount: PropTypes.number,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onShowFileSizeWarning: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired,
|
||||
wrapper: PropTypes.bool,
|
||||
@@ -42,11 +46,17 @@ export default class AttachmentButton extends PureComponent {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
attachFileFromCamera = async () => {
|
||||
attachPhotoFromCamera = () => {
|
||||
return this.attachFileFromCamera('photo');
|
||||
};
|
||||
|
||||
attachFileFromCamera = async (mediaType) => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const options = {
|
||||
quality: 1.0,
|
||||
quality: 0.8,
|
||||
videoQuality: 'high',
|
||||
noData: true,
|
||||
mediaType,
|
||||
storageOptions: {
|
||||
cameraRoll: true,
|
||||
waitUntilSaved: true,
|
||||
@@ -84,7 +94,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
attachFileFromLibrary = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const options = {
|
||||
quality: 1.0,
|
||||
quality: 0.8,
|
||||
noData: true,
|
||||
permissionDenied: {
|
||||
title: formatMessage({
|
||||
@@ -116,10 +126,14 @@ export default class AttachmentButton extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
attachVideoFromCamera = () => {
|
||||
return this.attachFileFromCamera('video');
|
||||
};
|
||||
|
||||
attachVideoFromLibraryAndroid = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const options = {
|
||||
quality: 1.0,
|
||||
videoQuality: 'high',
|
||||
mediaType: 'video',
|
||||
noData: true,
|
||||
permissionDenied: {
|
||||
@@ -283,8 +297,20 @@ export default class AttachmentButton extends PureComponent {
|
||||
return true;
|
||||
};
|
||||
|
||||
uploadFiles = (images) => {
|
||||
this.props.uploadFiles(images);
|
||||
uploadFiles = async (files) => {
|
||||
const file = files[0];
|
||||
if (!file.fileSize | !file.fileName) {
|
||||
const path = (file.path || file.uri).replace('file://', '');
|
||||
const fileInfo = await RNFetchBlob.fs.stat(path);
|
||||
file.fileSize = fileInfo.size;
|
||||
file.fileName = fileInfo.filename;
|
||||
}
|
||||
|
||||
if (file.fileSize > this.props.maxFileSize) {
|
||||
this.props.onShowFileSizeWarning(file.fileName);
|
||||
} else {
|
||||
this.props.uploadFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
handleFileAttachmentOption = (action) => {
|
||||
@@ -313,12 +339,19 @@ export default class AttachmentButton extends PureComponent {
|
||||
this.props.blurTextBox();
|
||||
const options = {
|
||||
items: [{
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromCamera),
|
||||
action: () => this.handleFileAttachmentOption(this.attachPhotoFromCamera),
|
||||
text: {
|
||||
id: t('mobile.file_upload.camera'),
|
||||
defaultMessage: 'Take Photo or Video',
|
||||
id: t('mobile.file_upload.camera_photo'),
|
||||
defaultMessage: 'Take Photo',
|
||||
},
|
||||
icon: 'camera',
|
||||
}, {
|
||||
action: () => this.handleFileAttachmentOption(this.attachVideoFromCamera),
|
||||
text: {
|
||||
id: t('mobile.file_upload.camera_video'),
|
||||
defaultMessage: 'Take Video',
|
||||
},
|
||||
icon: 'video-camera',
|
||||
}, {
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromLibrary),
|
||||
text: {
|
||||
|
||||
@@ -26,8 +26,8 @@ export default class AtMention extends PureComponent {
|
||||
defaultChannel: PropTypes.object,
|
||||
inChannel: PropTypes.array,
|
||||
isSearch: PropTypes.bool,
|
||||
listHeight: PropTypes.number,
|
||||
matchTerm: PropTypes.string,
|
||||
maxListHeight: PropTypes.number,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
onResultCountChange: PropTypes.func.isRequired,
|
||||
outChannel: PropTypes.array,
|
||||
@@ -204,7 +204,7 @@ export default class AtMention extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {isSearch, listHeight, theme} = this.props;
|
||||
const {maxListHeight, theme} = this.props;
|
||||
const {mentionComplete, sections} = this.state;
|
||||
|
||||
if (sections.length === 0 || mentionComplete) {
|
||||
@@ -219,7 +219,7 @@ export default class AtMention extends PureComponent {
|
||||
<SectionList
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={this.keyExtractor}
|
||||
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
|
||||
style={[style.listView, {maxHeight: maxListHeight}]}
|
||||
sections={sections}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
@@ -235,8 +235,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
listView: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
search: {
|
||||
minHeight: 125,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ export default class Autocomplete extends PureComponent {
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
deviceHeight: PropTypes.number,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
maxHeight: PropTypes.number,
|
||||
rootId: PropTypes.string,
|
||||
isSearch: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
@@ -84,12 +85,21 @@ export default class Autocomplete extends PureComponent {
|
||||
this.setState({keyboardOffset: 0});
|
||||
};
|
||||
|
||||
listHeight() {
|
||||
let offset = Platform.select({ios: 65, android: 75});
|
||||
if (DeviceInfo.getModel().includes('iPhone X')) {
|
||||
offset = 90;
|
||||
maxListHeight() {
|
||||
let maxHeight;
|
||||
if (this.props.maxHeight) {
|
||||
maxHeight = this.props.maxHeight;
|
||||
} else {
|
||||
// List is expanding downwards, likely from the search box
|
||||
let offset = Platform.select({ios: 65, android: 75});
|
||||
if (DeviceInfo.getModel().includes('iPhone X')) {
|
||||
offset = 90;
|
||||
}
|
||||
|
||||
maxHeight = this.props.deviceHeight - offset - this.state.keyboardOffset;
|
||||
}
|
||||
return this.props.deviceHeight - offset - this.state.keyboardOffset;
|
||||
|
||||
return maxHeight;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -113,26 +123,29 @@ export default class Autocomplete extends PureComponent {
|
||||
containerStyle.push(style.borders);
|
||||
}
|
||||
}
|
||||
const listHeight = this.listHeight();
|
||||
|
||||
const maxListHeight = this.maxListHeight();
|
||||
|
||||
return (
|
||||
<View style={wrapperStyle}>
|
||||
<View style={containerStyle}>
|
||||
<AtMention
|
||||
listHeight={listHeight}
|
||||
maxListHeight={maxListHeight}
|
||||
onResultCountChange={this.handleAtMentionCountChange}
|
||||
{...this.props}
|
||||
/>
|
||||
<ChannelMention
|
||||
listHeight={listHeight}
|
||||
maxListHeight={maxListHeight}
|
||||
onResultCountChange={this.handleChannelMentionCountChange}
|
||||
{...this.props}
|
||||
/>
|
||||
<EmojiSuggestion
|
||||
maxListHeight={maxListHeight}
|
||||
onResultCountChange={this.handleEmojiCountChange}
|
||||
{...this.props}
|
||||
/>
|
||||
<SlashSuggestion
|
||||
maxListHeight={maxListHeight}
|
||||
onResultCountChange={this.handleCommandCountChange}
|
||||
{...this.props}
|
||||
/>
|
||||
@@ -167,7 +180,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
container: {
|
||||
bottom: 0,
|
||||
maxHeight: 200,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
|
||||
@@ -39,8 +39,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 8,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
},
|
||||
sectionText: {
|
||||
fontSize: 12,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {SectionList} from 'react-native';
|
||||
import {Platform, SectionList} from 'react-native';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
|
||||
@@ -25,8 +25,8 @@ export default class ChannelMention extends PureComponent {
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
isSearch: PropTypes.bool,
|
||||
listHeight: PropTypes.number,
|
||||
matchTerm: PropTypes.string,
|
||||
maxListHeight: PropTypes.number,
|
||||
myChannels: PropTypes.array,
|
||||
myMembers: PropTypes.object,
|
||||
otherChannels: PropTypes.array,
|
||||
@@ -158,6 +158,10 @@ export default class ChannelMention extends PureComponent {
|
||||
if (isSearch) {
|
||||
const channelOrIn = mentionPart.includes('in:') ? 'in:' : 'channel:';
|
||||
completedDraft = mentionPart.replace(CHANNEL_MENTION_SEARCH_REGEX, `${channelOrIn} ${mention} `);
|
||||
} else if (Platform.OS === 'ios') {
|
||||
// We are going to set a double ~ on iOS to prevent the auto correct from taking over and replacing it
|
||||
// with the wrong value, this is a hack but I could not found another way to solve it
|
||||
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~~${mention} `);
|
||||
} else {
|
||||
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
|
||||
}
|
||||
@@ -167,6 +171,14 @@ export default class ChannelMention extends PureComponent {
|
||||
}
|
||||
|
||||
onChangeText(completedDraft, true);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
// This is the second part of the hack were we replace the double ~ with just one
|
||||
// after the auto correct vanished
|
||||
setTimeout(() => {
|
||||
onChangeText(completedDraft.replace(`~~${mention} `, `~${mention} `));
|
||||
});
|
||||
}
|
||||
this.setState({mentionComplete: true});
|
||||
};
|
||||
|
||||
@@ -194,7 +206,7 @@ export default class ChannelMention extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {isSearch, listHeight, theme} = this.props;
|
||||
const {maxListHeight, theme} = this.props;
|
||||
const {mentionComplete, sections} = this.state;
|
||||
|
||||
if (sections.length === 0 || mentionComplete) {
|
||||
@@ -209,7 +221,7 @@ export default class ChannelMention extends PureComponent {
|
||||
<SectionList
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={this.keyExtractor}
|
||||
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
|
||||
style={[style.listView, {maxHeight: maxListHeight}]}
|
||||
sections={sections}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
@@ -225,8 +237,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
listView: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
search: {
|
||||
minHeight: 125,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -15,7 +15,6 @@ import {changeOpacity} from 'app/utils/theme';
|
||||
export default class DateSuggestion extends PureComponent {
|
||||
static propTypes = {
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
listHeight: PropTypes.number,
|
||||
locale: PropTypes.string.isRequired,
|
||||
matchTerm: PropTypes.string,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FlatList,
|
||||
Platform,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
@@ -29,6 +30,7 @@ export default class EmojiSuggestion extends Component {
|
||||
emojis: PropTypes.array.isRequired,
|
||||
isSearch: PropTypes.bool,
|
||||
fuse: PropTypes.object.isRequired,
|
||||
maxListHeight: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
onResultCountChange: PropTypes.func.isRequired,
|
||||
@@ -132,13 +134,28 @@ export default class EmojiSuggestion extends Component {
|
||||
actions.addReactionToLatestPost(emoji, rootId);
|
||||
onChangeText('');
|
||||
} else {
|
||||
let completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `:${emoji}: `);
|
||||
// We are going to set a double : on iOS to prevent the auto correct from taking over and replacing it
|
||||
// with the wrong value, this is a hack but I could not found another way to solve it
|
||||
let completedDraft;
|
||||
if (Platform.OS === 'ios') {
|
||||
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `::${emoji}: `);
|
||||
} else {
|
||||
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `:${emoji}: `);
|
||||
}
|
||||
|
||||
if (value.length > cursorPosition) {
|
||||
completedDraft += value.substring(cursorPosition);
|
||||
}
|
||||
|
||||
onChangeText(completedDraft);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
// This is the second part of the hack were we replace the double : with just one
|
||||
// after the auto correct vanished
|
||||
setTimeout(() => {
|
||||
onChangeText(completedDraft.replace(`::${emoji}: `, `:${emoji}: `));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
@@ -171,18 +188,20 @@ export default class EmojiSuggestion extends Component {
|
||||
getItemLayout = ({index}) => ({length: 40, offset: 40 * index, index})
|
||||
|
||||
render() {
|
||||
const {maxListHeight, theme} = this.props;
|
||||
|
||||
if (!this.state.active) {
|
||||
// If we are not in an active state return null so nothing is rendered
|
||||
// other components are not blocked.
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps='always'
|
||||
style={style.listView}
|
||||
style={[style.listView, {maxHeight: maxListHeight}]}
|
||||
extraData={this.state}
|
||||
data={this.state.dataSource}
|
||||
keyExtractor={this.keyExtractor}
|
||||
|
||||
@@ -5,10 +5,12 @@ import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FlatList,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import SlashSuggestionItem from './slash_suggestion_item';
|
||||
@@ -25,6 +27,7 @@ export default class SlashSuggestion extends Component {
|
||||
commands: PropTypes.array,
|
||||
commandsRequest: PropTypes.object.isRequired,
|
||||
isSearch: PropTypes.bool,
|
||||
maxListHeight: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
onResultCountChange: PropTypes.func.isRequired,
|
||||
@@ -110,10 +113,23 @@ export default class SlashSuggestion extends Component {
|
||||
completeSuggestion = (command) => {
|
||||
const {onChangeText} = this.props;
|
||||
|
||||
const completedDraft = `/${command} `;
|
||||
// We are going to set a double / on iOS to prevent the auto correct from taking over and replacing it
|
||||
// with the wrong value, this is a hack but I could not found another way to solve it
|
||||
let completedDraft = `/${command} `;
|
||||
if (Platform.OS === 'ios') {
|
||||
completedDraft = `//${command} `;
|
||||
}
|
||||
|
||||
onChangeText(completedDraft);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
// This is the second part of the hack were we replace the double / with just one
|
||||
// after the auto correct vanished
|
||||
setTimeout(() => {
|
||||
onChangeText(completedDraft.replace(`//${command} `, `/${command} `));
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
active: false,
|
||||
suggestionComplete: true,
|
||||
@@ -133,22 +149,25 @@ export default class SlashSuggestion extends Component {
|
||||
)
|
||||
|
||||
render() {
|
||||
const {maxListHeight, theme} = this.props;
|
||||
|
||||
if (!this.state.active) {
|
||||
// If we are not in an active state return null so nothing is rendered
|
||||
// other components are not blocked.
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps='always'
|
||||
style={style.listView}
|
||||
style={[style.listView, {maxHeight: maxListHeight}]}
|
||||
extraData={this.state}
|
||||
data={this.state.dataSource}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.renderItem}
|
||||
ItemSeparatorComponent={AutocompleteDivider}
|
||||
pageSize={10}
|
||||
initialListSize={10}
|
||||
/>
|
||||
|
||||
@@ -53,8 +53,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 8,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
|
||||
@@ -23,7 +23,6 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
membersCount: PropTypes.number,
|
||||
size: PropTypes.number,
|
||||
status: PropTypes.string,
|
||||
teammateDeletedAt: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
isArchived: PropTypes.bool.isRequired,
|
||||
@@ -46,7 +45,6 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
size,
|
||||
status,
|
||||
theme,
|
||||
teammateDeletedAt,
|
||||
type,
|
||||
isArchived,
|
||||
} = this.props;
|
||||
@@ -116,13 +114,6 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (type === General.DM_CHANNEL && teammateDeletedAt) {
|
||||
icon = (
|
||||
<Image
|
||||
source={require('assets/images/status/archive_avatar.png')}
|
||||
style={{width: size, height: size, tintColor: offlineColor}}
|
||||
/>
|
||||
);
|
||||
} else if (type === General.DM_CHANNEL) {
|
||||
switch (status) {
|
||||
case General.AWAY:
|
||||
|
||||
@@ -27,7 +27,7 @@ exports[`CustomList should match snapshot 1`] = `
|
||||
scrollRenderAheadDistance={0}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#aaa",
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ exports[`CustomList should match snapshot, renderFooter 2`] = `
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#aaa",
|
||||
"backgroundColor": "#ffffff",
|
||||
"height": 70,
|
||||
"justifyContent": "center",
|
||||
}
|
||||
@@ -53,7 +53,7 @@ exports[`CustomList should match snapshot, renderFooter 2`] = `
|
||||
id="mobile.loading_members"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(170,170,170,0.6)",
|
||||
"color": "rgba(61,60,64,0.6)",
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -64,14 +64,14 @@ exports[`CustomList should match snapshot, renderSectionHeader 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#aaa",
|
||||
"backgroundColor": "#ffffff",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(170,170,170,0.07)",
|
||||
"backgroundColor": "rgba(61,60,64,0.07)",
|
||||
"paddingLeft": 10,
|
||||
"paddingVertical": 2,
|
||||
}
|
||||
@@ -80,7 +80,7 @@ exports[`CustomList should match snapshot, renderSectionHeader 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"color": "#aaa",
|
||||
"color": "#3d3c40",
|
||||
"fontWeight": "600",
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ exports[`CustomList should match snapshot, renderSeparator 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(170,170,170,0.1)",
|
||||
"backgroundColor": "rgba(61,60,64,0.1)",
|
||||
"flex": 1,
|
||||
"height": 1,
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ export default class ChannelListRow extends React.PureComponent {
|
||||
return (
|
||||
<CustomListRow
|
||||
id={this.props.id}
|
||||
theme={this.props.theme}
|
||||
onPress={this.props.onPress ? this.onPress : null}
|
||||
enabled={this.props.enabled}
|
||||
selectable={this.props.selectable}
|
||||
@@ -84,6 +83,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
purpose: {
|
||||
marginTop: 7,
|
||||
|
||||
@@ -4,17 +4,16 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import ConditionalTouchable from 'app/components/conditional_touchable';
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class CustomListRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func,
|
||||
enabled: PropTypes.bool,
|
||||
selectable: PropTypes.bool,
|
||||
@@ -28,12 +27,11 @@ export default class CustomListRow extends React.PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<ConditionalTouchable
|
||||
touchable={Boolean(this.props.enabled && this.props.onPress)}
|
||||
onPress={this.props.onPress}
|
||||
style={style.touchable}
|
||||
>
|
||||
<View style={style.container}>
|
||||
{this.props.selectable &&
|
||||
@@ -58,40 +56,40 @@ export default class CustomListRow extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: 65,
|
||||
paddingHorizontal: 15,
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
children: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
selector: {
|
||||
height: 28,
|
||||
width: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#888',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
selectorContainer: {
|
||||
flex: 1,
|
||||
height: 50,
|
||||
paddingRight: 15,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
selectorDisabled: {
|
||||
backgroundColor: '#888',
|
||||
},
|
||||
selectorFilled: {
|
||||
backgroundColor: '#378FD2',
|
||||
borderWidth: 0,
|
||||
},
|
||||
};
|
||||
const style = StyleSheet.create({
|
||||
touchable: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: 65,
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
children: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
selector: {
|
||||
height: 28,
|
||||
width: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#888',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
selectorContainer: {
|
||||
height: 50,
|
||||
paddingRight: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
selectorDisabled: {
|
||||
backgroundColor: '#888',
|
||||
},
|
||||
selectorFilled: {
|
||||
backgroundColor: '#378FD2',
|
||||
borderWidth: 0,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import {createMembersSections, loadingText} from 'app/utils/member_list';
|
||||
|
||||
import CustomList from './index';
|
||||
@@ -13,7 +15,7 @@ describe('CustomList', () => {
|
||||
|
||||
const baseProps = {
|
||||
data: [{username: 'username_1'}, {username: 'username_2'}],
|
||||
theme: {centerChannelBg: '#aaa', centerChannelColor: '#aaa'},
|
||||
theme: Preferences.THEMES.default,
|
||||
searching: false,
|
||||
onListEndReached: emptyFunc,
|
||||
onListEndReachedThreshold: 0,
|
||||
|
||||
@@ -44,7 +44,6 @@ export default class OptionListRow extends React.PureComponent {
|
||||
return (
|
||||
<CustomListRow
|
||||
id={value}
|
||||
theme={theme}
|
||||
onPress={this.onPress}
|
||||
enabled={enabled}
|
||||
selectable={selectable}
|
||||
|
||||
@@ -6,7 +6,7 @@ exports[`UserListRow should match snapshot 1`] = `
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginLeft": 10,
|
||||
"marginHorizontal": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -14,41 +14,13 @@ exports[`UserListRow should match snapshot 1`] = `
|
||||
enabled={true}
|
||||
id="21345"
|
||||
onPress={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"color": "#3d3c40",
|
||||
"flexDirection": "row",
|
||||
"marginLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -59,14 +31,12 @@ exports[`UserListRow should match snapshot 1`] = `
|
||||
</Component>
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"marginLeft": 5,
|
||||
},
|
||||
Object {
|
||||
"justifyContent": "center",
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component>
|
||||
@@ -84,13 +54,6 @@ exports[`UserListRow should match snapshot 1`] = `
|
||||
</Component>
|
||||
</Component>
|
||||
</Component>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"width": 25,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomListRow>
|
||||
</Component>
|
||||
`;
|
||||
@@ -101,7 +64,7 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginLeft": 10,
|
||||
"marginHorizontal": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -109,41 +72,13 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
|
||||
enabled={true}
|
||||
id="21345"
|
||||
onPress={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"color": "#3d3c40",
|
||||
"flexDirection": "row",
|
||||
"marginLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -154,14 +89,12 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
|
||||
</Component>
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"marginLeft": 5,
|
||||
},
|
||||
Object {
|
||||
"justifyContent": "center",
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component>
|
||||
@@ -177,13 +110,6 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
|
||||
/>
|
||||
</Component>
|
||||
</Component>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"width": 25,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomListRow>
|
||||
</Component>
|
||||
`;
|
||||
@@ -194,7 +120,7 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginLeft": 10,
|
||||
"marginHorizontal": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -202,41 +128,13 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
|
||||
enabled={true}
|
||||
id="21345"
|
||||
onPress={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"color": "#3d3c40",
|
||||
"flexDirection": "row",
|
||||
"marginLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -247,14 +145,12 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
|
||||
</Component>
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"marginLeft": 5,
|
||||
},
|
||||
Object {
|
||||
"justifyContent": "center",
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component>
|
||||
@@ -267,16 +163,22 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
@user
|
||||
</Component>
|
||||
</Component>
|
||||
<Component>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"color": undefined,
|
||||
"fontSize": 12,
|
||||
"marginTop": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
</Component>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"width": 25,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomListRow>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
@@ -58,13 +58,6 @@ export default class UserListRow extends React.PureComponent {
|
||||
}, {username});
|
||||
}
|
||||
|
||||
if (user.delete_at > 0) {
|
||||
usernameDisplay = formatMessage({
|
||||
id: 'more_direct_channels.directchannel.deactivated',
|
||||
defaultMessage: '{displayname} - Deactivated',
|
||||
}, {displayname: usernameDisplay});
|
||||
}
|
||||
|
||||
const teammateDisplay = displayUsername(user, teammateNameDisplay);
|
||||
const showTeammateDisplay = teammateDisplay !== username;
|
||||
|
||||
@@ -72,7 +65,6 @@ export default class UserListRow extends React.PureComponent {
|
||||
<View style={style.container}>
|
||||
<CustomListRow
|
||||
id={id}
|
||||
theme={theme}
|
||||
onPress={this.onPress}
|
||||
enabled={enabled}
|
||||
selectable={selectable}
|
||||
@@ -84,7 +76,7 @@ export default class UserListRow extends React.PureComponent {
|
||||
size={32}
|
||||
/>
|
||||
</View>
|
||||
<View style={[style.textContainer, (showTeammateDisplay ? style.showTeammateDisplay : style.hideTeammateDisplay)]}>
|
||||
<View style={style.textContainer}>
|
||||
<View>
|
||||
<Text
|
||||
style={style.username}
|
||||
@@ -105,8 +97,16 @@ export default class UserListRow extends React.PureComponent {
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
{user.delete_at > 0 &&
|
||||
<View>
|
||||
<Text
|
||||
style={style.deactivated}
|
||||
>
|
||||
{formatMessage({id: 'mobile.user_list.deactivated', defaultMessage: 'Deactivated'})}
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
<View style={style.rightFiller}/>
|
||||
</CustomListRow>
|
||||
</View>
|
||||
);
|
||||
@@ -118,23 +118,19 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginLeft: 10,
|
||||
marginHorizontal: 10,
|
||||
},
|
||||
profileContainer: {
|
||||
flexDirection: 'row',
|
||||
marginLeft: 10,
|
||||
alignItems: 'center',
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
textContainer: {
|
||||
marginLeft: 5,
|
||||
},
|
||||
showTeammateDisplay: {
|
||||
marginLeft: 10,
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
},
|
||||
hideTeammateDisplay: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
displayName: {
|
||||
fontSize: 15,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
@@ -143,8 +139,10 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
rightFiller: {
|
||||
width: 25,
|
||||
deactivated: {
|
||||
marginTop: 2,
|
||||
fontSize: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {configure, shallow} from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import UserListRow from './user_list_row';
|
||||
|
||||
configure({adapter: new Adapter()});
|
||||
|
||||
jest.mock('react-intl');
|
||||
jest.mock('app/utils/theme', () => {
|
||||
const original = require.requireActual('app/utils/theme');
|
||||
|
||||
@@ -254,8 +254,6 @@ export default class CustomSectionList extends React.PureComponent {
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginBottom: 20,
|
||||
|
||||
@@ -82,14 +82,8 @@ export default class Emoji extends React.PureComponent {
|
||||
}
|
||||
|
||||
setImageUrl = (imageUrl) => {
|
||||
let prefix = '';
|
||||
if (Platform.OS === 'android') {
|
||||
prefix = 'file://';
|
||||
}
|
||||
|
||||
const uri = `${prefix}${imageUrl}`;
|
||||
this.setState({
|
||||
imageUrl: uri,
|
||||
imageUrl,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -457,8 +457,9 @@ export default class EmojiPicker extends PureComponent {
|
||||
<SafeAreaView excludeHeader={true}>
|
||||
<KeyboardAvoidingView
|
||||
behavior='padding'
|
||||
style={{flex: 1}}
|
||||
style={styles.flex}
|
||||
keyboardVerticalOffset={keyboardOffset}
|
||||
enabled={Platform.OS === 'ios'}
|
||||
>
|
||||
<View style={styles.searchBar}>
|
||||
<SearchBar
|
||||
@@ -496,6 +497,9 @@ export default class EmojiPicker extends PureComponent {
|
||||
|
||||
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
bottomContent: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.3),
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ErrorText should match snapshot 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "red",
|
||||
"fontSize": 12,
|
||||
"marginBottom": 15,
|
||||
"marginTop": 15,
|
||||
"textAlign": "left",
|
||||
},
|
||||
Object {
|
||||
"color": "#fd5960",
|
||||
},
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"marginHorizontal": 15,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
Username must begin with a letter and contain between 3 and 22 characters including numbers, lowercase letters, and the symbols
|
||||
</Component>
|
||||
`;
|
||||
@@ -1,18 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {GlobalStyles} from 'app/styles';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
class ErrorText extends PureComponent {
|
||||
export default class ErrorText extends PureComponent {
|
||||
static propTypes = {
|
||||
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
textStyle: CustomPropTypes.Style,
|
||||
@@ -54,12 +52,3 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ErrorText);
|
||||
29
app/components/error_text/error_text.test.js
Normal file
29
app/components/error_text/error_text.test.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import ErrorText from './error_text.js';
|
||||
|
||||
describe('ErrorText', () => {
|
||||
const baseProps = {
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
marginHorizontal: 15,
|
||||
},
|
||||
theme: Preferences.THEMES.default,
|
||||
error: {
|
||||
message: 'Username must begin with a letter and contain between 3 and 22 characters including numbers, lowercase letters, and the symbols',
|
||||
},
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ErrorText {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
15
app/components/error_text/index.js
Normal file
15
app/components/error_text/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import ErrorText from './error_text.js';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ErrorText);
|
||||
@@ -86,6 +86,7 @@ export default class FileAttachmentIcon extends PureComponent {
|
||||
const styles = StyleSheet.create({
|
||||
fileIconWrapper: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#fff',
|
||||
justifyContent: 'center',
|
||||
borderTopLeftRadius: 2,
|
||||
borderBottomLeftRadius: 2,
|
||||
|
||||
@@ -107,8 +107,8 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
let width = imageWidth;
|
||||
let imageStyle = {height, width};
|
||||
if (imageSize === IMAGE_SIZE.Preview) {
|
||||
height = 100;
|
||||
width = this.calculateNeededWidth(file.height, file.width, height) || 100;
|
||||
height = 80;
|
||||
width = this.calculateNeededWidth(file.height, file.width, height) || 80;
|
||||
imageStyle = {height, width, position: 'absolute', top: 0, left: 0, borderBottomLeftRadius: 2, borderTopLeftRadius: 2};
|
||||
}
|
||||
|
||||
|
||||
@@ -101,12 +101,8 @@ export default class FileAttachmentList extends Component {
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
let path = cache.path;
|
||||
if (Platform.OS === 'android') {
|
||||
path = `file://${path}`;
|
||||
}
|
||||
|
||||
uri = path;
|
||||
const prefix = Platform.OS === 'android' ? 'file://' : '';
|
||||
uri = `${prefix}${cache.path}`;
|
||||
}
|
||||
|
||||
results.push({
|
||||
|
||||
@@ -175,6 +175,7 @@ export default class FileUploadItem extends PureComponent {
|
||||
filePreviewComponent = (
|
||||
<FileAttachmentImage
|
||||
file={file}
|
||||
imageSize='fullsize'
|
||||
imageHeight={100}
|
||||
imageWidth={100}
|
||||
wrapperHeight={100}
|
||||
|
||||
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
@@ -17,11 +18,10 @@ export default class FileUploadPreview extends PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string.isRequired,
|
||||
channelIsLoading: PropTypes.bool,
|
||||
createPostRequestStatus: PropTypes.string.isRequired,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
files: PropTypes.array.isRequired,
|
||||
filesUploadingForCurrentChannel: PropTypes.bool.isRequired,
|
||||
inputHeight: PropTypes.number.isRequired,
|
||||
fileSizeWarning: PropTypes.string,
|
||||
rootId: PropTypes.string,
|
||||
showFileMaxWarning: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
@@ -44,12 +44,17 @@ export default class FileUploadPreview extends PureComponent {
|
||||
render() {
|
||||
const {
|
||||
showFileMaxWarning,
|
||||
fileSizeWarning,
|
||||
channelIsLoading,
|
||||
filesUploadingForCurrentChannel,
|
||||
deviceHeight,
|
||||
files,
|
||||
} = this.props;
|
||||
if (channelIsLoading || (!files.length && !filesUploadingForCurrentChannel)) {
|
||||
|
||||
if (
|
||||
!fileSizeWarning && !showFileMaxWarning &&
|
||||
(channelIsLoading || (!files.length && !filesUploadingForCurrentChannel))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -70,7 +75,11 @@ export default class FileUploadPreview extends PureComponent {
|
||||
defaultMessage='Uploads limited to 5 files maximum.'
|
||||
/>
|
||||
)}
|
||||
|
||||
{Boolean(fileSizeWarning) &&
|
||||
<Text style={style.warning}>
|
||||
{fileSizeWarning}
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,6 @@ function mapStateToProps(state, ownProps) {
|
||||
|
||||
return {
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
createPostRequestStatus: state.requests.posts.createPost.status,
|
||||
deviceHeight,
|
||||
filesUploadingForCurrentChannel: checkForFileUploadingInChannel(state, ownProps.channelId, ownProps.rootId),
|
||||
theme: getTheme(state),
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {getStatusBarHeight} from 'app/selectors/device';
|
||||
|
||||
import KeyboardLayout from './keyboard_layout';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
statusBarHeight: getStatusBarHeight(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(KeyboardLayout);
|
||||
export default KeyboardLayout;
|
||||
|
||||
@@ -3,34 +3,33 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Keyboard, Platform, View} from 'react-native';
|
||||
import {
|
||||
Keyboard,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import * as CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
|
||||
export default class KeyboardLayout extends PureComponent {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
statusBarHeight: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
keyboardVerticalOffset: 0,
|
||||
style: CustomPropTypes.Style,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.subscriptions = [];
|
||||
this.count = 0;
|
||||
this.state = {
|
||||
bottom: 0,
|
||||
keyboardHeight: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
if (Platform.OS === 'ios') {
|
||||
this.subscriptions = [
|
||||
Keyboard.addListener('keyboardWillChangeFrame', this.onKeyboardChange),
|
||||
Keyboard.addListener('keyboardWillShow', this.onKeyboardWillShow),
|
||||
Keyboard.addListener('keyboardWillHide', this.onKeyboardWillHide),
|
||||
];
|
||||
}
|
||||
@@ -41,52 +40,36 @@ export default class KeyboardLayout extends PureComponent {
|
||||
}
|
||||
|
||||
onKeyboardWillHide = () => {
|
||||
this.setState({bottom: 0});
|
||||
this.setState({
|
||||
keyboardHeight: 0,
|
||||
});
|
||||
};
|
||||
|
||||
onKeyboardChange = (e) => {
|
||||
if (!e) {
|
||||
this.setState({bottom: 0});
|
||||
return;
|
||||
}
|
||||
|
||||
const {endCoordinates} = e;
|
||||
const {height} = endCoordinates;
|
||||
|
||||
this.setState({bottom: height});
|
||||
onKeyboardWillShow = (e) => {
|
||||
this.setState({
|
||||
keyboardHeight: e?.endCoordinates?.height || 0,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {children, theme, ...otherProps} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
const layoutStyle = [this.props.style, style.keyboardLayout];
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return (
|
||||
<View
|
||||
style={style.keyboardLayout}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
if (Platform.OS === 'ios') {
|
||||
// iOS doesn't resize the app automatically
|
||||
layoutStyle.push({paddingBottom: this.state.keyboardHeight});
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[style.keyboardLayout, {marginBottom: this.state.bottom}]}
|
||||
>
|
||||
{children}
|
||||
<View style={layoutStyle}>
|
||||
{this.props.children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
keyboardLayout: {
|
||||
position: 'relative',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1,
|
||||
},
|
||||
};
|
||||
const style = StyleSheet.create({
|
||||
keyboardLayout: {
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
@@ -14,37 +13,23 @@ export default class Hashtag extends React.PureComponent {
|
||||
linkStyle: CustomPropTypes.Style.isRequired,
|
||||
onHashtagPress: PropTypes.func,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
actions: PropTypes.shape({
|
||||
showSearchModal: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
handlePress = () => {
|
||||
if (this.props.onHashtagPress) {
|
||||
this.props.onHashtagPress(this.props.hashtag);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
screen: 'Search',
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
passProps: {
|
||||
initialValue: '#' + this.props.hashtag,
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
screenBackgroundColor: this.props.theme.centerChannelBg,
|
||||
},
|
||||
};
|
||||
|
||||
// Close thread view, permalink view, etc
|
||||
this.props.navigator.dismissAllModals();
|
||||
this.props.navigator.popToRoot();
|
||||
|
||||
this.props.navigator.showModal(options);
|
||||
this.props.actions.showSearchModal(this.props.navigator, '#' + this.props.hashtag);
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -11,8 +11,13 @@ describe('Hashtag', () => {
|
||||
const baseProps = {
|
||||
hashtag: 'test',
|
||||
linkStyle: {color: 'red'},
|
||||
navigator: {},
|
||||
theme: {},
|
||||
navigator: {
|
||||
dismissAllModals: jest.fn(),
|
||||
popToRoot: jest.fn(),
|
||||
},
|
||||
actions: {
|
||||
showSearchModal: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
@@ -24,11 +29,6 @@ describe('Hashtag', () => {
|
||||
test('should open hashtag search on click', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
navigator: {
|
||||
dismissAllModals: jest.fn(),
|
||||
popToRoot: jest.fn(),
|
||||
showModal: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<Hashtag {...props}/>);
|
||||
@@ -37,22 +37,12 @@ describe('Hashtag', () => {
|
||||
|
||||
expect(props.navigator.dismissAllModals).toHaveBeenCalled();
|
||||
expect(props.navigator.popToRoot).toHaveBeenCalled();
|
||||
expect(props.navigator.showModal).toHaveBeenCalledWith(expect.objectContaining({
|
||||
screen: 'Search',
|
||||
passProps: {
|
||||
initialValue: '#test',
|
||||
},
|
||||
}));
|
||||
expect(props.actions.showSearchModal).toHaveBeenCalledWith(props.navigator, '#test');
|
||||
});
|
||||
|
||||
test('should call onHashtagPress if provided', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
navigator: {
|
||||
dismissAllModals: jest.fn(),
|
||||
popToRoot: jest.fn(),
|
||||
showModal: jest.fn(),
|
||||
},
|
||||
onHashtagPress: jest.fn(),
|
||||
};
|
||||
|
||||
@@ -62,7 +52,7 @@ describe('Hashtag', () => {
|
||||
|
||||
expect(props.navigator.dismissAllModals).not.toBeCalled();
|
||||
expect(props.navigator.popToRoot).not.toBeCalled();
|
||||
expect(props.navigator.showModal).not.toBeCalled();
|
||||
expect(props.actions.showSearchModal).not.toBeCalled();
|
||||
|
||||
expect(props.onHashtagPress).toBeCalled();
|
||||
});
|
||||
19
app/components/markdown/hashtag/index.js
Normal file
19
app/components/markdown/hashtag/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {showSearchModal} from 'app/actions/views/search';
|
||||
|
||||
import Hashtag from './hashtag';
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
showSearchModal,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Hashtag);
|
||||
@@ -223,7 +223,6 @@ export default class Markdown extends PureComponent {
|
||||
linkStyle={this.props.textStyles.link}
|
||||
onHashtagPress={this.props.onHashtagPress}
|
||||
navigator={this.props.navigator}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {PropTypes} from 'prop-types';
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Clipboard,
|
||||
StyleSheet,
|
||||
@@ -21,9 +21,8 @@ import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
const MAX_LINES = 4;
|
||||
|
||||
class MarkdownCodeBlock extends React.PureComponent {
|
||||
export default class MarkdownCodeBlock extends React.PureComponent {
|
||||
static propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
language: PropTypes.string,
|
||||
@@ -36,8 +35,13 @@ class MarkdownCodeBlock extends React.PureComponent {
|
||||
language: '',
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
handlePress = preventDoubleTap(() => {
|
||||
const {intl, navigator, theme} = this.props;
|
||||
const {navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
|
||||
const languageDisplayName = getDisplayNameForLanguage(this.props.language);
|
||||
let title;
|
||||
@@ -76,7 +80,7 @@ class MarkdownCodeBlock extends React.PureComponent {
|
||||
});
|
||||
|
||||
handleLongPress = async () => {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
const config = await mattermostManaged.getLocalConfig();
|
||||
|
||||
@@ -238,5 +242,3 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(MarkdownCodeBlock);
|
||||
|
||||
@@ -187,11 +187,7 @@ export default class MarkdownImage extends React.Component {
|
||||
};
|
||||
|
||||
setImageUrl = (imageURL) => {
|
||||
let uri = imageURL;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
uri = `file://${imageURL}`;
|
||||
}
|
||||
const uri = imageURL;
|
||||
|
||||
this.setState({uri});
|
||||
this.loadImageSize(uri);
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
import {Text, TouchableOpacity, View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
@@ -15,7 +17,7 @@ import {ViewTypes} from 'app/constants';
|
||||
export default class ActionMenu extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
doPostAction: PropTypes.func.isRequired,
|
||||
selectAttachmentMenuAction: PropTypes.func.isRequired,
|
||||
setMenuActionSelector: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
@@ -23,6 +25,8 @@ export default class ActionMenu extends PureComponent {
|
||||
dataSource: PropTypes.string,
|
||||
options: PropTypes.arrayOf(PropTypes.object),
|
||||
postId: PropTypes.string.isRequired,
|
||||
selected: PropTypes.object,
|
||||
teammateNameDisplay: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
};
|
||||
@@ -39,17 +43,34 @@ export default class ActionMenu extends PureComponent {
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (props.selected && props.selected !== state.selected) {
|
||||
return {
|
||||
selectedText: props.selected.displayText,
|
||||
selected: props.selected,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
handleSelect = (selected) => {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {dataSource, actions, postId, id} = this.props;
|
||||
const {
|
||||
actions,
|
||||
dataSource,
|
||||
id,
|
||||
postId,
|
||||
teammateNameDisplay,
|
||||
} = this.props;
|
||||
|
||||
let selectedText;
|
||||
let selectedValue;
|
||||
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
|
||||
selectedText = selected.username;
|
||||
selectedText = displayUsername(selected, teammateNameDisplay);
|
||||
selectedValue = selected.id;
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
selectedText = selected.display_name;
|
||||
@@ -61,11 +82,11 @@ export default class ActionMenu extends PureComponent {
|
||||
|
||||
this.setState({selectedText});
|
||||
|
||||
actions.doPostAction(postId, id, selectedValue);
|
||||
}
|
||||
actions.selectAttachmentMenuAction(postId, id, selectedText, selectedValue);
|
||||
};
|
||||
|
||||
goToMenuActionSelector = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {navigator, theme, actions, dataSource, options, name} = this.props;
|
||||
|
||||
actions.setMenuActionSelector(dataSource, this.handleSelect, options);
|
||||
@@ -73,7 +94,7 @@ export default class ActionMenu extends PureComponent {
|
||||
navigator.push({
|
||||
backButtonTitle: '',
|
||||
screen: 'MenuActionSelector',
|
||||
title: name || intl.formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'}),
|
||||
title: name || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'}),
|
||||
animated: true,
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
@@ -117,21 +138,24 @@ export default class ActionMenu extends PureComponent {
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<View style={style.input}>
|
||||
<Text
|
||||
style={selectedStyle}
|
||||
onPress={this.goToMenuActionSelector}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
<Icon
|
||||
name='chevron-down'
|
||||
onPress={this.goToMenuActionSelector}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
style={style.icon}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={style.flex}
|
||||
onPress={this.goToMenuActionSelector}
|
||||
>
|
||||
<View style={style.input}>
|
||||
<Text
|
||||
style={selectedStyle}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
<Icon
|
||||
name='chevron-down'
|
||||
color={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
style={style.icon}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{submitted}
|
||||
</View>
|
||||
);
|
||||
@@ -140,6 +164,9 @@ export default class ActionMenu extends PureComponent {
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
|
||||
@@ -4,15 +4,19 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {doPostAction} from 'mattermost-redux/actions/posts';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {setMenuActionSelector} from 'app/actions/views/post';
|
||||
import {setMenuActionSelector, selectAttachmentMenuAction} from 'app/actions/views/post';
|
||||
|
||||
import ActionMenu from './action_menu';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const actions = state.views.post.submittedMenuActions[ownProps.postId];
|
||||
const selected = actions?.[ownProps.id];
|
||||
|
||||
return {
|
||||
selected,
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
@@ -20,7 +24,7 @@ function mapStateToProps(state) {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
doPostAction,
|
||||
selectAttachmentMenuAction,
|
||||
setMenuActionSelector,
|
||||
}, dispatch),
|
||||
};
|
||||
|
||||
@@ -114,7 +114,7 @@ export default class MessageAttachment extends PureComponent {
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={style.actionsContainer}>
|
||||
<View style={style.bodyContainer}>
|
||||
{content}
|
||||
</View>
|
||||
);
|
||||
@@ -267,13 +267,7 @@ export default class MessageAttachment extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
setImageUrl = (imageURL) => {
|
||||
let imageUri = imageURL;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
imageUri = `file://${imageURL}`;
|
||||
}
|
||||
|
||||
setImageUrl = (imageUri) => {
|
||||
Image.getSize(imageUri, (width, height) => {
|
||||
const dimensions = calculateDimensions(height, width, this.maxImageWidth);
|
||||
if (this.mounted) {
|
||||
@@ -552,10 +546,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
marginTop: 5,
|
||||
padding: 5,
|
||||
},
|
||||
actionsContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@ exports[`PostAttachmentOpenGraph should match snapshot, without image and descri
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"borderColor": "rgba(170,170,170,0.2)",
|
||||
"borderColor": "rgba(61,60,64,0.2)",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"flex": 1,
|
||||
"marginTop": 10,
|
||||
@@ -26,7 +27,7 @@ exports[`PostAttachmentOpenGraph should match snapshot, without image and descri
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(170,170,170,0.5)",
|
||||
"color": "rgba(61,60,64,0.5)",
|
||||
"fontSize": 12,
|
||||
"marginBottom": 10,
|
||||
}
|
||||
@@ -58,7 +59,7 @@ exports[`PostAttachmentOpenGraph should match snapshot, without image and descri
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": undefined,
|
||||
"color": "#2389d7",
|
||||
"fontSize": 14,
|
||||
"marginBottom": 10,
|
||||
},
|
||||
@@ -75,6 +76,112 @@ exports[`PostAttachmentOpenGraph should match snapshot, without image and descri
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`PostAttachmentOpenGraph should match snapshot, without site_name 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"borderColor": "rgba(61,60,64,0.2)",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"flex": 1,
|
||||
"marginTop": 10,
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={3}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#2389d7",
|
||||
"fontSize": 14,
|
||||
"marginBottom": 10,
|
||||
},
|
||||
Object {
|
||||
"marginRight": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
Title
|
||||
</Component>
|
||||
</TouchableOpacity>
|
||||
</Component>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`PostAttachmentOpenGraph should match snapshot, without title and url 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"borderColor": "rgba(61,60,64,0.2)",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"flex": 1,
|
||||
"marginTop": 10,
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={3}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#2389d7",
|
||||
"fontSize": 14,
|
||||
"marginBottom": 10,
|
||||
},
|
||||
Object {
|
||||
"marginRight": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
https://mattermost.com/
|
||||
</Component>
|
||||
</TouchableOpacity>
|
||||
</Component>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`PostAttachmentOpenGraph should match state and snapshot, on renderDescription 1`] = `null`;
|
||||
|
||||
exports[`PostAttachmentOpenGraph should match state and snapshot, on renderDescription 2`] = `
|
||||
@@ -90,7 +197,7 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderDescr
|
||||
numberOfLines={5}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(170,170,170,0.7)",
|
||||
"color": "rgba(61,60,64,0.7)",
|
||||
"fontSize": 13,
|
||||
"marginBottom": 10,
|
||||
}
|
||||
@@ -108,6 +215,10 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"borderColor": "rgba(61,60,64,0.2)",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"marginTop": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -116,7 +227,7 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage
|
||||
style={
|
||||
Object {
|
||||
"height": 150,
|
||||
"width": 312,
|
||||
"width": 307,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -129,7 +240,7 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage
|
||||
},
|
||||
Object {
|
||||
"height": 150,
|
||||
"width": 312,
|
||||
"width": 307,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
Image,
|
||||
Linking,
|
||||
Platform,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
@@ -19,7 +18,7 @@ import {getNearestPoint} from 'app/utils/opengraph';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const MAX_IMAGE_HEIGHT = 150;
|
||||
const VIEWPORT_IMAGE_OFFSET = 88;
|
||||
const VIEWPORT_IMAGE_OFFSET = 93;
|
||||
const VIEWPORT_IMAGE_REPLY_OFFSET = 13;
|
||||
|
||||
export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
@@ -112,14 +111,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
};
|
||||
|
||||
getImageSize = (imageUrl) => {
|
||||
let prefix = '';
|
||||
if (Platform.OS === 'android') {
|
||||
prefix = 'file://';
|
||||
}
|
||||
|
||||
const uri = `${prefix}${imageUrl}`;
|
||||
|
||||
Image.getSize(uri, (width, height) => {
|
||||
Image.getSize(imageUrl, (width, height) => {
|
||||
const dimensions = calculateDimensions(height, width, this.getViewPostWidth());
|
||||
|
||||
if (this.mounted) {
|
||||
@@ -127,7 +119,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
...dimensions,
|
||||
originalHeight: height,
|
||||
originalWidth: width,
|
||||
imageUrl: uri,
|
||||
imageUrl,
|
||||
});
|
||||
}
|
||||
}, () => null);
|
||||
@@ -216,7 +208,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
style={{width, height}}
|
||||
>
|
||||
<Image
|
||||
ref='image'
|
||||
style={[style.image, {width, height}]}
|
||||
source={source}
|
||||
resizeMode='contain'
|
||||
@@ -227,7 +218,12 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {isReplyPost, openGraphData, theme} = this.props;
|
||||
const {
|
||||
isReplyPost,
|
||||
link,
|
||||
openGraphData,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
if (!openGraphData) {
|
||||
return null;
|
||||
@@ -235,8 +231,9 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
let siteName;
|
||||
if (openGraphData.site_name) {
|
||||
siteName = (
|
||||
<View style={style.flex}>
|
||||
<Text
|
||||
style={style.siteTitle}
|
||||
@@ -246,6 +243,13 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
{openGraphData.site_name}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const title = openGraphData.title || openGraphData.url || link;
|
||||
let siteTitle;
|
||||
if (title) {
|
||||
siteTitle = (
|
||||
<View style={style.wrapper}>
|
||||
<TouchableOpacity
|
||||
style={style.flex}
|
||||
@@ -256,10 +260,17 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
numberOfLines={3}
|
||||
ellipsizeMode='tail'
|
||||
>
|
||||
{openGraphData.title || openGraphData.url}
|
||||
{title}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
{siteName}
|
||||
{siteTitle}
|
||||
{this.renderDescription()}
|
||||
{this.renderImage()}
|
||||
</View>
|
||||
@@ -273,6 +284,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flex: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderWidth: 1,
|
||||
borderRadius: 3,
|
||||
marginTop: 10,
|
||||
padding: 10,
|
||||
},
|
||||
@@ -300,6 +312,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
imageContainer: {
|
||||
alignItems: 'center',
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderWidth: 1,
|
||||
borderRadius: 3,
|
||||
marginTop: 5,
|
||||
},
|
||||
image: {
|
||||
borderRadius: 3,
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import PostAttachmentOpenGraph from './post_attachment_opengraph';
|
||||
|
||||
describe('PostAttachmentOpenGraph', () => {
|
||||
@@ -26,9 +28,7 @@ describe('PostAttachmentOpenGraph', () => {
|
||||
isReplyPost: false,
|
||||
link: 'https://mattermost.com/',
|
||||
navigator: {},
|
||||
theme: {
|
||||
centerChannelColor: '#aaa',
|
||||
},
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match snapshot, without image and description', () => {
|
||||
@@ -44,6 +44,30 @@ describe('PostAttachmentOpenGraph', () => {
|
||||
expect(wrapper.find(TouchableOpacity).exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('should match snapshot, without site_name', () => {
|
||||
const newOpenGraphData = {
|
||||
title: 'Title',
|
||||
url: 'https://mattermost.com/',
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<PostAttachmentOpenGraph
|
||||
{...baseProps}
|
||||
openGraphData={newOpenGraphData}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, without title and url', () => {
|
||||
const wrapper = shallow(
|
||||
<PostAttachmentOpenGraph
|
||||
{...baseProps}
|
||||
openGraphData={{}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match state and snapshot, on renderImage', () => {
|
||||
const wrapper = shallow(
|
||||
<PostAttachmentOpenGraph {...baseProps}/>
|
||||
|
||||
@@ -132,6 +132,7 @@ export default class PostBody extends PureComponent {
|
||||
isPostEphemeral,
|
||||
isSystemMessage,
|
||||
managedConfig,
|
||||
message,
|
||||
onCopyText,
|
||||
onPostDelete,
|
||||
onPostEdit,
|
||||
@@ -149,7 +150,7 @@ export default class PostBody extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
if (managedConfig.copyAndPasteProtection !== 'true') {
|
||||
if (managedConfig.copyAndPasteProtection !== 'true' && message) {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'mobile.post_info.copy_post', defaultMessage: 'Copy Post'}),
|
||||
onPress: onCopyText,
|
||||
|
||||
@@ -254,13 +254,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
const viewPortWidth = deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0);
|
||||
|
||||
if (link && path) {
|
||||
let prefix = '';
|
||||
if (Platform.OS === 'android') {
|
||||
prefix = 'file://';
|
||||
}
|
||||
|
||||
const uri = `${prefix}${path}`;
|
||||
Image.getSize(uri, (width, height) => {
|
||||
Image.getSize(path, (width, height) => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
@@ -282,7 +276,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
originalHeight: height,
|
||||
originalWidth: width,
|
||||
linkLoaded: true,
|
||||
uri,
|
||||
uri: path,
|
||||
});
|
||||
}, () => this.setState({linkLoadError: true}));
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ exports[`DateHeader component should match snapshot with suffix 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#aaa",
|
||||
"backgroundColor": "#3d3c40",
|
||||
"flex": 1,
|
||||
"height": 1,
|
||||
"opacity": 0.2,
|
||||
@@ -36,7 +36,7 @@ exports[`DateHeader component should match snapshot with suffix 1`] = `
|
||||
month="short"
|
||||
style={
|
||||
Object {
|
||||
"color": "#aaa",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "600",
|
||||
}
|
||||
@@ -49,7 +49,7 @@ exports[`DateHeader component should match snapshot with suffix 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#aaa",
|
||||
"backgroundColor": "#3d3c40",
|
||||
"flex": 1,
|
||||
"height": 1,
|
||||
"opacity": 0.2,
|
||||
@@ -76,7 +76,7 @@ exports[`DateHeader component should match snapshot without suffix 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#aaa",
|
||||
"backgroundColor": "#3d3c40",
|
||||
"flex": 1,
|
||||
"height": 1,
|
||||
"opacity": 0.2,
|
||||
@@ -95,7 +95,7 @@ exports[`DateHeader component should match snapshot without suffix 1`] = `
|
||||
month="short"
|
||||
style={
|
||||
Object {
|
||||
"color": "#aaa",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "600",
|
||||
}
|
||||
@@ -108,7 +108,7 @@ exports[`DateHeader component should match snapshot without suffix 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#aaa",
|
||||
"backgroundColor": "#3d3c40",
|
||||
"flex": 1,
|
||||
"height": 1,
|
||||
"opacity": 0.2,
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import DateHeader from './date_header.js';
|
||||
|
||||
describe('DateHeader', () => {
|
||||
const baseProps = {
|
||||
theme: {centerChannelBg: '#aaa', centerChannelColor: '#aaa'},
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
describe('component should match snapshot', () => {
|
||||
|
||||
@@ -79,10 +79,11 @@ export default class PostList extends PostListBase {
|
||||
} = this.props;
|
||||
|
||||
const otherProps = {};
|
||||
const footer = typeof this.props.renderFooter === 'object' ? this.props.renderFooter : this.props.renderFooter();
|
||||
if (postIds.length) {
|
||||
otherProps.ListFooterComponent = this.props.renderFooter();
|
||||
otherProps.ListFooterComponent = footer;
|
||||
} else {
|
||||
otherProps.ListEmptyComponent = this.props.renderFooter();
|
||||
otherProps.ListEmptyComponent = footer;
|
||||
}
|
||||
|
||||
const hasPostsKey = postIds.length ? 'true' : 'false';
|
||||
|
||||
@@ -31,11 +31,12 @@ export default class PostListBase extends PureComponent {
|
||||
lastViewedAt: PropTypes.number, // Used by container // eslint-disable-line no-unused-prop-types
|
||||
navigator: PropTypes.object,
|
||||
onLoadMoreUp: PropTypes.func,
|
||||
onHashtagPress: PropTypes.func,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
onPostPress: PropTypes.func,
|
||||
onRefresh: PropTypes.func,
|
||||
postIds: PropTypes.array.isRequired,
|
||||
renderFooter: PropTypes.func,
|
||||
renderFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
|
||||
renderReplies: PropTypes.bool,
|
||||
serverURL: PropTypes.string.isRequired,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
@@ -156,6 +157,7 @@ export default class PostListBase extends PureComponent {
|
||||
highlightPostId,
|
||||
isSearchResult,
|
||||
navigator,
|
||||
onHashtagPress,
|
||||
onPostPress,
|
||||
renderReplies,
|
||||
shouldRenderReplyButton,
|
||||
@@ -168,6 +170,7 @@ export default class PostListBase extends PureComponent {
|
||||
postId={postId}
|
||||
previousPostId={previousPostId}
|
||||
nextPostId={nextPostId}
|
||||
onHashtagPress={onHashtagPress}
|
||||
onPermalinkPress={this.handlePermalinkPress}
|
||||
highlight={highlight}
|
||||
renderReplies={renderReplies}
|
||||
@@ -212,7 +215,6 @@ export default class PostListBase extends PureComponent {
|
||||
passProps: {
|
||||
isPermalink: true,
|
||||
onClose: this.handleClosePermalink,
|
||||
onPermalinkPress: this.handlePermalinkPress,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {handleCommentDraftChanged, handleCommentDraftSelectionChanged} from 'app
|
||||
import {userTyping} from 'app/actions/views/typing';
|
||||
import {getCurrentChannelDraft, getThreadDraft} from 'app/selectors/views';
|
||||
import {getChannelMembersForDm} from 'app/selectors/channel';
|
||||
import {getAllowedServerMaxFileSize} from 'app/utils/file';
|
||||
|
||||
import PostTextbox from './post_textbox';
|
||||
|
||||
@@ -53,6 +54,7 @@ function mapStateToProps(state, ownProps) {
|
||||
userIsOutOfOffice,
|
||||
deactivatedChannel,
|
||||
files: currentDraft.files,
|
||||
maxFileSize: getAllowedServerMaxFileSize(config),
|
||||
maxMessageLength: (config && parseInt(config.MaxPostSize || 0, 10)) || MAX_MESSAGE_LENGTH,
|
||||
theme: getTheme(state),
|
||||
uploadFileRequestStatus: state.requests.files.uploadFiles.status,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {intlShape} from 'react-intl';
|
||||
import Button from 'react-native-button';
|
||||
import {General, RequestStatus} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import AttachmentButton from 'app/components/attachment_button';
|
||||
import Autocomplete from 'app/components/autocomplete';
|
||||
@@ -21,6 +22,9 @@ import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
import Typing from './components/typing';
|
||||
|
||||
const AUTOCOMPLETE_MARGIN = 20;
|
||||
const AUTOCOMPLETE_MAX_HEIGHT = 200;
|
||||
|
||||
let PaperPlane = null;
|
||||
|
||||
export default class PostTextbox extends PureComponent {
|
||||
@@ -48,6 +52,7 @@ export default class PostTextbox extends PureComponent {
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
deactivatedChannel: PropTypes.bool.isRequired,
|
||||
files: PropTypes.array,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
maxMessageLength: PropTypes.number.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
rootId: PropTypes.string,
|
||||
@@ -76,6 +81,8 @@ export default class PostTextbox extends PureComponent {
|
||||
contentHeight: INITIAL_HEIGHT,
|
||||
cursorPosition: 0,
|
||||
keyboardType: 'default',
|
||||
fileSizeWarning: null,
|
||||
top: 0,
|
||||
value: props.value,
|
||||
showFileMaxWarning: false,
|
||||
};
|
||||
@@ -105,10 +112,6 @@ export default class PostTextbox extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
attachAutocomplete = (c) => {
|
||||
this.autocomplete = c;
|
||||
};
|
||||
|
||||
blur = () => {
|
||||
if (this.refs.input) {
|
||||
this.refs.input.blur();
|
||||
@@ -458,6 +461,23 @@ export default class PostTextbox extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
onShowFileSizeWarning = (filename) => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const fileSizeWarning = formatMessage({
|
||||
id: 'file_upload.fileAbove',
|
||||
defaultMessage: 'File above {max}MB cannot be uploaded: {filename}',
|
||||
}, {
|
||||
max: getFormattedFileSize({size: this.props.maxFileSize}),
|
||||
filename,
|
||||
});
|
||||
|
||||
this.setState({fileSizeWarning}, () => {
|
||||
setTimeout(() => {
|
||||
this.setState({fileSizeWarning: null});
|
||||
}, 3000);
|
||||
});
|
||||
};
|
||||
|
||||
onCloseChannelPress = () => {
|
||||
const {onCloseChannel, channelTeamId} = this.props;
|
||||
this.props.actions.selectPenultimateChannel(channelTeamId);
|
||||
@@ -487,6 +507,12 @@ export default class PostTextbox extends PureComponent {
|
||||
</View>);
|
||||
};
|
||||
|
||||
handleLayout = (e) => {
|
||||
this.setState({
|
||||
top: e.nativeEvent.layout.y,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {intl} = this.context;
|
||||
const {
|
||||
@@ -496,6 +522,7 @@ export default class PostTextbox extends PureComponent {
|
||||
channelIsReadOnly,
|
||||
deactivatedChannel,
|
||||
files,
|
||||
maxFileSize,
|
||||
navigator,
|
||||
rootId,
|
||||
theme,
|
||||
@@ -514,7 +541,14 @@ export default class PostTextbox extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
const {contentHeight, cursorPosition, showFileMaxWarning, value} = this.state;
|
||||
const {
|
||||
contentHeight,
|
||||
cursorPosition,
|
||||
fileSizeWarning,
|
||||
showFileMaxWarning,
|
||||
top,
|
||||
value,
|
||||
} = this.state;
|
||||
|
||||
const textInputHeight = Math.min(contentHeight, MAX_CONTENT_HEIGHT);
|
||||
const textValue = channelIsLoading ? '' : value;
|
||||
@@ -537,8 +571,10 @@ export default class PostTextbox extends PureComponent {
|
||||
theme={theme}
|
||||
navigator={navigator}
|
||||
fileCount={files.length}
|
||||
maxFileSize={maxFileSize}
|
||||
maxFileCount={MAX_FILE_COUNT}
|
||||
onShowFileMaxWarning={this.onShowFileMaxWarning}
|
||||
onShowFileSizeWarning={this.onShowFileSizeWarning}
|
||||
uploadFiles={this.handleUploadFiles}
|
||||
/>
|
||||
);
|
||||
@@ -547,48 +583,53 @@ export default class PostTextbox extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<React.Fragment>
|
||||
<Typing/>
|
||||
<FileUploadPreview
|
||||
channelId={channelId}
|
||||
files={files}
|
||||
inputHeight={textInputHeight}
|
||||
fileSizeWarning={fileSizeWarning}
|
||||
rootId={rootId}
|
||||
showFileMaxWarning={showFileMaxWarning}
|
||||
/>
|
||||
<Autocomplete
|
||||
ref={this.attachAutocomplete}
|
||||
cursorPosition={cursorPosition}
|
||||
maxHeight={Math.min(top - AUTOCOMPLETE_MARGIN, AUTOCOMPLETE_MAX_HEIGHT)}
|
||||
onChangeText={this.handleTextChange}
|
||||
value={this.state.value}
|
||||
rootId={rootId}
|
||||
/>
|
||||
{!channelIsArchived && <View style={style.inputWrapper}>
|
||||
{!channelIsReadOnly && attachmentButton}
|
||||
<View style={[inputContainerStyle, (channelIsReadOnly && {marginLeft: 10})]}>
|
||||
<TextInput
|
||||
ref='input'
|
||||
value={textValue}
|
||||
onChangeText={this.handleTextChange}
|
||||
onSelectionChange={this.handlePostDraftSelectionChanged}
|
||||
placeholder={intl.formatMessage(placeholder)}
|
||||
placeholderTextColor={changeOpacity('#000', 0.5)}
|
||||
multiline={true}
|
||||
numberOfLines={5}
|
||||
blurOnSubmit={false}
|
||||
underlineColorAndroid='transparent'
|
||||
style={[style.input, Platform.OS === 'android' ? {height: textInputHeight} : {maxHeight: MAX_CONTENT_HEIGHT}]}
|
||||
onContentSizeChange={this.handleContentSizeChange}
|
||||
keyboardType={this.state.keyboardType}
|
||||
onEndEditing={this.handleEndEditing}
|
||||
disableFullscreenUI={true}
|
||||
editable={!channelIsReadOnly}
|
||||
/>
|
||||
{this.renderSendButton()}
|
||||
{!channelIsArchived && (
|
||||
<View
|
||||
style={style.inputWrapper}
|
||||
onLayout={this.handleLayout}
|
||||
>
|
||||
{!channelIsReadOnly && attachmentButton}
|
||||
<View style={[inputContainerStyle, (channelIsReadOnly && {marginLeft: 10})]}>
|
||||
<TextInput
|
||||
ref='input'
|
||||
value={textValue}
|
||||
onChangeText={this.handleTextChange}
|
||||
onSelectionChange={this.handlePostDraftSelectionChanged}
|
||||
placeholder={intl.formatMessage(placeholder)}
|
||||
placeholderTextColor={changeOpacity('#000', 0.5)}
|
||||
multiline={true}
|
||||
numberOfLines={5}
|
||||
blurOnSubmit={false}
|
||||
underlineColorAndroid='transparent'
|
||||
style={[style.input, Platform.OS === 'android' ? {height: textInputHeight} : {maxHeight: MAX_CONTENT_HEIGHT}]}
|
||||
onContentSizeChange={this.handleContentSizeChange}
|
||||
keyboardType={this.state.keyboardType}
|
||||
onEndEditing={this.handleEndEditing}
|
||||
disableFullscreenUI={true}
|
||||
editable={!channelIsReadOnly}
|
||||
/>
|
||||
{this.renderSendButton()}
|
||||
</View>
|
||||
</View>
|
||||
</View>}
|
||||
)}
|
||||
{channelIsArchived && this.archivedView(theme, style)}
|
||||
</View>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,26 +81,14 @@ export default class ProgressiveImage extends PureComponent {
|
||||
|
||||
setImage = (uri) => {
|
||||
if (this.subscribedToCache) {
|
||||
let path = uri;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
path = `file://${uri}`;
|
||||
}
|
||||
|
||||
this.setState({uri: path});
|
||||
this.setState({uri});
|
||||
}
|
||||
};
|
||||
|
||||
setThumbnail = (thumb) => {
|
||||
if (this.subscribedToCache) {
|
||||
const {filename, imageUri} = this.props;
|
||||
let path = thumb;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
path = `file://${thumb}`;
|
||||
}
|
||||
|
||||
this.setState({thumb: path}, () => {
|
||||
this.setState({thumb}, () => {
|
||||
setTimeout(() => {
|
||||
ImageCacheManager.cache(filename, imageUri, this.setImage);
|
||||
}, 300);
|
||||
|
||||
@@ -39,9 +39,9 @@ exports[`ShowMoreButton should match, full snapshot 1`] = `
|
||||
<LinearGradient
|
||||
colors={
|
||||
Array [
|
||||
"rgba(47,62,78,0)",
|
||||
"rgba(47,62,78,0.75)",
|
||||
"#2f3e4e",
|
||||
"rgba(255,255,255,0)",
|
||||
"rgba(255,255,255,0.75)",
|
||||
"#ffffff",
|
||||
]
|
||||
}
|
||||
end={
|
||||
@@ -89,7 +89,7 @@ exports[`ShowMoreButton should match, full snapshot 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(221,221,221,0.2)",
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"flex": 1,
|
||||
"height": 1,
|
||||
"marginRight": 10,
|
||||
@@ -101,8 +101,8 @@ exports[`ShowMoreButton should match, full snapshot 1`] = `
|
||||
onPress={[MockFunction]}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#2f3e4e",
|
||||
"borderColor": "rgba(221,221,221,0.2)",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderColor": "rgba(61,60,64,0.2)",
|
||||
"borderRadius": 4,
|
||||
"borderWidth": 1,
|
||||
"height": 37,
|
||||
@@ -122,7 +122,7 @@ exports[`ShowMoreButton should match, full snapshot 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"color": undefined,
|
||||
"color": "#2389d7",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "600",
|
||||
"marginRight": 8,
|
||||
@@ -136,7 +136,7 @@ exports[`ShowMoreButton should match, full snapshot 1`] = `
|
||||
id="post_info.message.show_more"
|
||||
style={
|
||||
Object {
|
||||
"color": undefined,
|
||||
"color": "#2389d7",
|
||||
"fontSize": 13,
|
||||
"fontWeight": "600",
|
||||
}
|
||||
@@ -147,7 +147,7 @@ exports[`ShowMoreButton should match, full snapshot 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(221,221,221,0.2)",
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"flex": 1,
|
||||
"height": 1,
|
||||
"marginLeft": 10,
|
||||
|
||||
@@ -5,6 +5,8 @@ import React from 'react';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import ShowMoreButton from './show_more_button';
|
||||
@@ -14,10 +16,7 @@ describe('ShowMoreButton', () => {
|
||||
highlight: false,
|
||||
onPress: jest.fn(),
|
||||
showMore: true,
|
||||
theme: {
|
||||
centerChannelBg: '#2f3e4e',
|
||||
centerChannelColor: '#dddddd',
|
||||
},
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match, full snapshot', () => {
|
||||
|
||||
439
app/components/sidebars/drawer_layout.js
Normal file
439
app/components/sidebars/drawer_layout.js
Normal file
@@ -0,0 +1,439 @@
|
||||
/* eslint-disable */
|
||||
// Original work: https://github.com/react-native-community/react-native-drawer-layout .
|
||||
// Modified work: Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
Keyboard,
|
||||
PanResponder,
|
||||
StyleSheet,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
I18nManager,
|
||||
} from 'react-native';
|
||||
|
||||
const MIN_SWIPE_DISTANCE = 3;
|
||||
const DEVICE_WIDTH = parseFloat(Dimensions.get('window').width);
|
||||
const THRESHOLD = DEVICE_WIDTH / 2;
|
||||
const VX_MAX = 0.1;
|
||||
|
||||
const IDLE = 'Idle';
|
||||
const DRAGGING = 'Dragging';
|
||||
const SETTLING = 'Settling';
|
||||
|
||||
export type PropType = {
|
||||
children: any,
|
||||
drawerBackgroundColor?: string,
|
||||
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
|
||||
drawerPosition: 'left' | 'right',
|
||||
drawerWidth: number,
|
||||
keyboardDismissMode?: 'none' | 'on-drag',
|
||||
onDrawerClose?: Function,
|
||||
onDrawerOpen?: Function,
|
||||
onDrawerSlide?: Function,
|
||||
onDrawerStateChanged?: Function,
|
||||
renderNavigationView: () => any,
|
||||
statusBarBackgroundColor?: string,
|
||||
useNativeAnimations?: boolean,
|
||||
};
|
||||
|
||||
export type StateType = {
|
||||
accessibilityViewIsModal: boolean,
|
||||
drawerShown: boolean,
|
||||
openValue: any,
|
||||
};
|
||||
|
||||
export type EventType = {
|
||||
stopPropagation: Function,
|
||||
};
|
||||
|
||||
export type PanResponderEventType = {
|
||||
dx: number,
|
||||
dy: number,
|
||||
moveX: number,
|
||||
moveY: number,
|
||||
vx: number,
|
||||
vy: number,
|
||||
};
|
||||
|
||||
export type DrawerMovementOptionType = {
|
||||
velocity?: number,
|
||||
};
|
||||
|
||||
export default class DrawerLayout extends Component {
|
||||
props: PropType;
|
||||
state: StateType;
|
||||
_lastOpenValue: number;
|
||||
_panResponder: any;
|
||||
_isClosing: boolean;
|
||||
_closingAnchorValue: number;
|
||||
canClose: boolean;
|
||||
|
||||
static defaultProps = {
|
||||
drawerWidth: 0,
|
||||
drawerPosition: 'left',
|
||||
useNativeAnimations: false,
|
||||
};
|
||||
|
||||
static positions = {
|
||||
Left: 'left',
|
||||
Right: 'right',
|
||||
};
|
||||
|
||||
constructor(props: PropType, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.canClose = true;
|
||||
this.state = {
|
||||
accessibilityViewIsModal: false,
|
||||
drawerShown: false,
|
||||
openValue: new Animated.Value(0),
|
||||
};
|
||||
}
|
||||
|
||||
getDrawerPosition() {
|
||||
const { drawerPosition } = this.props;
|
||||
const rtl = I18nManager.isRTL;
|
||||
return rtl
|
||||
? drawerPosition === 'left' ? 'right' : 'left' // invert it
|
||||
: drawerPosition;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { openValue } = this.state;
|
||||
|
||||
openValue.addListener(({ value }) => {
|
||||
const drawerShown = value > 0;
|
||||
const accessibilityViewIsModal = drawerShown;
|
||||
if (drawerShown !== this.state.drawerShown) {
|
||||
this.setState({ drawerShown, accessibilityViewIsModal });
|
||||
}
|
||||
|
||||
if (this.props.keyboardDismissMode === 'on-drag') {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
|
||||
this._lastOpenValue = value;
|
||||
if (this.props.onDrawerSlide) {
|
||||
this.props.onDrawerSlide({ nativeEvent: { offset: value } });
|
||||
}
|
||||
});
|
||||
|
||||
this._panResponder = PanResponder.create({
|
||||
onMoveShouldSetPanResponder: this._shouldSetPanResponder,
|
||||
onPanResponderGrant: this._panResponderGrant,
|
||||
onPanResponderMove: this._panResponderMove,
|
||||
onPanResponderTerminationRequest: () => false,
|
||||
onPanResponderRelease: this._panResponderRelease,
|
||||
onPanResponderTerminate: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { accessibilityViewIsModal, drawerShown, openValue } = this.state;
|
||||
|
||||
const {
|
||||
drawerBackgroundColor,
|
||||
drawerWidth,
|
||||
drawerPosition,
|
||||
} = this.props;
|
||||
|
||||
/**
|
||||
* We need to use the "original" drawer position here
|
||||
* as RTL turns position left and right on its own
|
||||
**/
|
||||
const dynamicDrawerStyles = {
|
||||
backgroundColor: drawerBackgroundColor,
|
||||
width: drawerWidth,
|
||||
left: drawerPosition === 'left' ? 0 : null,
|
||||
right: drawerPosition === 'right' ? 0 : null,
|
||||
};
|
||||
|
||||
/* Drawer styles */
|
||||
let outputRange;
|
||||
|
||||
if (this.getDrawerPosition() === 'left') {
|
||||
outputRange = [-drawerWidth, 0];
|
||||
} else {
|
||||
outputRange = [drawerWidth, 0];
|
||||
}
|
||||
|
||||
const drawerTranslateX = openValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange,
|
||||
extrapolate: 'clamp',
|
||||
});
|
||||
const animatedDrawerStyles = {
|
||||
transform: [{ translateX: drawerTranslateX }],
|
||||
};
|
||||
|
||||
/* Overlay styles */
|
||||
const overlayOpacity = openValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 0.7],
|
||||
extrapolate: 'clamp',
|
||||
});
|
||||
const animatedOverlayStyles = { opacity: overlayOpacity };
|
||||
const pointerEvents = drawerShown ? 'auto' : 'none';
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{ flex: 1, backgroundColor: 'transparent' }}
|
||||
{...this._panResponder.panHandlers}
|
||||
>
|
||||
<Animated.View style={styles.main}>
|
||||
{this.props.children}
|
||||
</Animated.View>
|
||||
<TouchableWithoutFeedback
|
||||
pointerEvents={pointerEvents}
|
||||
onPress={this._onOverlayClick}
|
||||
>
|
||||
<Animated.View
|
||||
pointerEvents={pointerEvents}
|
||||
style={[styles.overlay, animatedOverlayStyles]}
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
<Animated.View
|
||||
accessibilityViewIsModal={accessibilityViewIsModal}
|
||||
style={[
|
||||
styles.drawer,
|
||||
dynamicDrawerStyles,
|
||||
animatedDrawerStyles,
|
||||
]}
|
||||
>
|
||||
{this.props.renderNavigationView()}
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_onOverlayClick = (e: EventType) => {
|
||||
e.stopPropagation();
|
||||
if (!this._isLockedClosed() && !this._isLockedOpen()) {
|
||||
this.closeDrawer();
|
||||
}
|
||||
};
|
||||
|
||||
_emitStateChanged = (newState: string) => {
|
||||
if (this.props.onDrawerStateChanged) {
|
||||
this.props.onDrawerStateChanged(newState);
|
||||
}
|
||||
};
|
||||
|
||||
openDrawer = (options: DrawerMovementOptionType = {}) => {
|
||||
this._emitStateChanged(SETTLING);
|
||||
Animated.spring(this.state.openValue, {
|
||||
toValue: 1,
|
||||
bounciness: 0,
|
||||
restSpeedThreshold: 0.1,
|
||||
useNativeDriver: this.props.useNativeAnimations,
|
||||
...options,
|
||||
}).start(() => {
|
||||
if (this.props.onDrawerOpen) {
|
||||
this.props.onDrawerOpen();
|
||||
}
|
||||
this._emitStateChanged(IDLE);
|
||||
});
|
||||
};
|
||||
|
||||
closeDrawer = (options: DrawerMovementOptionType = {}) => {
|
||||
this._emitStateChanged(SETTLING);
|
||||
Animated.spring(this.state.openValue, {
|
||||
toValue: 0,
|
||||
bounciness: 0,
|
||||
restSpeedThreshold: 1,
|
||||
useNativeDriver: this.props.useNativeAnimations,
|
||||
...options,
|
||||
}).start(() => {
|
||||
if (this.props.onDrawerClose) {
|
||||
this.props.onDrawerClose();
|
||||
}
|
||||
this._emitStateChanged(IDLE);
|
||||
});
|
||||
};
|
||||
|
||||
_handleDrawerOpen = () => {
|
||||
if (this.props.onDrawerOpen) {
|
||||
this.props.onDrawerOpen();
|
||||
}
|
||||
};
|
||||
|
||||
_handleDrawerClose = () => {
|
||||
if (this.props.onDrawerClose) {
|
||||
this.props.onDrawerClose();
|
||||
}
|
||||
};
|
||||
|
||||
_shouldSetPanResponder = (
|
||||
e: EventType,
|
||||
{ moveX, dx, dy }: PanResponderEventType,
|
||||
) => {
|
||||
if (!dx || !dy || Math.abs(dx) < MIN_SWIPE_DISTANCE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._isLockedClosed() || this._isLockedOpen() || !this.canClose) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.getDrawerPosition() === 'left') {
|
||||
const overlayArea = DEVICE_WIDTH -
|
||||
(DEVICE_WIDTH - this.props.drawerWidth);
|
||||
|
||||
if (this._lastOpenValue === 1) {
|
||||
if (
|
||||
(dx < 0 && Math.abs(dx) > Math.abs(dy) * 3) ||
|
||||
moveX > overlayArea
|
||||
) {
|
||||
this._isClosing = true;
|
||||
this._closingAnchorValue = this._getOpenValueForX(moveX);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (moveX <= 35 && dx > 0) {
|
||||
this._isClosing = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const overlayArea = DEVICE_WIDTH - this.props.drawerWidth;
|
||||
|
||||
if (this._lastOpenValue === 1) {
|
||||
if (
|
||||
(dx > 0 && Math.abs(dx) > Math.abs(dy) * 3) ||
|
||||
moveX < overlayArea
|
||||
) {
|
||||
this._isClosing = true;
|
||||
this._closingAnchorValue = this._getOpenValueForX(moveX);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (moveX >= DEVICE_WIDTH - 35 && dx < 0) {
|
||||
this._isClosing = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_panResponderGrant = () => {
|
||||
this._emitStateChanged(DRAGGING);
|
||||
};
|
||||
|
||||
_panResponderMove = (e: EventType, { moveX }: PanResponderEventType) => {
|
||||
let openValue = this._getOpenValueForX(moveX);
|
||||
|
||||
if (this._isClosing) {
|
||||
openValue = 1 - (this._closingAnchorValue - openValue);
|
||||
}
|
||||
|
||||
if (openValue > 1) {
|
||||
openValue = 1;
|
||||
} else if (openValue < 0) {
|
||||
openValue = 0;
|
||||
}
|
||||
|
||||
this.state.openValue.setValue(openValue);
|
||||
};
|
||||
|
||||
_panResponderRelease = (
|
||||
e: EventType,
|
||||
{ moveX, vx }: PanResponderEventType,
|
||||
) => {
|
||||
const previouslyOpen = this._isClosing;
|
||||
const isWithinVelocityThreshold = vx < VX_MAX && vx > -VX_MAX;
|
||||
|
||||
if (this.getDrawerPosition() === 'left') {
|
||||
if (
|
||||
(vx > 0 && moveX > THRESHOLD) ||
|
||||
vx >= VX_MAX ||
|
||||
(isWithinVelocityThreshold &&
|
||||
previouslyOpen &&
|
||||
moveX > THRESHOLD)
|
||||
) {
|
||||
this.openDrawer({ velocity: vx });
|
||||
} else if (
|
||||
(vx < 0 && moveX < THRESHOLD) ||
|
||||
vx < -VX_MAX ||
|
||||
(isWithinVelocityThreshold && !previouslyOpen)
|
||||
) {
|
||||
this.closeDrawer({ velocity: vx });
|
||||
} else if (previouslyOpen) {
|
||||
this.openDrawer();
|
||||
} else {
|
||||
this.closeDrawer();
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
(vx < 0 && moveX < THRESHOLD) ||
|
||||
vx <= -VX_MAX ||
|
||||
(isWithinVelocityThreshold &&
|
||||
previouslyOpen &&
|
||||
moveX < THRESHOLD)
|
||||
) {
|
||||
this.openDrawer({ velocity: (-1) * vx });
|
||||
} else if (
|
||||
(vx > 0 && moveX > THRESHOLD) ||
|
||||
vx > VX_MAX ||
|
||||
(isWithinVelocityThreshold && !previouslyOpen)
|
||||
) {
|
||||
this.closeDrawer({ velocity: (-1) * vx });
|
||||
} else if (previouslyOpen) {
|
||||
this.openDrawer();
|
||||
} else {
|
||||
this.closeDrawer();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_isLockedClosed = () => {
|
||||
return this.props.drawerLockMode === 'locked-closed' &&
|
||||
!this.state.drawerShown;
|
||||
};
|
||||
|
||||
_isLockedOpen = () => {
|
||||
return this.props.drawerLockMode === 'locked-open' &&
|
||||
this.state.drawerShown;
|
||||
};
|
||||
|
||||
_getOpenValueForX(x: number): number {
|
||||
const { drawerWidth } = this.props;
|
||||
|
||||
if (this.getDrawerPosition() === 'left') {
|
||||
return x / drawerWidth;
|
||||
}
|
||||
|
||||
// position === 'right'
|
||||
return (DEVICE_WIDTH - x) / drawerWidth;
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
drawer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
zIndex: 1001,
|
||||
},
|
||||
main: {
|
||||
flex: 1,
|
||||
zIndex: 0,
|
||||
},
|
||||
overlay: {
|
||||
backgroundColor: '#000',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
},
|
||||
});
|
||||
@@ -44,7 +44,6 @@ exports[`ChannelItem should match snapshot 1`] = `
|
||||
membersCount={1}
|
||||
size={16}
|
||||
status="online"
|
||||
teammateDeletedAt={0}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
@@ -95,14 +94,16 @@ exports[`ChannelItem should match snapshot 1`] = `
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
>
|
||||
display_name
|
||||
</Component>
|
||||
</Component>
|
||||
</Component>
|
||||
</TouchableHighlight>
|
||||
</AnimatedComponent>
|
||||
`;
|
||||
|
||||
exports[`ChannelItem should match snapshot for deactivated user 1`] = `
|
||||
exports[`ChannelItem should match snapshot for current user i.e currentUser (you) 1`] = `
|
||||
<AnimatedComponent>
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.85}
|
||||
@@ -123,6 +124,14 @@ exports[`ChannelItem should match snapshot for deactivated user 1`] = `
|
||||
]
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#579eff",
|
||||
"width": 5,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
@@ -132,21 +141,23 @@ exports[`ChannelItem should match snapshot for deactivated user 1`] = `
|
||||
"flexDirection": "row",
|
||||
"paddingLeft": 16,
|
||||
},
|
||||
undefined,
|
||||
Object {
|
||||
"backgroundColor": "rgba(255,255,255,0.1)",
|
||||
"paddingLeft": 11,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ChannelIcon
|
||||
channelId="channel_id"
|
||||
hasDraft={false}
|
||||
isActive={false}
|
||||
isActive={true}
|
||||
isArchived={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
size={16}
|
||||
status="online"
|
||||
teammateDeletedAt={100}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
@@ -197,13 +208,352 @@ exports[`ChannelItem should match snapshot for deactivated user 1`] = `
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
>
|
||||
{displayName} (you)
|
||||
</Component>
|
||||
</Component>
|
||||
</Component>
|
||||
</TouchableHighlight>
|
||||
</AnimatedComponent>
|
||||
`;
|
||||
|
||||
exports[`ChannelItem should match snapshot for current user i.e currentUser (you) when isSearchResult 1`] = `
|
||||
<AnimatedComponent>
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.85}
|
||||
delayPressOut={100}
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
underlayColor="rgba(69,120,191,0.5)"
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#579eff",
|
||||
"width": 5,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"paddingLeft": 16,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "rgba(255,255,255,0.1)",
|
||||
"paddingLeft": 11,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ChannelIcon
|
||||
channelId="channel_id"
|
||||
hasDraft={false}
|
||||
isActive={true}
|
||||
isArchived={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
size={16}
|
||||
status="online"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
type="D"
|
||||
/>
|
||||
<Component
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.4)",
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"fontWeight": "600",
|
||||
"height": "100%",
|
||||
"lineHeight": 44,
|
||||
"paddingRight": 40,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
{displayName} (you)
|
||||
</Component>
|
||||
</Component>
|
||||
</Component>
|
||||
</TouchableHighlight>
|
||||
</AnimatedComponent>
|
||||
`;
|
||||
|
||||
exports[`ChannelItem should match snapshot for deactivated user and is currentChannel 1`] = `
|
||||
<AnimatedComponent>
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.85}
|
||||
delayPressOut={100}
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
underlayColor="rgba(69,120,191,0.5)"
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#579eff",
|
||||
"width": 5,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"paddingLeft": 16,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "rgba(255,255,255,0.1)",
|
||||
"paddingLeft": 11,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ChannelIcon
|
||||
channelId="channel_id"
|
||||
hasDraft={false}
|
||||
isActive={true}
|
||||
isArchived={true}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
size={16}
|
||||
status="online"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
type="D"
|
||||
/>
|
||||
<Component
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.4)",
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"fontWeight": "600",
|
||||
"height": "100%",
|
||||
"lineHeight": 44,
|
||||
"paddingRight": 40,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
display_name
|
||||
</Component>
|
||||
</Component>
|
||||
</Component>
|
||||
</TouchableHighlight>
|
||||
</AnimatedComponent>
|
||||
`;
|
||||
|
||||
exports[`ChannelItem should match snapshot for deactivated user and is searchResult 1`] = `
|
||||
<AnimatedComponent>
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.85}
|
||||
delayPressOut={100}
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
underlayColor="rgba(69,120,191,0.5)"
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"paddingLeft": 16,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<ChannelIcon
|
||||
channelId="channel_id"
|
||||
hasDraft={false}
|
||||
isActive={false}
|
||||
isArchived={true}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
size={16}
|
||||
status="online"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
type="D"
|
||||
/>
|
||||
<Component
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.4)",
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"fontWeight": "600",
|
||||
"height": "100%",
|
||||
"lineHeight": 44,
|
||||
"paddingRight": 40,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
display_name
|
||||
</Component>
|
||||
</Component>
|
||||
</Component>
|
||||
</TouchableHighlight>
|
||||
</AnimatedComponent>
|
||||
`;
|
||||
|
||||
exports[`ChannelItem should match snapshot for deactivated user and not searchResults or currentChannel 1`] = `null`;
|
||||
|
||||
exports[`ChannelItem should match snapshot for no displayName 1`] = `null`;
|
||||
|
||||
exports[`ChannelItem should match snapshot for showUnreadForMsgs 1`] = `null`;
|
||||
|
||||
exports[`ChannelItem should match snapshot with draft 1`] = `
|
||||
<AnimatedComponent>
|
||||
<TouchableHighlight
|
||||
@@ -248,7 +598,6 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
|
||||
membersCount={1}
|
||||
size={16}
|
||||
status="online"
|
||||
teammateDeletedAt={0}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
@@ -299,6 +648,137 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
display_name
|
||||
</Component>
|
||||
</Component>
|
||||
</Component>
|
||||
</TouchableHighlight>
|
||||
</AnimatedComponent>
|
||||
`;
|
||||
|
||||
exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
|
||||
<AnimatedComponent>
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.85}
|
||||
delayPressOut={100}
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
underlayColor="rgba(69,120,191,0.5)"
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 44,
|
||||
},
|
||||
Object {
|
||||
"opacity": 0.5,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"paddingLeft": 16,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<ChannelIcon
|
||||
channelId="channel_id"
|
||||
hasDraft={false}
|
||||
isActive={false}
|
||||
isArchived={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
size={16}
|
||||
status="online"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
type="O"
|
||||
/>
|
||||
<Component
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.4)",
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"fontWeight": "600",
|
||||
"height": "100%",
|
||||
"lineHeight": 44,
|
||||
"paddingRight": 40,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
display_name
|
||||
</Component>
|
||||
<Badge
|
||||
count={1}
|
||||
countStyle={
|
||||
Object {
|
||||
"color": "#145dbf",
|
||||
"fontSize": 10,
|
||||
}
|
||||
}
|
||||
extraPaddingHorizontal={10}
|
||||
minHeight={20}
|
||||
minWidth={20}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": "#1153ab",
|
||||
"borderRadius": 10,
|
||||
"borderWidth": 1,
|
||||
"padding": 3,
|
||||
"position": "relative",
|
||||
"right": 16,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
</Component>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
@@ -22,11 +24,11 @@ const {View: AnimatedView} = Animated;
|
||||
export default class ChannelItem extends PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string.isRequired,
|
||||
channel: PropTypes.object,
|
||||
currentChannelId: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
fake: PropTypes.bool,
|
||||
isChannelMuted: PropTypes.bool,
|
||||
isMyUser: PropTypes.bool,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
isUnread: PropTypes.bool,
|
||||
hasDraft: PropTypes.bool,
|
||||
mentions: PropTypes.number.isRequired,
|
||||
@@ -34,12 +36,9 @@ export default class ChannelItem extends PureComponent {
|
||||
onSelectChannel: PropTypes.func.isRequired,
|
||||
shouldHideChannel: PropTypes.bool,
|
||||
showUnreadForMsgs: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string,
|
||||
teammateDeletedAt: PropTypes.number,
|
||||
type: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
unreadMsgs: PropTypes.number.isRequired,
|
||||
isArchived: PropTypes.bool.isRequired,
|
||||
isSearchResult: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -51,7 +50,8 @@ export default class ChannelItem extends PureComponent {
|
||||
};
|
||||
|
||||
onPress = preventDoubleTap(() => {
|
||||
const {channelId, currentChannelId, displayName, fake, onSelectChannel, type} = this.props;
|
||||
const {channelId, currentChannelId, displayName, onSelectChannel, channel} = this.props;
|
||||
const {type, fake} = channel;
|
||||
requestAnimationFrame(() => {
|
||||
onSelectChannel({id: channelId, display_name: displayName, fake, type}, currentChannelId);
|
||||
});
|
||||
@@ -91,21 +91,21 @@ export default class ChannelItem extends PureComponent {
|
||||
currentChannelId,
|
||||
displayName,
|
||||
isChannelMuted,
|
||||
isMyUser,
|
||||
currentUserId,
|
||||
isUnread,
|
||||
hasDraft,
|
||||
mentions,
|
||||
shouldHideChannel,
|
||||
status,
|
||||
teammateDeletedAt,
|
||||
theme,
|
||||
type,
|
||||
isArchived,
|
||||
isSearchResult,
|
||||
channel,
|
||||
} = this.props;
|
||||
|
||||
const isArchived = channel.delete_at > 0;
|
||||
|
||||
// Only ever show an archived channel if it's the currently viewed channel.
|
||||
// It should disappear as soon as one navigates to another channel.
|
||||
if (isArchived && (currentChannelId !== channelId)) {
|
||||
if (isArchived && (currentChannelId !== channelId) && !isSearchResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -120,7 +120,16 @@ export default class ChannelItem extends PureComponent {
|
||||
const {intl} = this.context;
|
||||
|
||||
let channelDisplayName = displayName;
|
||||
if (isMyUser) {
|
||||
let isCurrenUser = false;
|
||||
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
if (isSearchResult) {
|
||||
isCurrenUser = channel.id === currentUserId;
|
||||
} else {
|
||||
isCurrenUser = channel.teammate_id === currentUserId;
|
||||
}
|
||||
}
|
||||
if (isCurrenUser) {
|
||||
channelDisplayName = intl.formatMessage({
|
||||
id: 'channel_header.directchannel.you',
|
||||
defaultMessage: '{displayName} (you)',
|
||||
@@ -172,10 +181,9 @@ export default class ChannelItem extends PureComponent {
|
||||
hasDraft={hasDraft && channelId !== currentChannelId}
|
||||
membersCount={displayName.split(',').length}
|
||||
size={16}
|
||||
status={status}
|
||||
teammateDeletedAt={teammateDeletedAt}
|
||||
status={channel.status}
|
||||
theme={theme}
|
||||
type={type}
|
||||
type={channel.type}
|
||||
isArchived={isArchived}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,21 +3,31 @@
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {TouchableHighlight} from 'react-native';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import ChannelItem from './channel_item.js';
|
||||
|
||||
jest.useFakeTimers();
|
||||
jest.mock('react-intl');
|
||||
|
||||
describe('ChannelItem', () => {
|
||||
const channel = {
|
||||
id: 'channel_id',
|
||||
delete_at: 0,
|
||||
type: 'O',
|
||||
fake: false,
|
||||
status: 'online',
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
channelId: 'channel_id',
|
||||
channel,
|
||||
currentChannelId: 'current_channel_id',
|
||||
displayName: 'display_name',
|
||||
fake: false,
|
||||
isChannelMuted: false,
|
||||
isMyUser: true,
|
||||
currentUserId: 'currentUser',
|
||||
isUnread: true,
|
||||
hasDraft: false,
|
||||
mentions: 0,
|
||||
@@ -25,12 +35,9 @@ describe('ChannelItem', () => {
|
||||
onSelectChannel: () => {}, // eslint-disable-line no-empty-function
|
||||
shouldHideChannel: false,
|
||||
showUnreadForMsgs: true,
|
||||
status: 'online',
|
||||
teammateDeletedAt: 0,
|
||||
type: 'O',
|
||||
theme: Preferences.THEMES.default,
|
||||
unreadMsgs: 1,
|
||||
isArchived: false,
|
||||
isSearchResult: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
@@ -42,16 +49,132 @@ describe('ChannelItem', () => {
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for deactivated user', () => {
|
||||
test('should match snapshot with mentions and muted', () => {
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
teammateDeletedAt: 100,
|
||||
type: 'D',
|
||||
mentions: 1,
|
||||
isChannelMuted: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ChannelItem {...newProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for deactivated user and not searchResults or currentChannel', () => {
|
||||
const channelObj = {
|
||||
...channel,
|
||||
type: 'D',
|
||||
delete_at: 123,
|
||||
};
|
||||
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
channel: channelObj,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ChannelItem {...newProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for deactivated user and is searchResult', () => {
|
||||
const channelObj = {
|
||||
...channel,
|
||||
type: 'D',
|
||||
delete_at: 123,
|
||||
};
|
||||
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
isSearchResult: true,
|
||||
channel: channelObj,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ChannelItem {...newProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for deactivated user and is currentChannel', () => {
|
||||
const channelObj = {
|
||||
...channel,
|
||||
type: 'D',
|
||||
delete_at: 123,
|
||||
};
|
||||
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
channel: channelObj,
|
||||
currentChannelId: 'channel_id',
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ChannelItem {...newProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for no displayName', () => {
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
displayName: '',
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ChannelItem {...newProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for current user i.e currentUser (you)', () => {
|
||||
const channelObj = {
|
||||
...channel,
|
||||
type: 'D',
|
||||
teammate_id: 'currentUser',
|
||||
};
|
||||
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
channel: channelObj,
|
||||
currentChannelId: 'channel_id',
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ChannelItem {...newProps}/>,
|
||||
{context: {intl: {formatMessage: (intlId) => intlId.defaultMessage}}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for current user i.e currentUser (you) when isSearchResult', () => {
|
||||
const channelObj = {
|
||||
...channel,
|
||||
id: 'currentUser',
|
||||
type: 'D',
|
||||
teammate_id: 'somethingElse',
|
||||
};
|
||||
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
channel: channelObj,
|
||||
currentChannelId: 'channel_id',
|
||||
isSearchResult: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ChannelItem {...newProps}/>,
|
||||
{context: {intl: {formatMessage: (intlId) => intlId.defaultMessage}}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -66,4 +189,36 @@ describe('ChannelItem', () => {
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for showUnreadForMsgs', () => {
|
||||
const wrapper = shallow(
|
||||
<ChannelItem
|
||||
{...baseProps}
|
||||
hasDraft={true}
|
||||
shouldHideChannel={true}
|
||||
unreadMsgs={0}
|
||||
/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Should call onPress', () => {
|
||||
const onSelectChannel = jest.fn();
|
||||
|
||||
const wrapper = shallow(
|
||||
<ChannelItem
|
||||
{...baseProps}
|
||||
onSelectChannel={onSelectChannel}
|
||||
/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
wrapper.find(TouchableHighlight).simulate('press');
|
||||
jest.runAllTimers();
|
||||
|
||||
const expectedChannelParams = {id: baseProps.channelId, display_name: baseProps.displayName, fake: channel.fake, type: channel.type};
|
||||
expect(onSelectChannel).toHaveBeenCalledWith(expectedChannelParams, baseProps.currentChannelId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,23 +28,13 @@ function makeMapStateToProps() {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const channelDraft = getDraftForChannel(state, channel.id);
|
||||
|
||||
let isMyUser = false;
|
||||
let teammateDeletedAt = 0;
|
||||
let displayName = channel.display_name;
|
||||
let isArchived = false;
|
||||
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
if (ownProps.isSearchResult) {
|
||||
isMyUser = channel.id === currentUserId;
|
||||
teammateDeletedAt = channel.delete_at;
|
||||
} else {
|
||||
isMyUser = channel.teammate_id === currentUserId;
|
||||
if (!ownProps.isSearchResult) {
|
||||
const teammate = getUser(state, channel.teammate_id);
|
||||
if (teammate && teammate.delete_at) {
|
||||
teammateDeletedAt = teammate.delete_at;
|
||||
}
|
||||
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
|
||||
displayName = displayUsername(teammate, teammateNameDisplay, false);
|
||||
isArchived = channel.delete_at > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,19 +65,14 @@ function makeMapStateToProps() {
|
||||
channel,
|
||||
currentChannelId,
|
||||
displayName,
|
||||
fake: channel.fake,
|
||||
isChannelMuted: isChannelMuted(member),
|
||||
isMyUser,
|
||||
hasDraft: Boolean(channelDraft.draft || channelDraft.files.length),
|
||||
currentUserId,
|
||||
hasDraft: Boolean(channelDraft.draft.trim() || channelDraft.files.length),
|
||||
mentions: member ? member.mention_count : 0,
|
||||
shouldHideChannel,
|
||||
showUnreadForMsgs,
|
||||
status: channel.status,
|
||||
teammateDeletedAt,
|
||||
theme: getTheme(state),
|
||||
type: channel.type,
|
||||
unreadMsgs,
|
||||
isArchived,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -217,6 +217,9 @@ class FilteredList extends Component {
|
||||
nickname: u.nickname,
|
||||
fullname: `${u.first_name} ${u.last_name}`,
|
||||
delete_at: u.delete_at,
|
||||
|
||||
// need name key for DM's as we use it for sortChannelsByDisplayName with same display_name
|
||||
name: displayName,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import DrawerLayout from 'react-native-drawer-layout';
|
||||
|
||||
import {General, WebsocketEvents} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import DrawerLayout from 'app/components/sidebars/drawer_layout';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
@@ -50,8 +50,6 @@ export default class ChannelSidebar extends Component {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
swiperIndex = 1;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -60,9 +58,9 @@ export default class ChannelSidebar extends Component {
|
||||
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
|
||||
}
|
||||
|
||||
this.swiperIndex = 1;
|
||||
this.state = {
|
||||
show: false,
|
||||
lockMode: 'unlocked',
|
||||
openDrawerOffset,
|
||||
drawerOpened: false,
|
||||
};
|
||||
@@ -102,7 +100,7 @@ export default class ChannelSidebar extends Component {
|
||||
|
||||
return nextProps.currentTeamId !== currentTeamId ||
|
||||
nextProps.isLandscape !== isLandscape || nextProps.deviceWidth !== deviceWidth ||
|
||||
nextProps.teamsCount !== teamsCount || this.state.lockMode !== nextState.lockMode;
|
||||
nextProps.teamsCount !== teamsCount;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -257,10 +255,13 @@ export default class ChannelSidebar extends Component {
|
||||
|
||||
onPageSelected = (index) => {
|
||||
this.swiperIndex = index;
|
||||
if (this.swiperIndex === 0) {
|
||||
this.setState({lockMode: 'locked-open'});
|
||||
} else {
|
||||
this.setState({lockMode: 'unlocked'});
|
||||
|
||||
if (this.refs.drawer) {
|
||||
if (this.swiperIndex === 0) {
|
||||
this.refs.drawer.canClose = false;
|
||||
} else {
|
||||
this.refs.drawer.canClose = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -272,10 +273,16 @@ export default class ChannelSidebar extends Component {
|
||||
if (isLandscape || isTablet) {
|
||||
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
|
||||
}
|
||||
if (this.refs.drawer) {
|
||||
this.refs.drawer.canClose = true;
|
||||
}
|
||||
this.setState({openDrawerOffset});
|
||||
};
|
||||
|
||||
onSearchStart = () => {
|
||||
if (this.refs.drawer) {
|
||||
this.refs.drawer.canClose = false;
|
||||
}
|
||||
this.setState({openDrawerOffset: 0});
|
||||
};
|
||||
|
||||
@@ -372,16 +379,16 @@ export default class ChannelSidebar extends Component {
|
||||
|
||||
render() {
|
||||
const {children, deviceWidth} = this.props;
|
||||
const {lockMode, openDrawerOffset} = this.state;
|
||||
const {openDrawerOffset} = this.state;
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
drawerLockMode={lockMode}
|
||||
ref='drawer'
|
||||
renderNavigationView={this.renderNavigationView}
|
||||
onDrawerClose={this.handleDrawerClose}
|
||||
onDrawerOpen={this.handleDrawerOpen}
|
||||
drawerWidth={deviceWidth - openDrawerOffset}
|
||||
useNativeAnimations={true}
|
||||
>
|
||||
{children}
|
||||
</DrawerLayout>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentTeamId, getMySortedTeamIds} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentTeamId, getMySortedTeamIds, getJoinableTeamIds} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {handleTeamChange} from 'app/actions/views/select_team';
|
||||
@@ -20,6 +20,7 @@ function mapStateToProps(state) {
|
||||
return {
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
currentUrl: removeProtocol(getCurrentUrl(state)),
|
||||
hasOtherJoinableTeams: getJoinableTeamIds(state).length > 0,
|
||||
teamIds: getMySortedTeamIds(state, locale),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ export default class TeamsList extends PureComponent {
|
||||
closeChannelDrawer: PropTypes.func.isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
currentUrl: PropTypes.string.isRequired,
|
||||
hasOtherJoinableTeams: PropTypes.bool,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
teamIds: PropTypes.array.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
@@ -109,22 +110,25 @@ export default class TeamsList extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {teamIds, theme} = this.props;
|
||||
const {hasOtherJoinableTeams, teamIds, theme} = this.props;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const moreAction = (
|
||||
<TouchableHighlight
|
||||
style={styles.moreActionContainer}
|
||||
onPress={this.goToSelectTeam}
|
||||
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
|
||||
>
|
||||
<Text
|
||||
style={styles.moreAction}
|
||||
let moreAction;
|
||||
if (hasOtherJoinableTeams) {
|
||||
moreAction = (
|
||||
<TouchableHighlight
|
||||
style={styles.moreActionContainer}
|
||||
onPress={this.goToSelectTeam}
|
||||
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
|
||||
>
|
||||
{'+'}
|
||||
</Text>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
<Text
|
||||
style={styles.moreAction}
|
||||
>
|
||||
{'+'}
|
||||
</Text>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
||||
@@ -106,13 +106,14 @@ export default class TeamsListItem extends React.PureComponent {
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
teamWrapper: {
|
||||
marginTop: 20,
|
||||
marginTop: 10,
|
||||
},
|
||||
teamContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
teamNameContainer: {
|
||||
flex: 1,
|
||||
|
||||
@@ -11,13 +11,13 @@ import {
|
||||
ScrollView,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import DrawerLayout from 'react-native-drawer-layout';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import DrawerLayout from 'app/components/sidebars/drawer_layout';
|
||||
import UserStatus from 'app/components/user_status';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {confirmOutOfOfficeDisabled} from 'app/utils/status';
|
||||
@@ -358,6 +358,7 @@ export default class SettingsDrawer extends PureComponent {
|
||||
onDrawerOpen={this.handleDrawerOpen}
|
||||
drawerPosition='right'
|
||||
drawerWidth={deviceWidth - DRAWER_INITIAL_OFFSET}
|
||||
useNativeAnimations={true}
|
||||
>
|
||||
{children}
|
||||
</DrawerLayout>
|
||||
|
||||
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Image,
|
||||
Platform,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
@@ -50,12 +49,7 @@ export default class TeamIcon extends React.PureComponent {
|
||||
}
|
||||
|
||||
setImageURL = (teamIcon) => {
|
||||
let prefix = '';
|
||||
if (Platform.OS === 'android') {
|
||||
prefix = 'file://';
|
||||
}
|
||||
|
||||
this.setState({teamIcon: `${prefix}${teamIcon}`});
|
||||
this.setState({teamIcon});
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UserStatus should match snapshot, away status 1`] = `
|
||||
<Component
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/status/away.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"height": 32,
|
||||
"tintColor": "#ffbc42",
|
||||
"width": 32,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`UserStatus should match snapshot, dnd status 1`] = `
|
||||
<Component
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/status/dnd.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"height": 32,
|
||||
"tintColor": "#f74343",
|
||||
"width": 32,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`UserStatus should match snapshot, online status 1`] = `
|
||||
<Component
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/status/online.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"height": 32,
|
||||
"tintColor": "#06d6a0",
|
||||
"width": 32,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`UserStatus should match snapshot, should default to offline status 1`] = `
|
||||
<Component
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/status/offline.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"height": 32,
|
||||
"tintColor": "rgba(61,60,64,0.3)",
|
||||
"width": 32,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
@@ -6,6 +6,9 @@ import {Image} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import away from 'assets/images/status/away.png';
|
||||
import dnd from 'assets/images/status/dnd.png';
|
||||
import offline from 'assets/images/status/offline.png';
|
||||
@@ -47,7 +50,7 @@ export default class UserStatus extends PureComponent {
|
||||
iconColor = theme.onlineIndicator;
|
||||
break;
|
||||
default:
|
||||
iconColor = theme.centerChannelColor;
|
||||
iconColor = changeOpacity(theme.centerChannelColor, 0.3);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
58
app/components/user_status/user_status.test.js
Normal file
58
app/components/user_status/user_status.test.js
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import UserStatus from './user_status';
|
||||
|
||||
describe('UserStatus', () => {
|
||||
const baseProps = {
|
||||
size: 32,
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match snapshot, should default to offline status', () => {
|
||||
const wrapper = shallow(
|
||||
<UserStatus {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, away status', () => {
|
||||
const wrapper = shallow(
|
||||
<UserStatus
|
||||
{...baseProps}
|
||||
status={General.AWAY}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, dnd status', () => {
|
||||
const wrapper = shallow(
|
||||
<UserStatus
|
||||
{...baseProps}
|
||||
status={General.DND}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, online status', () => {
|
||||
const wrapper = shallow(
|
||||
<UserStatus
|
||||
{...baseProps}
|
||||
status={General.ONLINE}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -8,4 +8,6 @@ const VISIBILITY_CONFIG_DEFAULTS = {
|
||||
|
||||
export default {
|
||||
VISIBILITY_CONFIG_DEFAULTS,
|
||||
};
|
||||
VISIBILITY_SCROLL_DOWN: 'down',
|
||||
VISIBILITY_SCROLL_UP: 'up',
|
||||
};
|
||||
|
||||
@@ -73,6 +73,7 @@ const ViewTypes = keyMirror({
|
||||
SET_PROFILE_IMAGE_URI: null,
|
||||
|
||||
SELECTED_ACTION_MENU: null,
|
||||
SUBMIT_ATTACHMENT_MENU_ACTION: null,
|
||||
});
|
||||
|
||||
export default {
|
||||
|
||||
@@ -6,6 +6,7 @@ import RNFetchBlob from 'rn-fetch-blob';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {ClientError} from 'mattermost-redux/client/client4';
|
||||
|
||||
import mattermostBucket from 'app/mattermost_bucket';
|
||||
import LocalConfig from 'assets/config';
|
||||
@@ -28,10 +29,10 @@ const handleRedirectProtocol = (url, response) => {
|
||||
|
||||
Client4.doFetchWithResponse = async (url, options) => {
|
||||
if (!Client4.online) {
|
||||
throw {
|
||||
throw new ClientError(Client4.getUrl(), {
|
||||
message: 'no internet connection',
|
||||
url,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
let response;
|
||||
@@ -48,26 +49,27 @@ Client4.doFetchWithResponse = async (url, options) => {
|
||||
data = await response.json();
|
||||
} catch (err) {
|
||||
if (response && response.resp && response.resp.data && response.resp.data.includes('SSL certificate')) {
|
||||
throw {
|
||||
throw new ClientError(Client4.getUrl(), {
|
||||
message: 'You need to use a valid client certificate in order to connect to this Mattermost server',
|
||||
status_code: 401,
|
||||
url,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
throw {
|
||||
throw new ClientError(Client4.getUrl(), {
|
||||
message: 'Received invalid response from the server.',
|
||||
intl: {
|
||||
id: t('mobile.request.invalid_response'),
|
||||
defaultMessage: 'Received invalid response from the server.',
|
||||
},
|
||||
};
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
if (headers[HEADER_X_CLUSTER_ID] || headers[HEADER_X_CLUSTER_ID.toLowerCase()]) {
|
||||
const clusterId = headers[HEADER_X_CLUSTER_ID] || headers[HEADER_X_CLUSTER_ID.toLowerCase()];
|
||||
if (clusterId && this.clusterId !== clusterId) {
|
||||
this.clusterId = clusterId;
|
||||
if (clusterId && Client4.clusterId !== clusterId) {
|
||||
Client4.clusterId = clusterId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,12 +97,12 @@ Client4.doFetchWithResponse = async (url, options) => {
|
||||
console.error(msg); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
throw {
|
||||
throw new ClientError(Client4.getUrl(), {
|
||||
message: msg,
|
||||
server_error_id: data.id,
|
||||
status_code: data.status_code,
|
||||
url,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const initFetchConfig = async () => {
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {combineReducers} from 'redux';
|
||||
import {UserTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
function menuAction(state = {}, action) {
|
||||
function selectedMenuAction(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.SELECTED_ACTION_MENU:
|
||||
return action.data;
|
||||
@@ -15,6 +16,33 @@ function menuAction(state = {}, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function submittedMenuActions(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.SUBMIT_ATTACHMENT_MENU_ACTION: {
|
||||
const nextState = {...state};
|
||||
if (nextState[action.postId]) {
|
||||
nextState[action.postId] = {
|
||||
...nextState[action.postId],
|
||||
...action.data,
|
||||
};
|
||||
} else {
|
||||
nextState[action.postId] = action.data;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
menuAction,
|
||||
|
||||
// Currently selected menu action
|
||||
selectedMenuAction,
|
||||
|
||||
// Submitted menu actions per post
|
||||
submittedMenuActions,
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
AppState,
|
||||
Dimensions,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
@@ -26,7 +27,6 @@ import StatusBar from 'app/components/status_bar';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import mattermostBucket from 'app/mattermost_bucket';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
import networkConnectionListener from 'app/utils/network';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
@@ -321,8 +321,6 @@ export default class Channel extends PureComponent {
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (!currentChannelId) {
|
||||
if (channelsRequestFailed) {
|
||||
const PostListRetry = require('app/components/post_list_retry').default;
|
||||
@@ -337,7 +335,7 @@ export default class Channel extends PureComponent {
|
||||
const Loading = require('app/components/channel_loader').default;
|
||||
return (
|
||||
<SafeAreaView navigator={navigator}>
|
||||
<View style={style.loading}>
|
||||
<View style={style.flex}>
|
||||
<EmptyToolbar
|
||||
theme={theme}
|
||||
isLandscape={this.props.isLandscape}
|
||||
@@ -372,7 +370,7 @@ export default class Channel extends PureComponent {
|
||||
onPress={this.goToChannelInfo}
|
||||
/>
|
||||
<KeyboardLayout>
|
||||
<View style={style.postList}>
|
||||
<View style={style.flex}>
|
||||
<ChannelPostList navigator={navigator}/>
|
||||
</View>
|
||||
<PostTextbox
|
||||
@@ -392,19 +390,13 @@ export default class Channel extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
postList: {
|
||||
flex: 1,
|
||||
},
|
||||
loading: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1,
|
||||
},
|
||||
channelLoader: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
},
|
||||
};
|
||||
const style = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
channelLoader: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user