Compare commits

...

57 Commits

Author SHA1 Message Date
Elias Nahum
2f9253b753 Bump app build number to 151 (#2276) 2018-10-18 13:21:06 -03:00
Elias Nahum
e23ad78e50 Bump app version number to 1.13.1 2018-10-18 12:42:15 -03:00
Elias Nahum
07fd3605f6 Bump app build number to 150 (#2275) 2018-10-18 12:17:17 -03:00
Elias Nahum
3868929237 Hook the onMessage event only when needed (#2271) 2018-10-18 12:13:13 -03:00
Elias Nahum
e52e482274 Fix typo in build script (#2272) 2018-10-18 10:56:27 -03:00
Elias Nahum
cd29014cc5 Update build script to run npm ci instead of npm install (#2264) 2018-10-18 10:56:16 -03:00
Elias Nahum
afbcc2a203 Bump app build number to 149 (#2262) 2018-10-12 10:24:21 -03:00
Elias Nahum
94dec5e443 Fix uploading iOS videos (#2260) 2018-10-12 17:33:18 +08:00
Elias Nahum
f6e23a9e0c Bump app build number to 148 (#2259) 2018-10-11 17:16:52 -03:00
Elias Nahum
fe1fc259ad Fix crash when loading the permalink view (#2258) 2018-10-11 16:54:16 -03:00
sudheer
e480dcfacb MM-12652 Fix for android thread crash 2018-10-12 00:38:12 +05:30
Elias Nahum
62026e375a Set explicit white background to icon previews (#2256) 2018-10-11 15:32:09 -03:00
sudheer
9c22be4465 MM-12491 Fix archive channels staying in LHS
* Add isArchived flag for all DM's and channels
 * Exclude search results from channel_list component to show
   results when isSearchResult flag is set
   This is to let user search for deactivated users from jumpto
2018-10-11 23:12:57 +05:30
Elias Nahum
b5b948e58f Properly handle max file size (#2248)
* Properly handle max file size

* Feedback review
2018-10-11 14:12:11 -03:00
Martin Kraft
5d9b8a7e06 Updates redux ref. (#2254) 2018-10-11 12:50:03 -04:00
Elias Nahum
2b495f4c51 Change PR build filename to PR-{PR_ID} (#2250) 2018-10-10 21:19:27 -03:00
Elias Nahum
49a284f46d Bump app build number to 147 (#2249) 2018-10-10 16:57:54 -03:00
Sudheer
61a33b3025 MM-12560 Fix for infinite loading indicator for thread from search (#2246)
* MM-12560 Fix for infinite loading indicator for thread from search

* Change tests
2018-10-11 00:00:08 +05:30
Elias Nahum
01d7d08b25 Remove prepare-pr so it does not checkout the branch (#2245) 2018-10-10 09:38:02 -03:00
Elias Nahum
4bd364df42 translations PR 20181009 (#2239) 2018-10-10 08:43:22 -03:00
Martin Kraft
ef808262d7 MM-12552: Adds parameter to include archived channel search results. 2018-10-09 16:12:10 -04:00
Elias Nahum
95f8e72c11 Update Fastlane scripts (#2231)
* Upload builds to store after a successful build

* Update script to build PRs
2018-10-09 12:06:01 -03:00
Elias Nahum
3ea3fe7e2f Fix update keywords field on Android (#2229) 2018-10-09 11:57:16 -03:00
Elias Nahum
2e5f7fd60c Fix KeyboardAvoidView for android (#2226) 2018-10-09 11:55:11 -03:00
Harrison Healey
cb98efc6da MM-11477 Use custom thunk middleware to intercept leaked network errors (#2222)
* MM-11477 Use custom thunk middleware to intercept leaked network errors

* MM-11477 Always include url in Client4 errors

* Update redux
2018-10-09 10:21:45 -04:00
Saturnino Abril
09a49302f2 Add deactivated to separate row on user list and fix truncating user info (#2232) 2018-10-09 20:51:39 +08:00
Saturnino Abril
68fe3653a8 add opacity of 0.3 to circle stroke of offline status (#2233) 2018-10-09 20:48:41 +08:00
Elias Nahum
2cf74c07cb Fix gifs displaying as still images on Android (#2230) 2018-10-08 12:28:17 -03:00
Elias Nahum
a70688fe44 Missing translations on login screen (#2227) 2018-10-08 12:26:54 -03:00
Saturnino Abril
b8f4757300 [MM-12550] Fix hangup on permalink view when user deleted the post in it (#2228)
* fix hangup on permalink view when user deleted the post in it

* return null when no need to change state on getDerivedStateFromProps
2018-10-08 23:16:53 +08:00
Elias Nahum
b3ec19fe17 Fix Option to capture and share video (#2225) 2018-10-05 17:47:55 -03:00
Elias Nahum
faa3a55b3d Reset isLoadingMoreTop flag when switching channels (#2224) 2018-10-05 17:46:24 -03:00
Harrison Healey
dff9628b6e MM-12189 Set autocomplete max height based off available space (#2218)
* MM-12189 Set autocomplete max height based off available space

* Have each autocomplete type determine its own max height since DateSuggestion needs more space

* Remove unused props and ref
2018-10-05 09:58:53 -04:00
Saturnino Abril
82d9995f2b fix issue that blocks adding test for thread screen (#2221) 2018-10-05 21:50:03 +08:00
Elias Nahum
112fd06dfd Revert "image thumbnail not centered (#2210)" and fix it properly (#2219)
This reverts commit 5f31374e74.
2018-10-04 19:42:17 -03:00
Elias Nahum
92541025ef Fix fastlane android script (#2217) 2018-10-03 18:00:21 -03:00
Elias Nahum
d927642cb9 Bump app build number to 146 (#2216) 2018-10-03 16:29:21 -03:00
Elias Nahum
696b2e7c5e Fix search screen from braking and improved how to get more results (#2215)
* Fix search screen from braking and improved how to get more results

* Update mattermost-redux
2018-10-03 16:16:20 -03:00
Harrison Healey
bfac3546c9 MM-12424 Properly set softInputMode on Android activity (#2201) 2018-10-03 16:02:29 -03:00
Elias Nahum
e170bc1177 Fastlane update to include new build process (#2208) 2018-10-03 15:10:24 -03:00
Joram Wilander
b41777a7e0 MM-12348 Persist interactive menu choices past channel switch and share with thread view (#2207)
* Persist interactive menu choices past channel switch and share with thread view

* Remove unneeded async
2018-10-03 15:08:35 -03:00
Sudheer
783dc66b2a MM-12301 Add padding for error text on edit profile screen (#2214) 2018-10-03 15:07:12 -03:00
Elias Nahum
1c093f70a4 image thumbnail not centered (#2210)
* image thumbnail not centered

* feedback review
2018-10-03 15:05:22 -03:00
Martin Kraft
63bcdc42fb MM-12061: Fixes component name typo and adds missing param. (#2199) 2018-10-03 15:05:07 -03:00
Saturnino Abril
793aea617c fix empty space on link preview when title and URL is not present in open graph meta (#2197) 2018-10-03 21:48:05 +08:00
Elias Nahum
ed3e822b63 Draft channel indicator to ignore message with whitespaces only (#2209) 2018-10-03 09:51:20 -03:00
Elias Nahum
7bbe6ccd9f translations PR 20181001 (#2204) 2018-10-02 18:38:47 -03:00
Harrison Healey
bc159153d6 MM-12424 Remove unused props from KeyboardLayout and simplify methods (#2203)
* MM-12424 Remove unused props from KeyboardLayout and simplify methods

* Update snapshots
2018-10-02 17:13:04 -04:00
sudheer
167b91d7fd MM-12347 Fix crash of app when accessing jumpTo 2018-10-02 22:51:50 +05:30
Saturnino Abril
87b3e3e724 fix empty reaction row when profile is missing (#2196) 2018-10-01 18:41:14 +08:00
Saturnino Abril
cb1e286e4f Update unit test: use default theme, snapshot on wrapped elements and removal of unnecessary adapter (#2171)
* use default theme when unit testing the component

* update snapshots by getting wrapped elements only

* fix merge conflicts
2018-10-01 15:12:10 +08:00
Saturnino Abril
fb2f711359 fix blank space on link preview when no site name (#2193) 2018-10-01 15:07:49 +08:00
Saturnino Abril
07ff66726c add border to image of link preview and not allow such image width to exceed the view port width (#2189) 2018-10-01 09:39:49 +08:00
Elias Nahum
2bb69a9b7e Bump App build number to 145 (#2195) 2018-09-28 17:33:17 -03:00
Elias Nahum
a25423eb4b Fastlane (#2190)
* Have fastlane use BRANCH_TO_BUILD

* Update fastlane
2018-09-28 14:49:18 -03:00
Christopher Speller
dfb0557de6 Updating redux 2018-09-28 10:16:09 -07:00
Elias Nahum
093484eab3 Do not show a loading spinner if thread has root post loaded (#2186)
* Do not show a loading spinner if thread has root post loaded

* Feedback review

* Remove additional condition
2018-09-28 12:12:45 -03:00
132 changed files with 3805 additions and 3295 deletions

View File

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

View File

@@ -113,7 +113,7 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 144
versionCode 151
versionName "1.13.0"
multiDexEnabled = true
ndk {
@@ -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

View File

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

View File

@@ -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,15 @@ export function setMenuActionSelector(dataSource, onSelect, options) {
},
};
}
export function selectAttachmentMenuAction(postId, actionId, dataSource, displayText, value) {
return (dispatch) => {
dispatch({
type: ViewTypes.SUBMIT_ATTACHMENT_MENU_ACTION,
postId,
data: {displayText, value},
});
dispatch(doPostAction(postId, actionId, value));
};
}

View File

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

View File

@@ -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: 1,
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: 1,
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: {

View File

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

View File

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

View File

@@ -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,
@@ -194,7 +194,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 +209,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 +225,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
listView: {
backgroundColor: theme.centerChannelBg,
},
search: {
minHeight: 125,
},
};
});

View File

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

View File

@@ -29,6 +29,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,
@@ -171,18 +172,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}

View File

@@ -25,6 +25,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,
@@ -133,18 +134,20 @@ 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}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,6 @@ export default class OptionListRow extends React.PureComponent {
return (
<CustomListRow
id={value}
theme={theme}
onPress={this.onPress}
enabled={enabled}
selectable={selectable}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -175,6 +175,7 @@ export default class FileUploadItem extends PureComponent {
filePreviewComponent = (
<FileAttachmentImage
file={file}
imageSize='fullsize'
imageHeight={100}
imageWidth={100}
wrapperHeight={100}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,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 +23,7 @@ export default class ActionMenu extends PureComponent {
dataSource: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.object),
postId: PropTypes.string.isRequired,
selected: PropTypes.object,
theme: PropTypes.object.isRequired,
navigator: PropTypes.object,
};
@@ -39,6 +40,17 @@ 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;
@@ -61,7 +73,7 @@ export default class ActionMenu extends PureComponent {
this.setState({selectedText});
actions.doPostAction(postId, id, selectedValue);
actions.selectAttachmentMenuAction(postId, id, dataSource, selectedText, selectedValue);
}
goToMenuActionSelector = preventDoubleTap(() => {

View File

@@ -4,23 +4,23 @@
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 {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) {
return {
theme: getTheme(state),
selected: state.views.post.submittedMenuActions[ownProps.postId],
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
doPostAction,
selectAttachmentMenuAction,
setMenuActionSelector,
}, dispatch),
};

View File

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

View File

@@ -19,7 +19,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 {
@@ -216,7 +216,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
style={{width, height}}
>
<Image
ref='image'
style={[style.image, {width, height}]}
source={source}
resizeMode='contain'
@@ -227,7 +226,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 +239,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 +251,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 +268,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 +292,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flex: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderWidth: 1,
borderRadius: 3,
marginTop: 10,
padding: 10,
},
@@ -300,6 +320,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
imageContainer: {
alignItems: 'center',
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderWidth: 1,
borderRadius: 3,
marginTop: 5,
},
image: {
borderRadius: 3,

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ export default class PostListBase extends PureComponent {
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,

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,6 @@ exports[`ChannelItem should match snapshot 1`] = `
membersCount={1}
size={16}
status="online"
teammateDeletedAt={0}
theme={
Object {
"awayIndicator": "#ffbc42",
@@ -102,7 +101,9 @@ exports[`ChannelItem should match snapshot 1`] = `
</AnimatedComponent>
`;
exports[`ChannelItem should match snapshot for deactivated user 1`] = `
exports[`ChannelItem should match snapshot for deactivated user 1`] = `null`;
exports[`ChannelItem should match snapshot for deactivated user and is searchResult 1`] = `
<AnimatedComponent>
<TouchableHighlight
activeOpacity={0.85}
@@ -140,13 +141,12 @@ exports[`ChannelItem should match snapshot for deactivated user 1`] = `
channelId="channel_id"
hasDraft={false}
isActive={false}
isArchived={false}
isArchived={true}
isInfo={false}
isUnread={true}
membersCount={1}
size={16}
status="online"
teammateDeletedAt={100}
theme={
Object {
"awayIndicator": "#ffbc42",
@@ -204,6 +204,120 @@ exports[`ChannelItem should match snapshot for deactivated user 1`] = `
</AnimatedComponent>
`;
exports[`ChannelItem should match snapshot if channel is archived 1`] = `null`;
exports[`ChannelItem should match snapshot if channel is archived 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="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",
},
]
}
/>
</Component>
</Component>
</TouchableHighlight>
</AnimatedComponent>
`;
exports[`ChannelItem should match snapshot with draft 1`] = `
<AnimatedComponent>
<TouchableHighlight
@@ -248,7 +362,6 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
membersCount={1}
size={16}
status="online"
teammateDeletedAt={0}
theme={
Object {
"awayIndicator": "#ffbc42",

View File

@@ -35,11 +35,11 @@ export default class ChannelItem extends PureComponent {
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 = {
@@ -97,15 +97,15 @@ export default class ChannelItem extends PureComponent {
mentions,
shouldHideChannel,
status,
teammateDeletedAt,
theme,
type,
isArchived,
isSearchResult,
} = this.props;
// 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;
}
@@ -173,7 +173,6 @@ export default class ChannelItem extends PureComponent {
membersCount={displayName.split(',').length}
size={16}
status={status}
teammateDeletedAt={teammateDeletedAt}
theme={theme}
type={type}
isArchived={isArchived}

View File

@@ -26,7 +26,6 @@ describe('ChannelItem', () => {
shouldHideChannel: false,
showUnreadForMsgs: true,
status: 'online',
teammateDeletedAt: 0,
type: 'O',
theme: Preferences.THEMES.default,
unreadMsgs: 1,
@@ -45,8 +44,22 @@ describe('ChannelItem', () => {
test('should match snapshot for deactivated user', () => {
const newProps = {
...baseProps,
teammateDeletedAt: 100,
type: 'D',
isArchived: true,
};
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 newProps = {
...baseProps,
type: 'D',
isArchived: true,
isSearchResult: true,
};
const wrapper = shallow(
<ChannelItem {...newProps}/>,
@@ -66,4 +79,29 @@ describe('ChannelItem', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot if channel is archived', () => {
const wrapper = shallow(
<ChannelItem
{...baseProps}
isArchived={true}
/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot if channel is archived and is currentChannel', () => {
const wrapper = shallow(
<ChannelItem
{...baseProps}
isArchived={true}
currentChannelId={'channel_id'}
/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View File

@@ -29,22 +29,16 @@ function makeMapStateToProps() {
const channelDraft = getDraftForChannel(state, channel.id);
let isMyUser = false;
let teammateDeletedAt = 0;
let displayName = channel.display_name;
let isArchived = false;
const isArchived = channel.delete_at > 0;
if (channel.type === General.DM_CHANNEL) {
if (ownProps.isSearchResult) {
isMyUser = channel.id === currentUserId;
teammateDeletedAt = channel.delete_at;
} else {
isMyUser = channel.teammate_id === currentUserId;
isMyUser = channel.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;
}
}
@@ -78,12 +72,11 @@ function makeMapStateToProps() {
fake: channel.fake,
isChannelMuted: isChannelMuted(member),
isMyUser,
hasDraft: Boolean(channelDraft.draft || channelDraft.files.length),
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,

View File

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

View File

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

View File

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

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

View File

@@ -8,4 +8,6 @@ const VISIBILITY_CONFIG_DEFAULTS = {
export default {
VISIBILITY_CONFIG_DEFAULTS,
};
VISIBILITY_SCROLL_DOWN: 'down',
VISIBILITY_SCROLL_UP: 'up',
};

View File

@@ -73,6 +73,7 @@ const ViewTypes = keyMirror({
SET_PROFILE_IMAGE_URI: null,
SELECTED_ACTION_MENU: null,
SUBMIT_ATTACHMENT_MENU_ACTION: null,
});
export default {

View File

@@ -61,6 +61,7 @@ Client4.doFetchWithResponse = async (url, options) => {
id: t('mobile.request.invalid_response'),
defaultMessage: 'Received invalid response from the server.',
},
url,
};
}

View File

@@ -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,25 @@ function menuAction(state = {}, action) {
}
}
function submittedMenuActions(state = {}, action) {
switch (action.type) {
case ViewTypes.SUBMIT_ATTACHMENT_MENU_ACTION: {
const nextState = {...state};
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,
});

View File

@@ -395,6 +395,7 @@ export default class Channel extends PureComponent {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
postList: {
backgroundColor: theme.centerChannelBg,
flex: 1,
},
loading: {

View File

@@ -62,6 +62,10 @@ export default class ChannelPostList extends PureComponent {
visiblePostIds = this.getVisiblePostIds(nextProps);
}
if (this.props.channelId !== nextProps.channelId) {
this.isLoadingMoreTop = false;
}
this.setState({visiblePostIds});
}

View File

@@ -474,6 +474,7 @@ export default class ChannelInfo extends PureComponent {
status={status}
theme={theme}
type={currentChannel.type}
isArchived={currentChannel.delete_at !== 0}
/>
}
<View style={style.rowsContainer}>

View File

@@ -8,7 +8,7 @@ import {
View,
} from 'react-native';
import ChanneIcon from 'app/components/channel_icon';
import ChannelIcon from 'app/components/channel_icon';
import FormattedDate from 'app/components/formatted_date';
import FormattedText from 'app/components/formatted_text';
import Markdown from 'app/components/markdown';
@@ -28,6 +28,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
status: PropTypes.string,
theme: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
isArchived: PropTypes.bool.isRequired,
};
render() {
@@ -43,6 +44,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
status,
theme,
type,
isArchived,
} = this.props;
const style = getStyleSheet(theme);
@@ -52,13 +54,14 @@ export default class ChannelInfoHeader extends React.PureComponent {
return (
<View style={style.container}>
<View style={style.channelNameContainer}>
<ChanneIcon
<ChannelIcon
isInfo={true}
membersCount={memberCount - 1}
size={16}
status={status}
theme={theme}
type={type}
isArchived={isArchived}
/>
<Text
ellipsizeMode='tail'

View File

@@ -550,6 +550,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
errorText: {
fontSize: 14,
marginHorizontal: 15,
},
separator: {
height: 15,
@@ -560,4 +561,3 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
};
});

View File

@@ -4,7 +4,7 @@ exports[`ErrorTeamsList should match snapshot 1`] = `
<Component
style={
Object {
"backgroundColor": undefined,
"backgroundColor": "#ffffff",
"flex": 1,
}
}
@@ -24,7 +24,34 @@ exports[`ErrorTeamsList should match snapshot 1`] = `
}
}
onRetry={[Function]}
theme={Object {}}
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>
`;

View File

@@ -2,13 +2,13 @@
// 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 FailedNetworkAction from 'app/components/failed_network_action';
import ErrorTeamsList from './error_teams_list';
configure({adapter: new Adapter()});
describe('ErrorTeamsList', () => {
const navigator = {
setOnNavigatorEvent: () => {}, // eslint-disable-line no-empty-function
@@ -27,7 +27,7 @@ describe('ErrorTeamsList', () => {
logout: () => {}, // eslint-disable-line no-empty-function
selectDefaultTeam: () => {}, // eslint-disable-line no-empty-function
},
theme: {},
theme: Preferences.THEMES.default,
navigator,
};

View File

@@ -143,6 +143,14 @@ export default class Login extends PureComponent {
Keyboard.dismiss();
InteractionManager.runAfterInteractions(async () => {
if (!this.props.loginId) {
t('login.noEmail');
t('login.noEmailLdapUsername');
t('login.noEmailUsername');
t('login.noEmailUsernameLdapUsername');
t('login.noLdapUsername');
t('login.noUsername');
t('login.noUsernameLdapUsername');
// it's slightly weird to be constructing the message ID, but it's a bit nicer than triply nested if statements
let msgId = 'login.no';
if (this.props.config.EnableSignInWithEmail === 'true') {

View File

@@ -16,7 +16,7 @@ import {ViewTypes} from 'app/constants';
import MenuActionSelector from './menu_action_selector';
function mapStateToProps(state) {
const menuAction = state.views.post.menuAction || {};
const menuAction = state.views.post.selectedMenuAction || {};
let data;
let loadMoreRequestStatus;

View File

@@ -4,6 +4,8 @@ import React from 'react';
import {shallow} from 'enzyme';
import {IntlProvider} from 'react-intl';
import Preferences from 'mattermost-redux/constants/preferences';
import MenuActionSelector from './menu_action_selector.js';
jest.mock('rn-fetch-blob', () => ({
@@ -76,7 +78,7 @@ describe('MenuActionSelector', () => {
onSelect: jest.fn(),
data: [{text: 'text', value: 'value'}],
dataSource: null,
theme: {},
theme: Preferences.THEMES.default,
};
test('should match snapshot for explicit options', async () => {
@@ -84,7 +86,7 @@ describe('MenuActionSelector', () => {
<MenuActionSelector {...baseProps}/>,
{context: {intl}},
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for users', async () => {
@@ -98,10 +100,10 @@ describe('MenuActionSelector', () => {
<MenuActionSelector {...props}/>,
{context: {intl}},
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.getElement()).toMatchSnapshot();
wrapper.setState({isLoading: false});
wrapper.update();
expect(wrapper).toMatchSnapshot();
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for channels', async () => {
@@ -115,10 +117,10 @@ describe('MenuActionSelector', () => {
<MenuActionSelector {...props}/>,
{context: {intl}},
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.getElement()).toMatchSnapshot();
wrapper.setState({isLoading: false});
wrapper.update();
expect(wrapper).toMatchSnapshot();
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for searching', async () => {
@@ -134,6 +136,6 @@ describe('MenuActionSelector', () => {
);
wrapper.setState({isLoading: false, searching: true, term: 'name2'});
wrapper.update();
expect(wrapper).toMatchSnapshot();
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View File

@@ -9,6 +9,7 @@ import {
Keyboard,
KeyboardAvoidingView,
Platform,
StyleSheet,
TouchableWithoutFeedback,
View,
} from 'react-native';
@@ -130,8 +131,9 @@ export default class Mfa extends PureComponent {
return (
<KeyboardAvoidingView
behavior='padding'
style={{flex: 1}}
style={style.flex}
keyboardVerticalOffset={5}
enabled={Platform.OS === 'ios'}
>
<StatusBar/>
<TouchableWithoutFeedback onPress={this.blur}>
@@ -168,3 +170,9 @@ export default class Mfa extends PureComponent {
);
}
}
const style = StyleSheet.create({
flex: {
flex: 1,
},
});

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MoreChannels should match snapshot 1`] = `
<Connect(KeyboardLayout)>
<KeyboardLayout>
<Connect(StatusBar) />
<React.Fragment>
<Component
@@ -18,8 +18,8 @@ exports[`MoreChannels should match snapshot 1`] = `
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(170,170,170,0.2)",
"color": "#aaa",
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
}
}
@@ -29,10 +29,10 @@ exports[`MoreChannels should match snapshot 1`] = `
onFocus={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholderTextColor="rgba(170,170,170,0.5)"
tintColorDelete="rgba(170,170,170,0.5)"
tintColorSearch="rgba(170,170,170,0.5)"
titleCancelColor="#aaa"
placeholderTextColor="rgba(61,60,64,0.5)"
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
</Component>
@@ -67,13 +67,33 @@ exports[`MoreChannels should match snapshot 1`] = `
showSections={false}
theme={
Object {
"centerChannelBg": "#aaa",
"centerChannelColor": "#aaa",
"sidebarHeaderBg": "#aaa",
"sidebarHeaderTextColor": "#aaa",
"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",
}
}
/>
</React.Fragment>
</Connect(KeyboardLayout)>
</KeyboardLayout>
`;

View File

@@ -7,6 +7,7 @@ import {intlShape} from 'react-intl';
import {
Platform,
InteractionManager,
StyleSheet,
View,
} from 'react-native';
@@ -20,7 +21,7 @@ import Loading from 'app/components/loading';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import {alertErrorWithFallback} from 'app/utils/general';
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
import {changeOpacity, setNavigatorStyles} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
export default class MoreChannels extends PureComponent {
@@ -300,7 +301,6 @@ export default class MoreChannels extends PureComponent {
const {adding, channels, searching, term} = this.state;
const {formatMessage} = intl;
const isLoading = requestStatus.status === RequestStatus.STARTED || requestStatus.status === RequestStatus.NOT_STARTED;
const style = getStyleFromTheme(theme);
const more = searching ? () => true : this.loadMoreChannels;
let content;
@@ -320,7 +320,7 @@ export default class MoreChannels extends PureComponent {
content = (
<React.Fragment>
<View style={style.wrapper}>
<View style={style.searchbar}>
<SearchBar
ref='search_bar'
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
@@ -365,25 +365,8 @@ export default class MoreChannels extends PureComponent {
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
wrapper: {
marginVertical: 5,
},
container: {
flex: 1,
backgroundColor: theme.centerChannelBg,
},
navTitle: {
...Platform.select({
android: {
fontSize: 18,
},
ios: {
fontSize: 15,
fontWeight: 'bold',
},
}),
},
};
const style = StyleSheet.create({
searchbar: {
marginVertical: 5,
},
});

View File

@@ -4,6 +4,8 @@
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import MoreChannels from './more_channels.js';
jest.mock('react-intl');
@@ -29,12 +31,7 @@ describe('MoreChannels', () => {
currentUserId: 'current_user_id',
currentTeamId: 'current_team_id',
navigator,
theme: {
centerChannelBg: '#aaa',
centerChannelColor: '#aaa',
sidebarHeaderBg: '#aaa',
sidebarHeaderTextColor: '#aaa',
},
theme: Preferences.THEMES.default,
canCreateChannels: true,
channels: [{id: 'id', name: 'name', display_name: 'display_name'}],
closeButton: {},

View File

@@ -0,0 +1,152 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Permalink should match snapshot 1`] = `
<Connect(SafeAreaIos)
backgroundColor="transparent"
excludeHeader={true}
footerColor="transparent"
forceTop={44}
>
<Component
style={
Object {
"flex": 1,
"marginTop": 20,
}
}
>
<withAnimatable(Component)
animation="zoomIn"
delay={0}
direction="normal"
duration={200}
iterationCount={1}
iterationDelay={0}
onAnimationBegin={[Function]}
onAnimationEnd={[Function]}
onTransitionBegin={[Function]}
onTransitionEnd={[Function]}
style={
Object {
"borderRadius": 6,
"flex": 1,
"margin": 10,
"opacity": 0,
}
}
useNativeDriver={true}
>
<Component
style={
Object {
"alignItems": "center",
"backgroundColor": "#ffffff",
"borderTopLeftRadius": 6,
"borderTopRightRadius": 6,
"flexDirection": "row",
"height": 44,
"paddingRight": 16,
"width": "100%",
}
}
>
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"height": 44,
"justifyContent": "center",
"paddingLeft": 7,
"width": 40,
}
}
>
<Icon
allowFontScaling={false}
color="#3d3c40"
name="close"
size={20}
/>
</TouchableOpacity>
<Component
style={
Object {
"alignItems": "center",
"flex": 1,
"paddingRight": 40,
}
}
>
<Component
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "#3d3c40",
"fontSize": 17,
"fontWeight": "600",
}
}
>
channel_name
</Component>
</Component>
</Component>
<Component
style={
Object {
"backgroundColor": "#ffffff",
}
}
>
<Component
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"height": 1,
}
}
/>
</Component>
<Component
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
},
null,
]
}
>
<Loading
color="grey"
size="large"
style={Object {}}
/>
</Component>
</withAnimatable(Component)>
</Component>
</Connect(SafeAreaIos)>
`;
exports[`Permalink should match snapshot 2`] = `
<Component>
<Icon
allowFontScaling={false}
name="archive"
size={12}
style={
Array [
Object {
"color": "#3d3c40",
"fontSize": 16,
"paddingRight": 20,
},
]
}
/>
</Component>
`;

View File

@@ -86,10 +86,49 @@ export default class Permalink extends PureComponent {
intl: intlShape.isRequired,
};
static getDerivedStateFromProps(nextProps, prevState) {
const newState = {};
if (nextProps.focusedPostId !== prevState.focusedPostIdState) {
newState.focusedPostIdState = nextProps.focusedPostId;
}
if (nextProps.channelId && nextProps.channelId !== prevState.channelIdState) {
newState.channelIdState = nextProps.channelId;
}
if (nextProps.channelName && nextProps.channelName !== prevState.channelNameState) {
newState.channelNameState = nextProps.channelName;
}
if (nextProps.postIds && nextProps.postIds.length > 0 && nextProps.postIds !== prevState.postIdsState) {
newState.postIdsState = nextProps.postIds;
}
if (nextProps.focusedPostId !== prevState.focusedPostIdState) {
let loading = true;
if (nextProps.postIds && nextProps.postIds.length >= 10) {
loading = false;
}
newState.loading = loading;
}
if (Object.keys(newState).length === 0) {
return null;
}
return newState;
}
constructor(props) {
super(props);
const {postIds, channelName} = props;
const {
postIds,
channelId,
channelName,
focusedPostId,
} = props;
let loading = true;
if (postIds && postIds.length >= 10) {
@@ -103,10 +142,14 @@ export default class Permalink extends PureComponent {
loading,
error: '',
retry: false,
channelIdState: channelId,
channelNameState: channelName,
focusedPostIdState: focusedPostId,
postIdsState: postIds,
};
}
componentWillMount() {
componentDidMount() {
this.mounted = true;
if (this.state.loading) {
@@ -114,18 +157,9 @@ export default class Permalink extends PureComponent {
}
}
componentWillReceiveProps(nextProps) {
if (this.props.channelName !== nextProps.channelName && this.mounted) {
this.setState({title: nextProps.channelName});
}
if (this.props.focusedPostId !== nextProps.focusedPostId && this.mounted) {
this.setState({loading: true});
if (nextProps.postIds && nextProps.postIds.length < 10) {
this.loadPosts(nextProps);
} else {
this.setState({loading: false});
}
componentDidUpdate() {
if (this.state.loading) {
this.loadPosts(this.props);
}
}
@@ -176,11 +210,11 @@ export default class Permalink extends PureComponent {
};
handlePress = () => {
const {channelId, channelName} = this.props;
const {channelIdState, channelNameState} = this.state;
if (this.refs.view) {
this.refs.view.growOut().then(() => {
this.jumpToChannel(channelId, channelName);
this.jumpToChannel(channelIdState, channelNameState);
});
}
};
@@ -303,18 +337,21 @@ export default class Permalink extends PureComponent {
}
};
archivedIcon = (style) => {
let ico = null;
archivedIcon = () => {
const style = getStyleSheet(this.props.theme);
let icon = null;
if (this.props.channelIsArchived) {
ico = (<Text>
<AwesomeIcon
name='archive'
style={[style.archiveIcon]}
/>
{' '}
</Text>);
icon = (
<Text>
<AwesomeIcon
name='archive'
style={[style.archiveIcon]}
/>
{' '}
</Text>
);
}
return ico;
return icon;
};
render() {
@@ -324,10 +361,15 @@ export default class Permalink extends PureComponent {
navigator,
onHashtagPress,
onPermalinkPress,
postIds,
theme,
} = this.props;
const {error, retry, loading, title} = this.state;
const {
error,
retry,
loading,
postIdsState,
title,
} = this.state;
const style = getStyleSheet(theme);
let postList;
@@ -359,7 +401,7 @@ export default class Permalink extends PureComponent {
onHashtagPress={onHashtagPress}
onPermalinkPress={onPermalinkPress}
onPostPress={this.goToThread}
postIds={postIds}
postIds={postIdsState}
currentUserId={currentUserId}
lastViewedAt={0}
navigator={navigator}
@@ -404,7 +446,7 @@ export default class Permalink extends PureComponent {
numberOfLines={1}
style={style.title}
>
{this.archivedIcon(style)}
{this.archivedIcon()}
{title}
</Text>
</View>

View File

@@ -0,0 +1,132 @@
// 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 Permalink from './permalink.js';
jest.mock('react-intl');
describe('Permalink', () => {
const navigator = {
dismissAllModals: jest.fn(),
dismissModal: jest.fn(),
push: jest.fn(),
resetTo: jest.fn(),
setOnNavigatorEvent: jest.fn(),
};
const actions = {
getPostsAfter: jest.fn(),
getPostsBefore: jest.fn(),
getPostThread: jest.fn(),
getChannel: jest.fn(),
handleSelectChannel: jest.fn(),
handleTeamChange: jest.fn(),
joinChannel: jest.fn(),
loadThreadIfNecessary: jest.fn(),
markChannelAsRead: jest.fn(),
markChannelAsViewed: jest.fn(),
selectPost: jest.fn(),
setChannelDisplayName: jest.fn(),
setChannelLoading: jest.fn(),
};
const baseProps = {
actions,
channelId: 'channel_id',
channelIsArchived: false,
channelName: 'channel_name',
channelTeamId: 'team_id',
currentTeamId: 'current_team_id',
currentUserId: 'current_user_id',
focusedPostId: 'focused_post_id',
isPermalink: true,
myMembers: {},
navigator,
onClose: jest.fn(),
onHashtagPress: jest.fn(),
onPermalinkPress: jest.fn(),
onPress: jest.fn(),
postIds: ['post_id_1', 'focused_post_id', 'post_id_3'],
theme: Preferences.THEMES.default,
};
test('should match snapshot', () => {
const wrapper = shallow(
<Permalink {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
// match archived icon
wrapper.setProps({channelIsArchived: true});
expect(wrapper.instance().archivedIcon()).toMatchSnapshot();
});
test('should match state and call loadPosts on retry', () => {
const wrapper = shallow(
<Permalink {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
wrapper.instance().loadPosts = jest.fn();
wrapper.instance().retry();
expect(wrapper.instance().loadPosts).toHaveBeenCalledTimes(2);
expect(wrapper.instance().loadPosts).toBeCalledWith(baseProps);
});
test('should call handleClose on onNavigatorEvent(backPress)', () => {
const wrapper = shallow(
<Permalink {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
wrapper.instance().handleClose = jest.fn();
wrapper.instance().onNavigatorEvent({id: 'backPress'});
expect(wrapper.instance().handleClose).toHaveBeenCalledTimes(1);
});
test('should match state', () => {
const wrapper = shallow(
<Permalink {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.state('channelIdState')).toEqual(baseProps.channelId);
expect(wrapper.state('channelNameState')).toEqual(baseProps.channelName);
expect(wrapper.state('focusedPostIdState')).toEqual(baseProps.focusedPostId);
expect(wrapper.state('postIdsState')).toEqual(baseProps.postIds);
wrapper.setProps({channelId: ''});
expect(wrapper.state('channelIdState')).toEqual(baseProps.channelId);
wrapper.setProps({channelId: null});
expect(wrapper.state('channelIdState')).toEqual(baseProps.channelId);
wrapper.setProps({channelId: 'new_channel_id'});
expect(wrapper.state('channelIdState')).toEqual('new_channel_id');
wrapper.setProps({channelName: ''});
expect(wrapper.state('channelNameState')).toEqual(baseProps.channelName);
wrapper.setProps({channelName: null});
expect(wrapper.state('channelNameState')).toEqual(baseProps.channelName);
wrapper.setProps({channelName: 'new_channel_name'});
expect(wrapper.state('channelNameState')).toEqual('new_channel_name');
wrapper.setProps({focusedPostId: 'new_focused_post_id'});
expect(wrapper.state('focusedPostIdState')).toEqual('new_focused_post_id');
wrapper.setProps({postIds: []});
expect(wrapper.state('postIdsState')).toEqual(baseProps.postIds);
wrapper.setProps({postIds: ['post_id_1', 'focused_post_id']});
expect(wrapper.state('postIdsState')).toEqual(['post_id_1', 'focused_post_id']);
wrapper.setProps({postIds: baseProps.postIds, focusedPostId: baseProps.focusedPostId});
expect(wrapper.state('loading')).toEqual(true);
wrapper.setProps({postIds: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'], focusedPostId: 'new_focused_post_id'});
expect(wrapper.state('loading')).toEqual(false);
});
});

View File

@@ -66,6 +66,7 @@ function makeMapStateToProps() {
theme: getTheme(state),
enableDateSuggestion,
timezoneOffsetInSeconds,
viewArchivedChannels,
};
};
}

View File

@@ -32,6 +32,7 @@ import PostSeparator from 'app/components/post_separator';
import SafeAreaView from 'app/components/safe_area_view';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import {ListTypes} from 'app/constants';
import mattermostManaged from 'app/mattermost_managed';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -43,6 +44,7 @@ const SECTION_HEIGHT = 20;
const RECENT_LABEL_HEIGHT = 42;
const RECENT_SEPARATOR_HEIGHT = 3;
const MODIFIER_LABEL_HEIGHT = 58;
const SCROLL_UP_MULTIPLIER = 6;
const SEARCHING = 'searching';
const NO_RESULTS = 'no results';
@@ -71,6 +73,7 @@ export default class Search extends PureComponent {
theme: PropTypes.object.isRequired,
enableDateSuggestion: PropTypes.bool,
timezoneOffsetInSeconds: PropTypes.number.isRequired,
viewArchivedChannels: PropTypes.bool,
};
static defaultProps = {
@@ -89,6 +92,7 @@ export default class Search extends PureComponent {
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
this.isX = DeviceInfo.getModel().includes('iPhone X');
this.contentOffsetY = 0;
this.state = {
channelName: '',
cursorPosition: 0,
@@ -119,7 +123,9 @@ export default class Search extends PureComponent {
const {searchingStatus: status, recent, enableDateSuggestion} = this.props;
const {searchingStatus: prevStatus} = prevProps;
const recentLength = recent.length;
const shouldScroll = prevStatus !== status && (status === RequestStatus.SUCCESS || status === RequestStatus.STARTED) && !this.props.isSearchGettingMore && !prevProps.isSearchGettingMore;
const shouldScroll = prevStatus !== status &&
(status === RequestStatus.SUCCESS || status === RequestStatus.STARTED) &&
!this.props.isSearchGettingMore && !prevProps.isSearchGettingMore;
if (this.props.isLandscape !== prevProps.isLandscape) {
this.refs.searchBar.blur();
@@ -142,6 +148,30 @@ export default class Search extends PureComponent {
mattermostManaged.removeEventListener(this.listenerId);
}
archivedIndicator = (postID, style) => {
const channelIsArchived = this.props.archivedPostIds.includes(postID);
let archivedIndicator = null;
if (channelIsArchived) {
archivedIndicator = (
<View style={style.archivedIndicator}>
<Text>
<AwesomeIcon
name='archive'
style={style.archivedText}
/>
{' '}
<FormattedText
style={style.archivedText}
id='search_item.channelArchived'
defaultMessage='Archived'
/>
</Text>
</View>
);
}
return archivedIndicator;
};
cancelSearch = preventDoubleTap(() => {
const {navigator} = this.props;
this.handleTextChanged('', true);
@@ -196,11 +226,34 @@ export default class Search extends PureComponent {
this.showingPermalink = false;
};
handleLayout = (event) => {
const {height} = event.nativeEvent.layout;
this.setState({searchListHeight: height});
};
handlePermalinkPress = (postId, teamName) => {
this.props.actions.loadChannelsByTeamName(teamName);
this.showPermalinkView(postId, true);
};
handleScroll = (event) => {
const pageOffsetY = event.nativeEvent.contentOffset.y;
if (pageOffsetY > 0) {
const contentHeight = event.nativeEvent.contentSize.height;
const direction = (this.contentOffsetY < pageOffsetY) ?
ListTypes.VISIBILITY_SCROLL_UP :
ListTypes.VISIBILITY_SCROLL_DOWN;
this.contentOffsetY = pageOffsetY;
if (
direction === ListTypes.VISIBILITY_SCROLL_UP &&
(contentHeight - pageOffsetY) < (this.state.searchListHeight * SCROLL_UP_MULTIPLIER)
) {
this.getMoreSearchResults();
}
}
};
handleSelectionChange = (event) => {
const cursorPosition = event.nativeEvent.selection.end;
this.setState({
@@ -208,6 +261,10 @@ export default class Search extends PureComponent {
});
};
handleSearchButtonPress = preventDoubleTap((text) => {
this.search(text);
});
handleTextChanged = (value, selectionChanged) => {
const {actions, searchingStatus, isSearchGettingMore} = this.props;
this.setState({value});
@@ -243,6 +300,12 @@ export default class Search extends PureComponent {
return item.id || item;
};
getMoreSearchResults = debounce(() => {
if (this.state.value && this.props.postIds.length) {
this.props.actions.getMorePostsForSearch();
}
}, 100);
onNavigatorEvent = (event) => {
if (event.id === 'backPress') {
if (this.state.preview) {
@@ -259,40 +322,24 @@ export default class Search extends PureComponent {
this.showPermalinkView(post.id, false);
};
showPermalinkView = (postId, isPermalink) => {
const {actions, navigator} = this.props;
actions.selectFocusedPostId(postId);
if (!this.showingPermalink) {
const options = {
screen: 'Permalink',
animationType: 'none',
backButtonTitle: '',
overrideBackPress: true,
navigatorStyle: {
navBarHidden: true,
screenBackgroundColor: changeOpacity('#000', 0.2),
modalPresentationStyle: 'overCurrentContext',
},
passProps: {
isPermalink,
onClose: this.handleClosePermalink,
onHashtagPress: this.handleHashtagPress,
onPermalinkPress: this.handlePermalinkPress,
},
};
this.showingPermalink = true;
navigator.showModal(options);
}
};
removeSearchTerms = preventDoubleTap((item) => {
const {actions, currentTeamId} = this.props;
actions.removeSearchTerms(currentTeamId, item.terms);
});
renderFooter = () => {
if (this.props.isSearchGettingMore) {
const style = getStyleFromTheme(this.props.theme);
return (
<View style={style.loadingMore}>
<Loading/>
</View>
);
}
return null;
};
renderModifiers = ({item}) => {
const {theme} = this.props;
const style = getStyleFromTheme(theme);
@@ -327,30 +374,6 @@ export default class Search extends PureComponent {
);
};
archivedIndicator = (postID, style) => {
const channelIsArchived = this.props.archivedPostIds.includes(postID);
let archivedIndicator = null;
if (channelIsArchived) {
archivedIndicator = (
<View style={style.archivedIndicator}>
<Text>
<AwesomeIcon
name='archive'
style={style.archivedText}
/>
{' '}
<FormattedText
style={style.archivedText}
id='search_item.channelArchived'
defaultMessage='Archived'
/>
</Text>
</View>
);
}
return archivedIndicator;
};
renderPost = ({item, index}) => {
const {postIds, theme} = this.props;
const {managedConfig} = this.state;
@@ -380,7 +403,7 @@ export default class Search extends PureComponent {
}
return (
<View>
<View style={style.postResult}>
<ChannelDisplayName postId={item}/>
{this.archivedIndicator(postIds[index], style)}
<SearchResultPost
@@ -476,6 +499,35 @@ export default class Search extends PureComponent {
});
};
showPermalinkView = (postId, isPermalink) => {
const {actions, navigator} = this.props;
actions.selectFocusedPostId(postId);
if (!this.showingPermalink) {
const options = {
screen: 'Permalink',
animationType: 'none',
backButtonTitle: '',
overrideBackPress: true,
navigatorStyle: {
navBarHidden: true,
screenBackgroundColor: changeOpacity('#000', 0.2),
modalPresentationStyle: 'overCurrentContext',
},
passProps: {
isPermalink,
onClose: this.handleClosePermalink,
onHashtagPress: this.handleHashtagPress,
onPermalinkPress: this.handlePermalinkPress,
},
};
this.showingPermalink = true;
navigator.showModal(options);
}
};
scrollToTop = () => {
if (this.refs.list) {
this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
@@ -486,7 +538,7 @@ export default class Search extends PureComponent {
};
search = (terms, isOrSearch) => {
const {actions, currentTeamId} = this.props;
const {actions, currentTeamId, viewArchivedChannels} = this.props;
this.handleTextChanged(`${terms.trim()} `);
@@ -500,13 +552,17 @@ export default class Search extends PureComponent {
});
// timezone offset in seconds
actions.searchPostsWithParams(currentTeamId, {terms: terms.trim(), is_or_search: isOrSearch, time_zone_offset: this.props.timezoneOffsetInSeconds, page: 0, per_page: 20}, true);
const params = {
terms: terms.trim(),
is_or_search: isOrSearch,
time_zone_offset: this.props.timezoneOffsetInSeconds,
page: 0,
per_page: 20,
include_deleted_channels: viewArchivedChannels,
};
actions.searchPostsWithParams(currentTeamId, params, true);
};
handleSearchButtonPress = preventDoubleTap((text) => {
this.search(text);
});
setModifierValue = preventDoubleTap((modifier) => {
const {value} = this.state;
let newValue = '';
@@ -533,12 +589,6 @@ export default class Search extends PureComponent {
Keyboard.dismiss();
});
onEndReached = debounce(() => {
if (this.state.value) {
this.props.actions.getMorePostsForSearch();
}
}, 100);
render() {
const {
isLandscape,
@@ -721,8 +771,10 @@ export default class Search extends PureComponent {
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
stickySectionHeadersEnabled={Platform.OS === 'ios'}
onEndReached={this.onEndReached}
onEndReachedThreshold={Platform.OS === 'ios' ? 0 : 1}
onLayout={this.handleLayout}
onScroll={this.handleScroll}
scrollEventThrottle={60}
ListFooterComponent={this.renderFooter}
/>
<Autocomplete
cursorPosition={cursorPosition}
@@ -861,6 +913,12 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
archivedText: {
color: changeOpacity(theme.centerChannelColor, 0.4),
},
postResult: {
overflow: 'hidden',
},
loadingMore: {
height: 60,
},
};
});

View File

@@ -443,6 +443,7 @@ export default class SelectServer extends PureComponent {
behavior='padding'
style={style.container}
keyboardVerticalOffset={0}
enabled={Platform.OS === 'ios'}
>
<StatusBar barStyle={statusStyle}/>
<TouchableWithoutFeedback onPress={this.blur}>

View File

@@ -15,7 +15,34 @@ exports[`SelectTeam should match snapshot for fail of teams 1`] = `
}
}
onRetry={[Function]}
theme={Object {}}
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",
}
}
/>
`;
@@ -23,7 +50,7 @@ exports[`SelectTeam should match snapshot for teams 1`] = `
<Component
style={
Object {
"backgroundColor": undefined,
"backgroundColor": "#ffffff",
"flex": 1,
}
}

View File

@@ -3,6 +3,8 @@
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import {RequestStatus} from 'mattermost-redux/constants';
import SelectTeam from './select_team.js';
@@ -40,7 +42,7 @@ describe('SelectTeam', () => {
},
userWithoutTeams: false,
teams: [],
theme: {},
theme: Preferences.THEMES.default,
teamsRequest: {
status: RequestStatus.FAILURE,
},

View File

@@ -18,8 +18,30 @@ exports[`NotificationSettingsEmailAndroid should match snapshot 1`] = `
}
theme={
Object {
"centerChannelBg": "#aaa",
"centerChannelColor": "#aaa",
"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",
}
}
/>

View File

@@ -14,8 +14,30 @@ exports[`NotificationSettingsEmailIos should match snapshot, renderEmailSection
headerId="mobile.notification_settings.email.send"
theme={
Object {
"centerChannelBg": "#aaa",
"centerChannelColor": "#aaa",
"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",
}
}
>
@@ -33,8 +55,30 @@ exports[`NotificationSettingsEmailIos should match snapshot, renderEmailSection
selected={true}
theme={
Object {
"centerChannelBg": "#aaa",
"centerChannelColor": "#aaa",
"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",
}
}
/>
@@ -61,8 +105,30 @@ exports[`NotificationSettingsEmailIos should match snapshot, renderEmailSection
selected={false}
theme={
Object {
"centerChannelBg": "#aaa",
"centerChannelColor": "#aaa",
"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",
}
}
/>

View File

@@ -3,6 +3,8 @@
import React from 'react';
import Preferences from 'mattermost-redux/constants/preferences';
import {shallowWithIntl} from 'test/intl-test-helper';
import {emptyFunction} from 'app/utils/general';
@@ -22,10 +24,7 @@ describe('NotificationSettingsEmailAndroid', () => {
},
sendEmailNotifications: true,
siteName: 'Mattermost',
theme: {
centerChannelBg: '#aaa',
centerChannelColor: '#aaa',
},
theme: Preferences.THEMES.default,
};
test('should match snapshot', () => {

View File

@@ -4,6 +4,8 @@
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import {emptyFunction} from 'app/utils/general';
import SectionItem from 'app/screens/settings/section_item';
@@ -30,10 +32,7 @@ describe('NotificationSettingsEmailIos', () => {
},
sendEmailNotifications: true,
siteName: 'Mattermost',
theme: {
centerChannelBg: '#aaa',
centerChannelColor: '#aaa',
},
theme: Preferences.THEMES.default,
};
test('should match snapshot, renderEmailSection', () => {

View File

@@ -53,19 +53,19 @@ export default class NotificationSettingsMentionsBase extends PureComponent {
}
const comments = notifyProps.comments || 'any';
const mentionKeysString = mentionKeys.join(',');
const newState = {
...notifyProps,
comments,
newReplyValue: comments,
usernameMention: usernameMentionIndex > -1,
mention_keys: mentionKeys.join(','),
mention_keys: mentionKeysString,
androidKeywords: mentionKeysString,
showKeywordsModal: false,
showReplyModal: false,
};
this.keywords = newState.mention_keys;
return newState;
};

View File

@@ -23,8 +23,10 @@ import NotificationSettingsMentionsBase from './notification_settings_mention_ba
class NotificationSettingsMentionsAndroid extends NotificationSettingsMentionsBase {
cancelMentionKeys = () => {
this.setState({showKeywordsModal: false});
this.keywords = this.state.mention_keys;
this.setState({
androidKeywords: this.state.mention_keys,
showKeywordsModal: false,
});
};
cancelReplyNotification = () => {
@@ -34,8 +36,8 @@ class NotificationSettingsMentionsAndroid extends NotificationSettingsMentionsBa
});
};
onKeywordsChangeText = (value) => {
this.keywords = value;
onKeywordsChangeText = (androidKeywords) => {
this.setState({androidKeywords});
};
onReplyChanged = (value) => {
@@ -64,7 +66,7 @@ class NotificationSettingsMentionsAndroid extends NotificationSettingsMentionsBa
</View>
<TextInputWithLocalizedPlaceholder
autoFocus={true}
value={this.keywords}
value={this.state.androidKeywords}
blurOnSubmit={true}
onChangeText={this.onKeywordsChangeText}
onSubmitEditing={this.saveMentionKeys}
@@ -243,8 +245,7 @@ class NotificationSettingsMentionsAndroid extends NotificationSettingsMentionsBa
}
saveMentionKeys = () => {
this.setState({showKeywordsModal: false});
this.updateMentionKeys(this.keywords);
this.updateMentionKeys(this.state.androidKeywords);
};
saveReplyNotification = () => {

View File

@@ -161,6 +161,10 @@ class SSO extends PureComponent {
if (parsed.host.includes('.onelogin.com')) {
nextState.jsCode = oneLoginFormScalingJS;
} else if (parsed.host.includes(this.completedUrl)) {
this.webView.setNativeProps({
onMessage: this.onMessage,
});
}
if (Object.keys(nextState).length) {
@@ -233,7 +237,6 @@ class SSO extends PureComponent {
onNavigationStateChange={this.onNavigationStateChange}
onShouldStartLoadWithRequest={() => true}
renderLoading={this.renderLoading}
onMessage={this.onMessage}
injectedJavaScript={jsCode}
onLoadEnd={this.onLoadEnd}
useWebKit={true}

View File

@@ -0,0 +1,214 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`thread should match snapshot, has root post 1`] = `
<Connect(SafeAreaIos)
excludeHeader={true}
keyboardOffset={20}
>
<Connect(StatusBar) />
<KeyboardLayout
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
}
}
>
<Connect(PostList)
currentUserId="member_user_id"
indicateNewMessages={true}
navigator={
Object {
"dismissModal": [MockFunction],
"pop": [MockFunction],
"resetTo": [MockFunction],
"setTitle": [MockFunction] {
"calls": Array [
Array [
Object {
"title": undefined,
},
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
],
},
}
}
postIds={
Array [
"root_id",
"post_id_1",
"post_id_2",
]
}
renderFooter={
<Loading
color="grey"
size="large"
style={Object {}}
/>
}
/>
<Connect(PostTextbox)
channelId="channel_id"
channelIsArchived={false}
navigator={
Object {
"dismissModal": [MockFunction],
"pop": [MockFunction],
"resetTo": [MockFunction],
"setTitle": [MockFunction] {
"calls": Array [
Array [
Object {
"title": undefined,
},
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
],
},
}
}
onCloseChannel={[Function]}
rootId="root_id"
/>
</KeyboardLayout>
</Connect(SafeAreaIos)>
`;
exports[`thread should match snapshot, no root post, loading 1`] = `
<Connect(SafeAreaIos)
excludeHeader={true}
keyboardOffset={20}
>
<Connect(StatusBar) />
<KeyboardLayout
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
}
}
>
<Loading
color="grey"
size="large"
style={Object {}}
/>
</KeyboardLayout>
</Connect(SafeAreaIos)>
`;
exports[`thread should match snapshot, render footer 1`] = `
<Connect(PostList)
currentUserId="member_user_id"
indicateNewMessages={true}
navigator={
Object {
"dismissModal": [MockFunction],
"pop": [MockFunction],
"resetTo": [MockFunction],
"setTitle": [MockFunction] {
"calls": Array [
Array [
Object {
"title": undefined,
},
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
],
},
}
}
postIds={
Array [
"root_id",
"post_id_1",
"post_id_2",
]
}
renderFooter={
<Loading
color="grey"
size="large"
style={Object {}}
/>
}
/>
`;
exports[`thread should match snapshot, render footer 2`] = `
<Connect(PostList)
currentUserId="member_user_id"
indicateNewMessages={true}
lastViewedAt={0}
navigator={
Object {
"dismissModal": [MockFunction],
"pop": [MockFunction],
"resetTo": [MockFunction],
"setTitle": [MockFunction] {
"calls": Array [
Array [
Object {
"title": undefined,
},
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
],
},
}
}
postIds={
Array [
"root_id",
"post_id_1",
"post_id_2",
]
}
renderFooter={null}
/>
`;
exports[`thread should match snapshot, render footer 3`] = `
<Connect(SafeAreaIos)
excludeHeader={true}
keyboardOffset={20}
>
<Connect(StatusBar) />
<KeyboardLayout
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
}
}
>
<Loading
color="grey"
size="large"
style={Object {}}
/>
</KeyboardLayout>
</Connect(SafeAreaIos)>
`;

View File

@@ -4,7 +4,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Platform} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import {intlShape} from 'react-intl';
import {General, RequestStatus} from 'mattermost-redux/constants';
import Loading from 'app/components/loading';
@@ -16,7 +16,7 @@ import StatusBar from 'app/components/status_bar';
import {makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
import DeletedPost from 'app/components/deleted_post';
class Thread extends PureComponent {
export default class Thread extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
selectPost: PropTypes.func.isRequired,
@@ -24,7 +24,6 @@ class Thread extends PureComponent {
channelId: PropTypes.string.isRequired,
channelType: PropTypes.string,
displayName: PropTypes.string,
intl: intlShape.isRequired,
navigator: PropTypes.object,
myMember: PropTypes.object.isRequired,
rootId: PropTypes.string.isRequired,
@@ -36,8 +35,13 @@ class Thread extends PureComponent {
state = {};
static contextTypes = {
intl: intlShape,
};
componentWillMount() {
const {channelType, displayName, intl} = this.props;
const {channelType, displayName} = this.props;
const {intl} = this.context;
let title;
if (channelType === General.DM_CHANNEL) {
@@ -86,16 +90,21 @@ class Thread extends PureComponent {
hasRootPost = () => {
return this.props.postIds.includes(this.props.rootId);
}
};
renderFooter = () => {
if (!this.hasRootPost() && this.props.threadLoadingStatus.status !== RequestStatus.STARTED) {
return (
<DeletedPost theme={this.props.theme}/>
);
} else if (this.props.threadLoadingStatus.status === RequestStatus.STARTED) {
return (
<Loading/>
);
}
return null;
}
};
onCloseChannel = () => {
this.props.navigator.resetTo({
@@ -112,7 +121,7 @@ class Thread extends PureComponent {
screenBackgroundColor: 'transparent',
},
});
}
};
render() {
const {
@@ -126,14 +135,11 @@ class Thread extends PureComponent {
} = this.props;
const style = getStyle(theme);
let content;
if (this.props.threadLoadingStatus.status === RequestStatus.STARTED) {
content = (
<Loading/>
);
} else {
let postTextBox;
if (this.hasRootPost()) {
content = (
<PostList
renderFooter={this.renderFooter}
renderFooter={this.renderFooter()}
indicateNewMessages={true}
postIds={postIds}
currentUserId={myMember.user_id}
@@ -141,9 +147,7 @@ class Thread extends PureComponent {
navigator={navigator}
/>
);
}
let postTextBox;
if (this.hasRootPost() && this.props.threadLoadingStatus.status !== RequestStatus.STARTED) {
postTextBox = (
<PostTextbox
channelIsArchived={channelIsArchived}
@@ -153,6 +157,10 @@ class Thread extends PureComponent {
onCloseChannel={this.onCloseChannel}
/>
);
} else {
content = (
<Loading/>
);
}
return (
@@ -161,11 +169,7 @@ class Thread extends PureComponent {
keyboardOffset={20}
>
<StatusBar/>
<KeyboardLayout
behavior='padding'
style={style.container}
keyboardVerticalOffset={65}
>
<KeyboardLayout style={style.container}>
{content}
{postTextBox}
</KeyboardLayout>
@@ -182,5 +186,3 @@ const getStyle = makeStyleSheetFromTheme((theme) => {
},
};
});
export default injectIntl(Thread);

View File

@@ -0,0 +1,104 @@
// 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 {General, RequestStatus} from 'mattermost-redux/constants';
import PostList from 'app/components/post_list';
import Thread from './thread.js';
jest.mock('react-intl');
describe('thread', () => {
const navigator = {
dismissModal: jest.fn(),
pop: jest.fn(),
resetTo: jest.fn(),
setTitle: jest.fn(),
};
const baseProps = {
actions: {
selectPost: jest.fn(),
},
channelId: 'channel_id',
channelType: General.OPEN_CHANNEL,
displayName: 'channel_display_name',
navigator,
myMember: {last_viewed_at: 0, user_id: 'member_user_id'},
rootId: 'root_id',
theme: Preferences.THEMES.default,
postIds: ['root_id', 'post_id_1', 'post_id_2'],
channelIsArchived: false,
threadLoadingStatus: {status: RequestStatus.STARTED},
};
test('should match snapshot, has root post', () => {
const wrapper = shallow(
<Thread {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot, no root post, loading', () => {
const newPostIds = ['post_id_1', 'post_id_2'];
const wrapper = shallow(
<Thread
{...baseProps}
postIds={newPostIds}
/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should call props.navigator on onCloseChannel', () => {
const channelScreen = {
screen: 'Channel',
title: '',
animated: false,
backButtonTitle: '',
navigatorStyle: {
animated: true,
animationType: 'fade',
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
},
};
const newNavigator = {...navigator};
const wrapper = shallow(
<Thread
{...baseProps}
navigator={newNavigator}
/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
wrapper.instance().onCloseChannel();
expect(newNavigator.resetTo).toHaveBeenCalledTimes(1);
expect(newNavigator.resetTo).toBeCalledWith(channelScreen);
});
test('should match snapshot, render footer', () => {
const wrapper = shallow(
<Thread {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
// return loading
expect(wrapper.find(PostList).getElement()).toMatchSnapshot();
// return null
wrapper.setProps({threadLoadingStatus: {status: RequestStatus.SUCCESS}});
expect(wrapper.find(PostList).getElement()).toMatchSnapshot();
// return deleted post
const newPostIds = ['post_id_1', 'post_id_2'];
wrapper.setProps({postIds: newPostIds});
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View File

@@ -12,7 +12,7 @@ exports[`user_profile should match snapshot 1`] = `
<ScrollView
style={
Object {
"backgroundColor": "#aaa",
"backgroundColor": "#ffffff",
"flex": 1,
}
}
@@ -35,7 +35,7 @@ exports[`user_profile should match snapshot 1`] = `
<Component
style={
Object {
"color": "#aaa",
"color": "#3d3c40",
"fontSize": 15,
"marginTop": 15,
}
@@ -69,7 +69,7 @@ exports[`user_profile should match snapshot 1`] = `
<Component
style={
Object {
"color": "#aaa",
"color": "#3d3c40",
"fontSize": 15,
}
}
@@ -89,9 +89,30 @@ exports[`user_profile should match snapshot 1`] = `
textId="mobile.routes.user_profile.send_message"
theme={
Object {
"centerChannelBg": "#aaa",
"centerChannelColor": "#aaa",
"color": "#aaa",
"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",
}
}
togglable={false}

View File

@@ -2,10 +2,12 @@
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
jest.mock('react-intl');
import Preferences from 'mattermost-redux/constants/preferences';
import UserProfile from './user_profile.js';
jest.mock('react-intl');
jest.mock('app/utils/theme', () => {
const original = require.requireActual('app/utils/theme');
return {
@@ -29,11 +31,7 @@ describe('user_profile', () => {
resetTo: jest.fn(),
},
teams: [],
theme: {
centerChannelBg: '#aaa',
centerChannelColor: '#aaa',
color: '#aaa',
},
theme: Preferences.THEMES.default,
enableTimezone: false,
user: {
email: 'test@test.com',

View File

@@ -44,7 +44,10 @@ export function makePreparePostIdsForPostList() {
for (let i = posts.length - 1; i >= 0; i--) {
const post = posts[i];
if (post.type === Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL && !selectedPostId) {
if (
!post ||
(post.type === Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL && !selectedPostId)
) {
continue;
}

View File

@@ -21,6 +21,7 @@ import mattermostBucket from 'app/mattermost_bucket';
import Config from 'assets/config';
import {messageRetention} from './middleware';
import {createThunkMiddleware} from './thunk';
import {transformSet} from './utils';
function getAppReducer() {
@@ -276,8 +277,14 @@ export default function configureAppStore(initialState) {
},
};
const additionalMiddleware = [createSentryMiddleware(), messageRetention];
return configureStore(initialState, appReducer, offlineOptions, getAppReducer, {
additionalMiddleware,
});
const clientOptions = {
additionalMiddleware: [
createThunkMiddleware(),
createSentryMiddleware(),
messageRetention,
],
enableThunk: false, // We override the default thunk middleware
};
return configureStore(initialState, appReducer, offlineOptions, getAppReducer, clientOptions);
}

41
app/store/thunk.js Normal file
View File

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
captureMessage,
cleanUrlForLogging,
LOGGER_JAVASCRIPT_WARNING,
} from 'app/utils/sentry';
// Creates middleware that mimics thunk while catching network errors thrown by Client4 that haven't
// been otherwise handled.
export function createThunkMiddleware() {
return (store) => (next) => (action) => {
if (typeof action === 'function') {
const result = action(store.dispatch, store.getState);
if (result instanceof Promise) {
return result.catch((error) => {
if (error.url) {
// This is a connection error from mattermost-redux. This should've been handled
// within the action itself, so we'll log to Sentry enough to identify where
// that handling is missing.
captureMessage(
`Caught Client4 error "${error.message}" from "${cleanUrlForLogging(error.url)}"`,
LOGGER_JAVASCRIPT_WARNING,
store
);
return {error};
}
throw error;
});
}
return result;
}
return next(action);
};
}

View File

@@ -28,7 +28,10 @@ export const calculateDimensions = (height, width, viewPortWidth = 0, viewPortHe
) {
imageHeight = viewPortHeight || IMAGE_MAX_HEIGHT;
imageWidth = imageHeight * heightRatio;
} else if (imageHeight < IMAGE_MIN_DIMENSION) {
} else if (
imageHeight < IMAGE_MIN_DIMENSION &&
IMAGE_MIN_DIMENSION * heightRatio <= viewPortWidth
) {
imageHeight = IMAGE_MIN_DIMENSION;
imageWidth = imageHeight * heightRatio;
}

View File

@@ -56,4 +56,10 @@ describe('Images calculateDimensions', () => {
const {height} = calculateDimensions(1920, 1080, PORTRAIT_VIEWPORT, 340);
expect(height).toEqual(340);
});
it('images with height below 50 but setting to 50 will make the width exceed the view port width should remain as is', () => {
const {height, width} = calculateDimensions(45, 310, PORTRAIT_VIEWPORT);
expect(height).toEqual(45);
expect(width).toEqual(310);
});
});

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