Compare commits

...

94 Commits

Author SHA1 Message Date
Elias Nahum
9d743e6454 Bump app build number to 158 (#2341) 2018-11-14 12:59:14 -03:00
Elias Nahum
8a7dabfadd Fix regression when scrolling in search screen (#2339) 2018-11-14 10:52:05 -03:00
Elias Nahum
c1b2667d6b Bump app build number to 157 (#2338) 2018-11-13 20:01:30 -03:00
Elias Nahum
4eff598665 MM-12952 Avoid iOS autocorrect from overriding autocomplete values (#2335)
* Avoid iOS autocorrect from overriding autocomplete values

* Fix double ~ for channel mentions in Android
2018-11-13 19:47:14 -03:00
Elias Nahum
0be1500055 Bump app build number to 156 (#2333) 2018-11-09 20:50:19 -03:00
Harrison Healey
c0c91ebf71 MM-12901 Improve handling of hashtags on Recent Mentions, Flagged Posts, and Permalink screens (#2329)
* MM-12901 Create showSearchModal action and use for hashtags

* MM-12901 Improve handling of hashtags on Recent Mentions, Flagged Posts, and Permalink screens
2018-11-09 20:43:55 -03:00
Elias Nahum
071c0f94f0 Fix when message have multiple actions (#2331)
* Fix when message have multiple actions

* Feedback review
2018-11-09 18:39:38 -05:00
Elias Nahum
5e1e155d07 Fix prefetch (#2330) 2018-11-09 19:04:55 -03:00
Elias Nahum
75ec18c0f4 Fix Android image preview (#2323) 2018-11-09 18:22:00 -03:00
Harrison Healey
d19e269dec MM-11477 Wrap objects thrown by Client4 in a proper error type (#2328)
* MM-11477 Wrap objects thrown by Client4 in a proper error type

* Update mattermost-redux
2018-11-08 12:18:51 -05:00
Elias Nahum
bbd65b292f Fix Build on Jenkins 2018-11-06 21:09:47 -03:00
Elias Nahum
f0ce1fdc2b Bump app build number to 155 (#2322) 2018-11-06 14:10:21 -03:00
Elias Nahum
05259326f6 do not wait for rehydration is credentials are false (#2312) 2018-11-05 15:25:57 -03:00
Elias Nahum
1787a04f6e Set the file prefix in image_cache_manager (#2313) 2018-11-05 15:24:52 -03:00
Elias Nahum
76ed62b153 Keep connection as active when retrying teams (#2314) 2018-11-05 15:20:22 -03:00
Elias Nahum
a717ec1757 Bump app build number to 154 (#2311) 2018-11-02 18:14:21 -03:00
Elias Nahum
2d7b3a5685 Allow user system CA's for Android (#2301) 2018-11-02 18:06:34 -03:00
sudheer
8f461acd50 MM-12841 Fix for mention channel ordering with unread channels
* MM-Redux has update
2018-11-02 23:08:20 +05:30
Elias Nahum
dd318c2a10 Show join teams option in the team sidebar when there are other joinable teams (#2293) 2018-10-31 14:08:46 -03:00
Elias Nahum
c71389c98c Allow channel drawer to close when showing the team list (#2290)
* Allow channel drawer to close when showing the team list

* prevent closing the sidebar when jump to is active

* Add proper header to drawer_layout.js
2018-10-31 13:41:10 -03:00
Elias Nahum
30fb178f8b Fix team utils test eslint (#2295) 2018-10-30 15:49:18 -03:00
Elias Nahum
3f66128e3e Bump app build number to 153 (#2294) 2018-10-30 15:10:47 -03:00
Elias Nahum
c010cbc74b Add padding to tap effect for team list item (#2291) 2018-10-30 09:49:19 -03:00
Harrison Healey
8ba96ecc70 MM-12467 Made borders between autocomplete items consistent (#2289) 2018-10-29 15:23:20 -04:00
Elias Nahum
b76469c867 Retry fetching teams if no teams are found (#2288)
* Retry fetching teams if no teams are found

* Remove flow
2018-10-27 00:51:06 -03:00
Elias Nahum
c4e6d072da Capture video in high quality (#2287) 2018-10-27 00:49:45 -03:00
Elias Nahum
83dad65f1f translations PR 20181023 (#2284) 2018-10-24 10:46:13 -03:00
Elias Nahum
82f4526704 Bump app build number to 152 (#2282) 2018-10-19 10:18:29 -03:00
Jani Uusitalo
2e4cdc8309 Fix 'pre-buid' typo in build-android target (#2281) 2018-10-19 09:01:57 -03:00
sudheer
278fbe3ca9 Use native transition on drawyerLayout component 2018-10-19 01:57:17 +05:30
Elias Nahum
ceaf153875 Bump app build number to 151 (#2276) 2018-10-18 13:21:25 -03:00
Elias Nahum
1ed3efeb40 Bump app build number to 150 (#2275) 2018-10-18 12:18:07 -03:00
Elias Nahum
8a72af7969 Hook the onMessage event only when needed (#2271) 2018-10-18 12:12:42 -03:00
Elias Nahum
773590b83d Fix typo in build script (#2272) 2018-10-18 10:57:04 -03:00
Elias Nahum
a5e735233a Bump app version number to 1.14.0 (#2270) 2018-10-16 09:43:25 -03:00
Elias Nahum
ec1c66c9ec Fix backgroundColor when updating the theme in real time (#2268) 2018-10-15 21:31:50 -03:00
Elias Nahum
e1fd773850 Fix Upload file size (#2252) 2018-10-15 21:30:51 -03:00
Sudheer
09ed38ca98 Fix for LHS populating you next to current user (#2263)
* Fix for LHS populating you next to current user

* Add test cases
 * Refactor to use avaiable props conditions in component
   instead of passing flags from connector
2018-10-15 21:01:02 +05:30
Elias Nahum
c70d563a88 Update build script to run npm ci instead of npm install (#2264) 2018-10-14 20:42:45 -03:00
Elias Nahum
c44d6d49e9 Scroll search results to the top (#2241) 2018-10-13 01:38:20 -03:00
Elias Nahum
251b91d7c0 Set Roboto as the default font family for Android (#2244) 2018-10-13 01:38:02 -03:00
Powhu Yang
72fce0cb87 Fix iOS Share Extension crashes (#2242) 2018-10-13 01:37:42 -03:00
Elias Nahum
a1f3c2c6ef Do not show the option to copy post if there is no text (#2240) 2018-10-13 01:37: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
198 changed files with 5293 additions and 3620 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-build check-style prepare-android-build ## Build the Android app
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
@@ -188,7 +207,7 @@ build-android: | pre-run check-style prepare-android-build ## Creates an Android
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
unsigned-ios: pre-run check-style
unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS app
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
@@ -202,7 +221,7 @@ unsigned-ios: pre-run check-style
@rm -rf build-ios/
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
unsigned-android: pre-run check-style prepare-android-build
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
@@ -215,6 +234,21 @@ unsigned-android: pre-run check-style prepare-android-build
test: | pre-run check-style ## Runs tests
@npm test
build-pr: | can-build-pr stop pre-build check-style ## Build a PR from the mattermost-mobile repo
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@echo "Building App from PR ${PR_ID}"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build_pr pr:PR-${PR_ID}
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
can-build-pr:
@if [ -z ${PR_ID} ]; then \
echo a PR number needs to be specified; \
exit 1; \
fi
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

View File

@@ -1361,7 +1361,7 @@ SOFTWARE.
## react-native-drawer-layout
This product contains 'react-native-drawer-layout' by Brent Vatne.
This product contains a modified version of 'react-native-drawer-layout' by Brent Vatne.
A platform-agnostic drawer layout. Pure JavaScript implementation on iOS and native implementation on Android. Why? Because the drawer layout is a useful component regardless of the platform! And if you can use it without changing any code, that's perfect

View File

@@ -113,8 +113,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 144
versionName "1.13.0"
versionCode 158
versionName "1.14.0"
multiDexEnabled = true
ndk {
abiFilters "armeabi-v7a", "x86"
@@ -214,11 +214,11 @@ dependencies {
implementation project(':react-native-recyclerview-list')
// For animated GIF support
implementation 'com.facebook.fresco:animated-base-support:1.3.0'
implementation 'com.facebook.fresco:fresco:1.10.0'
implementation 'com.facebook.fresco:animated-gif:1.10.0'
// For WebP support, including animated WebP
implementation 'com.facebook.fresco:animated-gif:1.3.0'
implementation 'com.facebook.fresco:animated-webp:1.3.0'
implementation 'com.facebook.fresco:webpsupport:1.3.0'
implementation 'com.facebook.fresco:animated-webp:1.10.0'
implementation 'com.facebook.fresco:webpsupport:1.10.0'
}
// Run this once to be able to run the application with BUCK

View File

@@ -16,6 +16,7 @@
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config"
>
<meta-data android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<!-- Trust preinstalled CAs -->
<certificates src="system" />
<!-- Additionally trust user added CAs -->
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

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

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {ViewTypes} from 'app/constants';
export function handleSearchDraftChanged(text) {
@@ -11,3 +13,25 @@ export function handleSearchDraftChanged(text) {
}, getState);
};
}
export function showSearchModal(navigator, initialValue = '') {
return (dispatch, getState) => {
const theme = getTheme(getState());
const options = {
screen: 'Search',
animated: true,
backButtonTitle: '',
overrideBackPress: true,
passProps: {
initialValue,
},
navigatorStyle: {
navBarHidden: true,
screenBackgroundColor: theme.centerChannelBg,
},
};
navigator.showModal(options);
};
}

View File

@@ -3,16 +3,18 @@
import {batchActions} from 'redux-batched-actions';
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {getMyTeams} from 'mattermost-redux/actions/teams';
import {RequestStatus} from 'mattermost-redux/constants';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {NavigationTypes} from 'app/constants';
import {selectFirstAvailableTeam} from 'app/utils/teams';
import {setChannelDisplayName} from './channel';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
export function handleTeamChange(teamId, selectChannel = true) {
return async (dispatch, getState) => {
@@ -46,21 +48,30 @@ export function selectDefaultTeam() {
const {teams: allTeams, myMembers} = state.entities.teams;
const teams = Object.keys(myMembers).map((key) => allTeams[key]);
let defaultTeam;
if (ExperimentalPrimaryTeam) {
defaultTeam = teams.find((t) => t.name === ExperimentalPrimaryTeam.toLowerCase());
}
if (!defaultTeam) {
defaultTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
}
let defaultTeam = selectFirstAvailableTeam(teams, ExperimentalPrimaryTeam);
if (defaultTeam) {
handleTeamChange(defaultTeam.id)(dispatch, getState);
dispatch(handleTeamChange(defaultTeam.id));
} else if (state.requests.teams.getTeams.status === RequestStatus.FAILURE || state.requests.teams.getMyTeams.status === RequestStatus.FAILURE) {
EventEmitter.emit(NavigationTypes.NAVIGATION_ERROR_TEAMS);
} else {
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
// If for some reason we reached this point cause of a failure in rehydration or something
// lets fetch the teams one more time to make sure the user does not belong to any team
const {data, error} = await dispatch(getMyTeams());
if (error) {
EventEmitter.emit(NavigationTypes.NAVIGATION_ERROR_TEAMS);
return;
}
if (data) {
defaultTeam = selectFirstAvailableTeam(data, ExperimentalPrimaryTeam);
}
if (defaultTeam) {
dispatch(handleTeamChange(defaultTeam.id));
} else {
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
}
}
};
}

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
/* eslint-disable global-require*/
import {AsyncStorage, Linking, NativeModules, Platform} from 'react-native';
import {AsyncStorage, Linking, NativeModules, Platform, Text} from 'react-native';
import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain';
import {loadMe} from 'mattermost-redux/actions/users';
@@ -62,10 +62,37 @@ export default class App {
// Usage deeplinking
Linking.addEventListener('url', this.handleDeepLink);
this.setFontFamily();
this.getStartupThemes();
this.getAppCredentials();
}
setFontFamily = () => {
// Set a global font for Android
if (Platform.OS === 'android') {
const defaultFontFamily = {
style: {
fontFamily: 'Roboto',
},
};
const TextRender = Text.render;
const initialDefaultProps = Text.defaultProps;
Text.defaultProps = {
...initialDefaultProps,
...defaultFontFamily,
};
Text.render = function render(props, ...args) {
const oldProps = props;
let newProps = {...props, style: [defaultFontFamily.style, props.style]};
try {
return Reflect.apply(TextRender, this, [newProps, ...args]);
} finally {
newProps = oldProps;
}
};
}
};
getTranslations = () => {
if (this.translations) {
return this.translations;
@@ -115,6 +142,8 @@ export default class App {
this.waitForRehydration = true;
}
}
} else {
this.waitForRehydration = false;
}
} catch (error) {
return null;

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: 0.8,
videoQuality: 'high',
noData: true,
mediaType,
storageOptions: {
cameraRoll: true,
waitUntilSaved: true,
@@ -84,7 +94,7 @@ export default class AttachmentButton extends PureComponent {
attachFileFromLibrary = () => {
const {formatMessage} = this.context.intl;
const options = {
quality: 1.0,
quality: 0.8,
noData: true,
permissionDenied: {
title: formatMessage({
@@ -116,10 +126,14 @@ export default class AttachmentButton extends PureComponent {
});
};
attachVideoFromCamera = () => {
return this.attachFileFromCamera('video');
};
attachVideoFromLibraryAndroid = () => {
const {formatMessage} = this.context.intl;
const options = {
quality: 1.0,
videoQuality: 'high',
mediaType: 'video',
noData: true,
permissionDenied: {
@@ -283,8 +297,20 @@ export default class AttachmentButton extends PureComponent {
return true;
};
uploadFiles = (images) => {
this.props.uploadFiles(images);
uploadFiles = async (files) => {
const file = files[0];
if (!file.fileSize | !file.fileName) {
const path = (file.path || file.uri).replace('file://', '');
const fileInfo = await RNFetchBlob.fs.stat(path);
file.fileSize = fileInfo.size;
file.fileName = fileInfo.filename;
}
if (file.fileSize > this.props.maxFileSize) {
this.props.onShowFileSizeWarning(file.fileName);
} else {
this.props.uploadFiles(files);
}
};
handleFileAttachmentOption = (action) => {
@@ -313,12 +339,19 @@ export default class AttachmentButton extends PureComponent {
this.props.blurTextBox();
const options = {
items: [{
action: () => this.handleFileAttachmentOption(this.attachFileFromCamera),
action: () => this.handleFileAttachmentOption(this.attachPhotoFromCamera),
text: {
id: t('mobile.file_upload.camera'),
defaultMessage: 'Take Photo or Video',
id: t('mobile.file_upload.camera_photo'),
defaultMessage: 'Take Photo',
},
icon: 'camera',
}, {
action: () => this.handleFileAttachmentOption(this.attachVideoFromCamera),
text: {
id: t('mobile.file_upload.camera_video'),
defaultMessage: 'Take Video',
},
icon: 'video-camera',
}, {
action: () => this.handleFileAttachmentOption(this.attachFileFromLibrary),
text: {

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

@@ -39,8 +39,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
justifyContent: 'center',
paddingLeft: 8,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
},
sectionText: {
fontSize: 12,

View File

@@ -3,7 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {SectionList} from 'react-native';
import {Platform, SectionList} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
@@ -25,8 +25,8 @@ export default class ChannelMention extends PureComponent {
currentTeamId: PropTypes.string.isRequired,
cursorPosition: PropTypes.number.isRequired,
isSearch: PropTypes.bool,
listHeight: PropTypes.number,
matchTerm: PropTypes.string,
maxListHeight: PropTypes.number,
myChannels: PropTypes.array,
myMembers: PropTypes.object,
otherChannels: PropTypes.array,
@@ -158,6 +158,10 @@ export default class ChannelMention extends PureComponent {
if (isSearch) {
const channelOrIn = mentionPart.includes('in:') ? 'in:' : 'channel:';
completedDraft = mentionPart.replace(CHANNEL_MENTION_SEARCH_REGEX, `${channelOrIn} ${mention} `);
} else if (Platform.OS === 'ios') {
// We are going to set a double ~ on iOS to prevent the auto correct from taking over and replacing it
// with the wrong value, this is a hack but I could not found another way to solve it
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~~${mention} `);
} else {
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
}
@@ -167,6 +171,14 @@ export default class ChannelMention extends PureComponent {
}
onChangeText(completedDraft, true);
if (Platform.OS === 'ios') {
// This is the second part of the hack were we replace the double ~ with just one
// after the auto correct vanished
setTimeout(() => {
onChangeText(completedDraft.replace(`~~${mention} `, `~${mention} `));
});
}
this.setState({mentionComplete: true});
};
@@ -194,7 +206,7 @@ export default class ChannelMention extends PureComponent {
};
render() {
const {isSearch, listHeight, theme} = this.props;
const {maxListHeight, theme} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
@@ -209,7 +221,7 @@ export default class ChannelMention extends PureComponent {
<SectionList
keyboardShouldPersistTaps='always'
keyExtractor={this.keyExtractor}
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
style={[style.listView, {maxHeight: maxListHeight}]}
sections={sections}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
@@ -225,8 +237,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
listView: {
backgroundColor: theme.centerChannelBg,
},
search: {
minHeight: 125,
},
};
});

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

@@ -5,6 +5,7 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
FlatList,
Platform,
Text,
TouchableOpacity,
View,
@@ -29,6 +30,7 @@ export default class EmojiSuggestion extends Component {
emojis: PropTypes.array.isRequired,
isSearch: PropTypes.bool,
fuse: PropTypes.object.isRequired,
maxListHeight: PropTypes.number,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
@@ -132,13 +134,28 @@ export default class EmojiSuggestion extends Component {
actions.addReactionToLatestPost(emoji, rootId);
onChangeText('');
} else {
let completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `:${emoji}: `);
// We are going to set a double : on iOS to prevent the auto correct from taking over and replacing it
// with the wrong value, this is a hack but I could not found another way to solve it
let completedDraft;
if (Platform.OS === 'ios') {
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `::${emoji}: `);
} else {
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `:${emoji}: `);
}
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
}
onChangeText(completedDraft);
if (Platform.OS === 'ios') {
// This is the second part of the hack were we replace the double : with just one
// after the auto correct vanished
setTimeout(() => {
onChangeText(completedDraft.replace(`::${emoji}: `, `:${emoji}: `));
});
}
}
this.setState({
@@ -171,18 +188,20 @@ export default class EmojiSuggestion extends Component {
getItemLayout = ({index}) => ({length: 40, offset: 40 * index, index})
render() {
const {maxListHeight, theme} = this.props;
if (!this.state.active) {
// If we are not in an active state return null so nothing is rendered
// other components are not blocked.
return null;
}
const style = getStyleFromTheme(this.props.theme);
const style = getStyleFromTheme(theme);
return (
<FlatList
keyboardShouldPersistTaps='always'
style={style.listView}
style={[style.listView, {maxHeight: maxListHeight}]}
extraData={this.state}
data={this.state.dataSource}
keyExtractor={this.keyExtractor}

View File

@@ -5,10 +5,12 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
FlatList,
Platform,
} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import SlashSuggestionItem from './slash_suggestion_item';
@@ -25,6 +27,7 @@ export default class SlashSuggestion extends Component {
commands: PropTypes.array,
commandsRequest: PropTypes.object.isRequired,
isSearch: PropTypes.bool,
maxListHeight: PropTypes.number,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
@@ -110,10 +113,23 @@ export default class SlashSuggestion extends Component {
completeSuggestion = (command) => {
const {onChangeText} = this.props;
const completedDraft = `/${command} `;
// We are going to set a double / on iOS to prevent the auto correct from taking over and replacing it
// with the wrong value, this is a hack but I could not found another way to solve it
let completedDraft = `/${command} `;
if (Platform.OS === 'ios') {
completedDraft = `//${command} `;
}
onChangeText(completedDraft);
if (Platform.OS === 'ios') {
// This is the second part of the hack were we replace the double / with just one
// after the auto correct vanished
setTimeout(() => {
onChangeText(completedDraft.replace(`//${command} `, `/${command} `));
});
}
this.setState({
active: false,
suggestionComplete: true,
@@ -133,22 +149,25 @@ export default class SlashSuggestion extends Component {
)
render() {
const {maxListHeight, theme} = this.props;
if (!this.state.active) {
// If we are not in an active state return null so nothing is rendered
// other components are not blocked.
return null;
}
const style = getStyleFromTheme(this.props.theme);
const style = getStyleFromTheme(theme);
return (
<FlatList
keyboardShouldPersistTaps='always'
style={style.listView}
style={[style.listView, {maxHeight: maxListHeight}]}
extraData={this.state}
data={this.state.dataSource}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
ItemSeparatorComponent={AutocompleteDivider}
pageSize={10}
initialListSize={10}
/>

View File

@@ -53,8 +53,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
justifyContent: 'center',
paddingHorizontal: 8,
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,

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

@@ -82,14 +82,8 @@ export default class Emoji extends React.PureComponent {
}
setImageUrl = (imageUrl) => {
let prefix = '';
if (Platform.OS === 'android') {
prefix = 'file://';
}
const uri = `${prefix}${imageUrl}`;
this.setState({
imageUrl: uri,
imageUrl,
});
};

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

@@ -101,12 +101,8 @@ export default class FileAttachmentList extends Component {
}
if (cache) {
let path = cache.path;
if (Platform.OS === 'android') {
path = `file://${path}`;
}
uri = path;
const prefix = Platform.OS === 'android' ? 'file://' : '';
uri = `${prefix}${cache.path}`;
}
results.push({

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,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import {intlShape} from 'react-intl';
import {Text} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
@@ -14,37 +13,23 @@ export default class Hashtag extends React.PureComponent {
linkStyle: CustomPropTypes.Style.isRequired,
onHashtagPress: PropTypes.func,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
};
static contextTypes = {
intl: intlShape,
actions: PropTypes.shape({
showSearchModal: PropTypes.func.isRequired,
}).isRequired,
};
handlePress = () => {
if (this.props.onHashtagPress) {
this.props.onHashtagPress(this.props.hashtag);
return;
}
const options = {
screen: 'Search',
animated: true,
backButtonTitle: '',
passProps: {
initialValue: '#' + this.props.hashtag,
},
navigatorStyle: {
navBarHidden: true,
screenBackgroundColor: this.props.theme.centerChannelBg,
},
};
// Close thread view, permalink view, etc
this.props.navigator.dismissAllModals();
this.props.navigator.popToRoot();
this.props.navigator.showModal(options);
this.props.actions.showSearchModal(this.props.navigator, '#' + this.props.hashtag);
};
render() {

View File

@@ -11,8 +11,13 @@ describe('Hashtag', () => {
const baseProps = {
hashtag: 'test',
linkStyle: {color: 'red'},
navigator: {},
theme: {},
navigator: {
dismissAllModals: jest.fn(),
popToRoot: jest.fn(),
},
actions: {
showSearchModal: jest.fn(),
},
};
test('should match snapshot', () => {
@@ -24,11 +29,6 @@ describe('Hashtag', () => {
test('should open hashtag search on click', () => {
const props = {
...baseProps,
navigator: {
dismissAllModals: jest.fn(),
popToRoot: jest.fn(),
showModal: jest.fn(),
},
};
const wrapper = shallow(<Hashtag {...props}/>);
@@ -37,22 +37,12 @@ describe('Hashtag', () => {
expect(props.navigator.dismissAllModals).toHaveBeenCalled();
expect(props.navigator.popToRoot).toHaveBeenCalled();
expect(props.navigator.showModal).toHaveBeenCalledWith(expect.objectContaining({
screen: 'Search',
passProps: {
initialValue: '#test',
},
}));
expect(props.actions.showSearchModal).toHaveBeenCalledWith(props.navigator, '#test');
});
test('should call onHashtagPress if provided', () => {
const props = {
...baseProps,
navigator: {
dismissAllModals: jest.fn(),
popToRoot: jest.fn(),
showModal: jest.fn(),
},
onHashtagPress: jest.fn(),
};
@@ -62,7 +52,7 @@ describe('Hashtag', () => {
expect(props.navigator.dismissAllModals).not.toBeCalled();
expect(props.navigator.popToRoot).not.toBeCalled();
expect(props.navigator.showModal).not.toBeCalled();
expect(props.actions.showSearchModal).not.toBeCalled();
expect(props.onHashtagPress).toBeCalled();
});

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {showSearchModal} from 'app/actions/views/search';
import Hashtag from './hashtag';
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showSearchModal,
}, dispatch),
};
}
export default connect(null, mapDispatchToProps)(Hashtag);

View File

@@ -223,7 +223,6 @@ export default class Markdown extends PureComponent {
linkStyle={this.props.textStyles.link}
onHashtagPress={this.props.onHashtagPress}
navigator={this.props.navigator}
theme={this.props.theme}
/>
);
}

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

@@ -187,11 +187,7 @@ export default class MarkdownImage extends React.Component {
};
setImageUrl = (imageURL) => {
let uri = imageURL;
if (Platform.OS === 'android') {
uri = `file://${imageURL}`;
}
const uri = imageURL;
this.setState({uri});
this.loadImageSize(uri);

View File

@@ -2,11 +2,13 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Text, View} from 'react-native';
import {Text, TouchableOpacity, View} from 'react-native';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import Icon from 'react-native-vector-icons/FontAwesome';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import FormattedText from 'app/components/formatted_text';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
@@ -15,7 +17,7 @@ import {ViewTypes} from 'app/constants';
export default class ActionMenu extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
doPostAction: PropTypes.func.isRequired,
selectAttachmentMenuAction: PropTypes.func.isRequired,
setMenuActionSelector: PropTypes.func.isRequired,
}).isRequired,
id: PropTypes.string.isRequired,
@@ -23,6 +25,8 @@ export default class ActionMenu extends PureComponent {
dataSource: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.object),
postId: PropTypes.string.isRequired,
selected: PropTypes.object,
teammateNameDisplay: PropTypes.string,
theme: PropTypes.object.isRequired,
navigator: PropTypes.object,
};
@@ -39,17 +43,34 @@ export default class ActionMenu extends PureComponent {
};
}
static getDerivedStateFromProps(props, state) {
if (props.selected && props.selected !== state.selected) {
return {
selectedText: props.selected.displayText,
selected: props.selected,
};
}
return null;
}
handleSelect = (selected) => {
if (!selected) {
return;
}
const {dataSource, actions, postId, id} = this.props;
const {
actions,
dataSource,
id,
postId,
teammateNameDisplay,
} = this.props;
let selectedText;
let selectedValue;
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
selectedText = selected.username;
selectedText = displayUsername(selected, teammateNameDisplay);
selectedValue = selected.id;
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
selectedText = selected.display_name;
@@ -61,11 +82,11 @@ export default class ActionMenu extends PureComponent {
this.setState({selectedText});
actions.doPostAction(postId, id, selectedValue);
}
actions.selectAttachmentMenuAction(postId, id, selectedText, selectedValue);
};
goToMenuActionSelector = preventDoubleTap(() => {
const {intl} = this.context;
const {formatMessage} = this.context.intl;
const {navigator, theme, actions, dataSource, options, name} = this.props;
actions.setMenuActionSelector(dataSource, this.handleSelect, options);
@@ -73,7 +94,7 @@ export default class ActionMenu extends PureComponent {
navigator.push({
backButtonTitle: '',
screen: 'MenuActionSelector',
title: name || intl.formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'}),
title: name || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
@@ -117,21 +138,24 @@ export default class ActionMenu extends PureComponent {
return (
<View style={style.container}>
<View style={style.input}>
<Text
style={selectedStyle}
onPress={this.goToMenuActionSelector}
numberOfLines={1}
>
{text}
</Text>
<Icon
name='chevron-down'
onPress={this.goToMenuActionSelector}
color={changeOpacity(theme.centerChannelColor, 0.5)}
style={style.icon}
/>
</View>
<TouchableOpacity
style={style.flex}
onPress={this.goToMenuActionSelector}
>
<View style={style.input}>
<Text
style={selectedStyle}
numberOfLines={1}
>
{text}
</Text>
<Icon
name='chevron-down'
color={changeOpacity(theme.centerChannelColor, 0.5)}
style={style.icon}
/>
</View>
</TouchableOpacity>
{submitted}
</View>
);
@@ -140,6 +164,9 @@ export default class ActionMenu extends PureComponent {
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
flex: {
flex: 1,
},
container: {
width: '100%',
flex: 1,

View File

@@ -4,15 +4,19 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {doPostAction} from 'mattermost-redux/actions/posts';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {setMenuActionSelector} from 'app/actions/views/post';
import {setMenuActionSelector, selectAttachmentMenuAction} from 'app/actions/views/post';
import ActionMenu from './action_menu';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const actions = state.views.post.submittedMenuActions[ownProps.postId];
const selected = actions?.[ownProps.id];
return {
selected,
teammateNameDisplay: getTeammateNameDisplaySetting(state),
theme: getTheme(state),
};
}
@@ -20,7 +24,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
doPostAction,
selectAttachmentMenuAction,
setMenuActionSelector,
}, dispatch),
};

View File

@@ -114,7 +114,7 @@ export default class MessageAttachment extends PureComponent {
});
return (
<View style={style.actionsContainer}>
<View style={style.bodyContainer}>
{content}
</View>
);
@@ -267,13 +267,7 @@ export default class MessageAttachment extends PureComponent {
}
};
setImageUrl = (imageURL) => {
let imageUri = imageURL;
if (Platform.OS === 'android') {
imageUri = `file://${imageURL}`;
}
setImageUrl = (imageUri) => {
Image.getSize(imageUri, (width, height) => {
const dimensions = calculateDimensions(height, width, this.maxImageWidth);
if (this.mounted) {
@@ -552,10 +546,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
marginTop: 5,
padding: 5,
},
actionsContainer: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
},
};
});

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

@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import {
Image,
Linking,
Platform,
Text,
TouchableOpacity,
TouchableWithoutFeedback,
@@ -19,7 +18,7 @@ import {getNearestPoint} from 'app/utils/opengraph';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const MAX_IMAGE_HEIGHT = 150;
const VIEWPORT_IMAGE_OFFSET = 88;
const VIEWPORT_IMAGE_OFFSET = 93;
const VIEWPORT_IMAGE_REPLY_OFFSET = 13;
export default class PostAttachmentOpenGraph extends PureComponent {
@@ -112,14 +111,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
};
getImageSize = (imageUrl) => {
let prefix = '';
if (Platform.OS === 'android') {
prefix = 'file://';
}
const uri = `${prefix}${imageUrl}`;
Image.getSize(uri, (width, height) => {
Image.getSize(imageUrl, (width, height) => {
const dimensions = calculateDimensions(height, width, this.getViewPostWidth());
if (this.mounted) {
@@ -127,7 +119,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
...dimensions,
originalHeight: height,
originalWidth: width,
imageUrl: uri,
imageUrl,
});
}
}, () => null);
@@ -216,7 +208,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
style={{width, height}}
>
<Image
ref='image'
style={[style.image, {width, height}]}
source={source}
resizeMode='contain'
@@ -227,7 +218,12 @@ export default class PostAttachmentOpenGraph extends PureComponent {
}
render() {
const {isReplyPost, openGraphData, theme} = this.props;
const {
isReplyPost,
link,
openGraphData,
theme,
} = this.props;
if (!openGraphData) {
return null;
@@ -235,8 +231,9 @@ export default class PostAttachmentOpenGraph extends PureComponent {
const style = getStyleSheet(theme);
return (
<View style={style.container}>
let siteName;
if (openGraphData.site_name) {
siteName = (
<View style={style.flex}>
<Text
style={style.siteTitle}
@@ -246,6 +243,13 @@ export default class PostAttachmentOpenGraph extends PureComponent {
{openGraphData.site_name}
</Text>
</View>
);
}
const title = openGraphData.title || openGraphData.url || link;
let siteTitle;
if (title) {
siteTitle = (
<View style={style.wrapper}>
<TouchableOpacity
style={style.flex}
@@ -256,10 +260,17 @@ export default class PostAttachmentOpenGraph extends PureComponent {
numberOfLines={3}
ellipsizeMode='tail'
>
{openGraphData.title || openGraphData.url}
{title}
</Text>
</TouchableOpacity>
</View>
);
}
return (
<View style={style.container}>
{siteName}
{siteTitle}
{this.renderDescription()}
{this.renderImage()}
</View>
@@ -273,6 +284,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flex: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderWidth: 1,
borderRadius: 3,
marginTop: 10,
padding: 10,
},
@@ -300,6 +312,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
imageContainer: {
alignItems: 'center',
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderWidth: 1,
borderRadius: 3,
marginTop: 5,
},
image: {
borderRadius: 3,

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

@@ -132,6 +132,7 @@ export default class PostBody extends PureComponent {
isPostEphemeral,
isSystemMessage,
managedConfig,
message,
onCopyText,
onPostDelete,
onPostEdit,
@@ -149,7 +150,7 @@ export default class PostBody extends PureComponent {
});
}
if (managedConfig.copyAndPasteProtection !== 'true') {
if (managedConfig.copyAndPasteProtection !== 'true' && message) {
actions.push({
text: formatMessage({id: 'mobile.post_info.copy_post', defaultMessage: 'Copy Post'}),
onPress: onCopyText,

View File

@@ -254,13 +254,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
const viewPortWidth = deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0);
if (link && path) {
let prefix = '';
if (Platform.OS === 'android') {
prefix = 'file://';
}
const uri = `${prefix}${path}`;
Image.getSize(uri, (width, height) => {
Image.getSize(path, (width, height) => {
if (!this.mounted) {
return;
}
@@ -282,7 +276,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
originalHeight: height,
originalWidth: width,
linkLoaded: true,
uri,
uri: path,
});
}, () => this.setState({linkLoadError: true}));
}

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

@@ -31,11 +31,12 @@ export default class PostListBase extends PureComponent {
lastViewedAt: PropTypes.number, // Used by container // eslint-disable-line no-unused-prop-types
navigator: PropTypes.object,
onLoadMoreUp: PropTypes.func,
onHashtagPress: PropTypes.func,
onPermalinkPress: PropTypes.func,
onPostPress: PropTypes.func,
onRefresh: PropTypes.func,
postIds: PropTypes.array.isRequired,
renderFooter: PropTypes.func,
renderFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
renderReplies: PropTypes.bool,
serverURL: PropTypes.string.isRequired,
shouldRenderReplyButton: PropTypes.bool,
@@ -156,6 +157,7 @@ export default class PostListBase extends PureComponent {
highlightPostId,
isSearchResult,
navigator,
onHashtagPress,
onPostPress,
renderReplies,
shouldRenderReplyButton,
@@ -168,6 +170,7 @@ export default class PostListBase extends PureComponent {
postId={postId}
previousPostId={previousPostId}
nextPostId={nextPostId}
onHashtagPress={onHashtagPress}
onPermalinkPress={this.handlePermalinkPress}
highlight={highlight}
renderReplies={renderReplies}
@@ -212,7 +215,6 @@ export default class PostListBase extends PureComponent {
passProps: {
isPermalink: true,
onClose: this.handleClosePermalink,
onPermalinkPress: this.handlePermalinkPress,
},
};

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

@@ -81,26 +81,14 @@ export default class ProgressiveImage extends PureComponent {
setImage = (uri) => {
if (this.subscribedToCache) {
let path = uri;
if (Platform.OS === 'android') {
path = `file://${uri}`;
}
this.setState({uri: path});
this.setState({uri});
}
};
setThumbnail = (thumb) => {
if (this.subscribedToCache) {
const {filename, imageUri} = this.props;
let path = thumb;
if (Platform.OS === 'android') {
path = `file://${thumb}`;
}
this.setState({thumb: path}, () => {
this.setState({thumb}, () => {
setTimeout(() => {
ImageCacheManager.cache(filename, imageUri, this.setImage);
}, 300);

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

@@ -0,0 +1,439 @@
/* eslint-disable */
// Original work: https://github.com/react-native-community/react-native-drawer-layout .
// Modified work: Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
// @flow
import React, { Component } from 'react';
import {
Animated,
Dimensions,
Keyboard,
PanResponder,
StyleSheet,
TouchableWithoutFeedback,
View,
I18nManager,
} from 'react-native';
const MIN_SWIPE_DISTANCE = 3;
const DEVICE_WIDTH = parseFloat(Dimensions.get('window').width);
const THRESHOLD = DEVICE_WIDTH / 2;
const VX_MAX = 0.1;
const IDLE = 'Idle';
const DRAGGING = 'Dragging';
const SETTLING = 'Settling';
export type PropType = {
children: any,
drawerBackgroundColor?: string,
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
drawerPosition: 'left' | 'right',
drawerWidth: number,
keyboardDismissMode?: 'none' | 'on-drag',
onDrawerClose?: Function,
onDrawerOpen?: Function,
onDrawerSlide?: Function,
onDrawerStateChanged?: Function,
renderNavigationView: () => any,
statusBarBackgroundColor?: string,
useNativeAnimations?: boolean,
};
export type StateType = {
accessibilityViewIsModal: boolean,
drawerShown: boolean,
openValue: any,
};
export type EventType = {
stopPropagation: Function,
};
export type PanResponderEventType = {
dx: number,
dy: number,
moveX: number,
moveY: number,
vx: number,
vy: number,
};
export type DrawerMovementOptionType = {
velocity?: number,
};
export default class DrawerLayout extends Component {
props: PropType;
state: StateType;
_lastOpenValue: number;
_panResponder: any;
_isClosing: boolean;
_closingAnchorValue: number;
canClose: boolean;
static defaultProps = {
drawerWidth: 0,
drawerPosition: 'left',
useNativeAnimations: false,
};
static positions = {
Left: 'left',
Right: 'right',
};
constructor(props: PropType, context: any) {
super(props, context);
this.canClose = true;
this.state = {
accessibilityViewIsModal: false,
drawerShown: false,
openValue: new Animated.Value(0),
};
}
getDrawerPosition() {
const { drawerPosition } = this.props;
const rtl = I18nManager.isRTL;
return rtl
? drawerPosition === 'left' ? 'right' : 'left' // invert it
: drawerPosition;
}
componentWillMount() {
const { openValue } = this.state;
openValue.addListener(({ value }) => {
const drawerShown = value > 0;
const accessibilityViewIsModal = drawerShown;
if (drawerShown !== this.state.drawerShown) {
this.setState({ drawerShown, accessibilityViewIsModal });
}
if (this.props.keyboardDismissMode === 'on-drag') {
Keyboard.dismiss();
}
this._lastOpenValue = value;
if (this.props.onDrawerSlide) {
this.props.onDrawerSlide({ nativeEvent: { offset: value } });
}
});
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder: this._shouldSetPanResponder,
onPanResponderGrant: this._panResponderGrant,
onPanResponderMove: this._panResponderMove,
onPanResponderTerminationRequest: () => false,
onPanResponderRelease: this._panResponderRelease,
onPanResponderTerminate: () => {},
});
}
render() {
const { accessibilityViewIsModal, drawerShown, openValue } = this.state;
const {
drawerBackgroundColor,
drawerWidth,
drawerPosition,
} = this.props;
/**
* We need to use the "original" drawer position here
* as RTL turns position left and right on its own
**/
const dynamicDrawerStyles = {
backgroundColor: drawerBackgroundColor,
width: drawerWidth,
left: drawerPosition === 'left' ? 0 : null,
right: drawerPosition === 'right' ? 0 : null,
};
/* Drawer styles */
let outputRange;
if (this.getDrawerPosition() === 'left') {
outputRange = [-drawerWidth, 0];
} else {
outputRange = [drawerWidth, 0];
}
const drawerTranslateX = openValue.interpolate({
inputRange: [0, 1],
outputRange,
extrapolate: 'clamp',
});
const animatedDrawerStyles = {
transform: [{ translateX: drawerTranslateX }],
};
/* Overlay styles */
const overlayOpacity = openValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 0.7],
extrapolate: 'clamp',
});
const animatedOverlayStyles = { opacity: overlayOpacity };
const pointerEvents = drawerShown ? 'auto' : 'none';
return (
<View
style={{ flex: 1, backgroundColor: 'transparent' }}
{...this._panResponder.panHandlers}
>
<Animated.View style={styles.main}>
{this.props.children}
</Animated.View>
<TouchableWithoutFeedback
pointerEvents={pointerEvents}
onPress={this._onOverlayClick}
>
<Animated.View
pointerEvents={pointerEvents}
style={[styles.overlay, animatedOverlayStyles]}
/>
</TouchableWithoutFeedback>
<Animated.View
accessibilityViewIsModal={accessibilityViewIsModal}
style={[
styles.drawer,
dynamicDrawerStyles,
animatedDrawerStyles,
]}
>
{this.props.renderNavigationView()}
</Animated.View>
</View>
);
}
_onOverlayClick = (e: EventType) => {
e.stopPropagation();
if (!this._isLockedClosed() && !this._isLockedOpen()) {
this.closeDrawer();
}
};
_emitStateChanged = (newState: string) => {
if (this.props.onDrawerStateChanged) {
this.props.onDrawerStateChanged(newState);
}
};
openDrawer = (options: DrawerMovementOptionType = {}) => {
this._emitStateChanged(SETTLING);
Animated.spring(this.state.openValue, {
toValue: 1,
bounciness: 0,
restSpeedThreshold: 0.1,
useNativeDriver: this.props.useNativeAnimations,
...options,
}).start(() => {
if (this.props.onDrawerOpen) {
this.props.onDrawerOpen();
}
this._emitStateChanged(IDLE);
});
};
closeDrawer = (options: DrawerMovementOptionType = {}) => {
this._emitStateChanged(SETTLING);
Animated.spring(this.state.openValue, {
toValue: 0,
bounciness: 0,
restSpeedThreshold: 1,
useNativeDriver: this.props.useNativeAnimations,
...options,
}).start(() => {
if (this.props.onDrawerClose) {
this.props.onDrawerClose();
}
this._emitStateChanged(IDLE);
});
};
_handleDrawerOpen = () => {
if (this.props.onDrawerOpen) {
this.props.onDrawerOpen();
}
};
_handleDrawerClose = () => {
if (this.props.onDrawerClose) {
this.props.onDrawerClose();
}
};
_shouldSetPanResponder = (
e: EventType,
{ moveX, dx, dy }: PanResponderEventType,
) => {
if (!dx || !dy || Math.abs(dx) < MIN_SWIPE_DISTANCE) {
return false;
}
if (this._isLockedClosed() || this._isLockedOpen() || !this.canClose) {
return false;
}
if (this.getDrawerPosition() === 'left') {
const overlayArea = DEVICE_WIDTH -
(DEVICE_WIDTH - this.props.drawerWidth);
if (this._lastOpenValue === 1) {
if (
(dx < 0 && Math.abs(dx) > Math.abs(dy) * 3) ||
moveX > overlayArea
) {
this._isClosing = true;
this._closingAnchorValue = this._getOpenValueForX(moveX);
return true;
}
} else {
if (moveX <= 35 && dx > 0) {
this._isClosing = false;
return true;
}
return false;
}
} else {
const overlayArea = DEVICE_WIDTH - this.props.drawerWidth;
if (this._lastOpenValue === 1) {
if (
(dx > 0 && Math.abs(dx) > Math.abs(dy) * 3) ||
moveX < overlayArea
) {
this._isClosing = true;
this._closingAnchorValue = this._getOpenValueForX(moveX);
return true;
}
} else {
if (moveX >= DEVICE_WIDTH - 35 && dx < 0) {
this._isClosing = false;
return true;
}
return false;
}
}
};
_panResponderGrant = () => {
this._emitStateChanged(DRAGGING);
};
_panResponderMove = (e: EventType, { moveX }: PanResponderEventType) => {
let openValue = this._getOpenValueForX(moveX);
if (this._isClosing) {
openValue = 1 - (this._closingAnchorValue - openValue);
}
if (openValue > 1) {
openValue = 1;
} else if (openValue < 0) {
openValue = 0;
}
this.state.openValue.setValue(openValue);
};
_panResponderRelease = (
e: EventType,
{ moveX, vx }: PanResponderEventType,
) => {
const previouslyOpen = this._isClosing;
const isWithinVelocityThreshold = vx < VX_MAX && vx > -VX_MAX;
if (this.getDrawerPosition() === 'left') {
if (
(vx > 0 && moveX > THRESHOLD) ||
vx >= VX_MAX ||
(isWithinVelocityThreshold &&
previouslyOpen &&
moveX > THRESHOLD)
) {
this.openDrawer({ velocity: vx });
} else if (
(vx < 0 && moveX < THRESHOLD) ||
vx < -VX_MAX ||
(isWithinVelocityThreshold && !previouslyOpen)
) {
this.closeDrawer({ velocity: vx });
} else if (previouslyOpen) {
this.openDrawer();
} else {
this.closeDrawer();
}
} else {
if (
(vx < 0 && moveX < THRESHOLD) ||
vx <= -VX_MAX ||
(isWithinVelocityThreshold &&
previouslyOpen &&
moveX < THRESHOLD)
) {
this.openDrawer({ velocity: (-1) * vx });
} else if (
(vx > 0 && moveX > THRESHOLD) ||
vx > VX_MAX ||
(isWithinVelocityThreshold && !previouslyOpen)
) {
this.closeDrawer({ velocity: (-1) * vx });
} else if (previouslyOpen) {
this.openDrawer();
} else {
this.closeDrawer();
}
}
};
_isLockedClosed = () => {
return this.props.drawerLockMode === 'locked-closed' &&
!this.state.drawerShown;
};
_isLockedOpen = () => {
return this.props.drawerLockMode === 'locked-open' &&
this.state.drawerShown;
};
_getOpenValueForX(x: number): number {
const { drawerWidth } = this.props;
if (this.getDrawerPosition() === 'left') {
return x / drawerWidth;
}
// position === 'right'
return (DEVICE_WIDTH - x) / drawerWidth;
}
}
const styles = StyleSheet.create({
drawer: {
position: 'absolute',
top: 0,
bottom: 0,
zIndex: 1001,
},
main: {
flex: 1,
zIndex: 0,
},
overlay: {
backgroundColor: '#000',
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
zIndex: 1000,
},
});

View File

@@ -44,7 +44,6 @@ exports[`ChannelItem should match snapshot 1`] = `
membersCount={1}
size={16}
status="online"
teammateDeletedAt={0}
theme={
Object {
"awayIndicator": "#ffbc42",
@@ -95,14 +94,16 @@ exports[`ChannelItem should match snapshot 1`] = `
},
]
}
/>
>
display_name
</Component>
</Component>
</Component>
</TouchableHighlight>
</AnimatedComponent>
`;
exports[`ChannelItem should match snapshot for deactivated user 1`] = `
exports[`ChannelItem should match snapshot for current user i.e currentUser (you) 1`] = `
<AnimatedComponent>
<TouchableHighlight
activeOpacity={0.85}
@@ -123,6 +124,14 @@ exports[`ChannelItem should match snapshot for deactivated user 1`] = `
]
}
>
<Component
style={
Object {
"backgroundColor": "#579eff",
"width": 5,
}
}
/>
<Component
style={
Array [
@@ -132,21 +141,23 @@ exports[`ChannelItem should match snapshot for deactivated user 1`] = `
"flexDirection": "row",
"paddingLeft": 16,
},
undefined,
Object {
"backgroundColor": "rgba(255,255,255,0.1)",
"paddingLeft": 11,
},
]
}
>
<ChannelIcon
channelId="channel_id"
hasDraft={false}
isActive={false}
isActive={true}
isArchived={false}
isInfo={false}
isUnread={true}
membersCount={1}
size={16}
status="online"
teammateDeletedAt={100}
theme={
Object {
"awayIndicator": "#ffbc42",
@@ -197,13 +208,352 @@ exports[`ChannelItem should match snapshot for deactivated user 1`] = `
},
]
}
/>
>
{displayName} (you)
</Component>
</Component>
</Component>
</TouchableHighlight>
</AnimatedComponent>
`;
exports[`ChannelItem should match snapshot for current user i.e currentUser (you) when isSearchResult 1`] = `
<AnimatedComponent>
<TouchableHighlight
activeOpacity={0.85}
delayPressOut={100}
onLongPress={[Function]}
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
<Component
style={
Array [
Object {
"flex": 1,
"flexDirection": "row",
"height": 44,
},
undefined,
]
}
>
<Component
style={
Object {
"backgroundColor": "#579eff",
"width": 5,
}
}
/>
<Component
style={
Array [
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"paddingLeft": 16,
},
Object {
"backgroundColor": "rgba(255,255,255,0.1)",
"paddingLeft": 11,
},
]
}
>
<ChannelIcon
channelId="channel_id"
hasDraft={false}
isActive={true}
isArchived={false}
isInfo={false}
isUnread={true}
membersCount={1}
size={16}
status="online"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
type="D"
/>
<Component
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"color": "rgba(255,255,255,0.4)",
"flex": 1,
"fontSize": 14,
"fontWeight": "600",
"height": "100%",
"lineHeight": 44,
"paddingRight": 40,
"textAlignVertical": "center",
},
Object {
"color": "#ffffff",
},
]
}
>
{displayName} (you)
</Component>
</Component>
</Component>
</TouchableHighlight>
</AnimatedComponent>
`;
exports[`ChannelItem should match snapshot for deactivated user and is currentChannel 1`] = `
<AnimatedComponent>
<TouchableHighlight
activeOpacity={0.85}
delayPressOut={100}
onLongPress={[Function]}
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
<Component
style={
Array [
Object {
"flex": 1,
"flexDirection": "row",
"height": 44,
},
undefined,
]
}
>
<Component
style={
Object {
"backgroundColor": "#579eff",
"width": 5,
}
}
/>
<Component
style={
Array [
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"paddingLeft": 16,
},
Object {
"backgroundColor": "rgba(255,255,255,0.1)",
"paddingLeft": 11,
},
]
}
>
<ChannelIcon
channelId="channel_id"
hasDraft={false}
isActive={true}
isArchived={true}
isInfo={false}
isUnread={true}
membersCount={1}
size={16}
status="online"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
type="D"
/>
<Component
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"color": "rgba(255,255,255,0.4)",
"flex": 1,
"fontSize": 14,
"fontWeight": "600",
"height": "100%",
"lineHeight": 44,
"paddingRight": 40,
"textAlignVertical": "center",
},
Object {
"color": "#ffffff",
},
]
}
>
display_name
</Component>
</Component>
</Component>
</TouchableHighlight>
</AnimatedComponent>
`;
exports[`ChannelItem should match snapshot for deactivated user and is searchResult 1`] = `
<AnimatedComponent>
<TouchableHighlight
activeOpacity={0.85}
delayPressOut={100}
onLongPress={[Function]}
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
<Component
style={
Array [
Object {
"flex": 1,
"flexDirection": "row",
"height": 44,
},
undefined,
]
}
>
<Component
style={
Array [
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"paddingLeft": 16,
},
undefined,
]
}
>
<ChannelIcon
channelId="channel_id"
hasDraft={false}
isActive={false}
isArchived={true}
isInfo={false}
isUnread={true}
membersCount={1}
size={16}
status="online"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
type="D"
/>
<Component
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"color": "rgba(255,255,255,0.4)",
"flex": 1,
"fontSize": 14,
"fontWeight": "600",
"height": "100%",
"lineHeight": 44,
"paddingRight": 40,
"textAlignVertical": "center",
},
Object {
"color": "#ffffff",
},
]
}
>
display_name
</Component>
</Component>
</Component>
</TouchableHighlight>
</AnimatedComponent>
`;
exports[`ChannelItem should match snapshot for deactivated user and not searchResults or currentChannel 1`] = `null`;
exports[`ChannelItem should match snapshot for no displayName 1`] = `null`;
exports[`ChannelItem should match snapshot for showUnreadForMsgs 1`] = `null`;
exports[`ChannelItem should match snapshot with draft 1`] = `
<AnimatedComponent>
<TouchableHighlight
@@ -248,7 +598,6 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
membersCount={1}
size={16}
status="online"
teammateDeletedAt={0}
theme={
Object {
"awayIndicator": "#ffbc42",
@@ -299,6 +648,137 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
},
]
}
>
display_name
</Component>
</Component>
</Component>
</TouchableHighlight>
</AnimatedComponent>
`;
exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
<AnimatedComponent>
<TouchableHighlight
activeOpacity={0.85}
delayPressOut={100}
onLongPress={[Function]}
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
<Component
style={
Array [
Object {
"flex": 1,
"flexDirection": "row",
"height": 44,
},
Object {
"opacity": 0.5,
},
]
}
>
<Component
style={
Array [
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"paddingLeft": 16,
},
undefined,
]
}
>
<ChannelIcon
channelId="channel_id"
hasDraft={false}
isActive={false}
isArchived={false}
isInfo={false}
isUnread={true}
membersCount={1}
size={16}
status="online"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
type="O"
/>
<Component
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"color": "rgba(255,255,255,0.4)",
"flex": 1,
"fontSize": 14,
"fontWeight": "600",
"height": "100%",
"lineHeight": 44,
"paddingRight": 40,
"textAlignVertical": "center",
},
Object {
"color": "#ffffff",
},
]
}
>
display_name
</Component>
<Badge
count={1}
countStyle={
Object {
"color": "#145dbf",
"fontSize": 10,
}
}
extraPaddingHorizontal={10}
minHeight={20}
minWidth={20}
onPress={[Function]}
style={
Object {
"backgroundColor": undefined,
"borderColor": "#1153ab",
"borderRadius": 10,
"borderWidth": 1,
"padding": 3,
"position": "relative",
"right": 16,
}
}
/>
</Component>
</Component>

View File

@@ -2,6 +2,8 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {General} from 'mattermost-redux/constants';
import PropTypes from 'prop-types';
import {
Animated,
@@ -22,11 +24,11 @@ const {View: AnimatedView} = Animated;
export default class ChannelItem extends PureComponent {
static propTypes = {
channelId: PropTypes.string.isRequired,
channel: PropTypes.object,
currentChannelId: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
fake: PropTypes.bool,
isChannelMuted: PropTypes.bool,
isMyUser: PropTypes.bool,
currentUserId: PropTypes.string.isRequired,
isUnread: PropTypes.bool,
hasDraft: PropTypes.bool,
mentions: PropTypes.number.isRequired,
@@ -34,12 +36,9 @@ export default class ChannelItem extends PureComponent {
onSelectChannel: PropTypes.func.isRequired,
shouldHideChannel: PropTypes.bool,
showUnreadForMsgs: PropTypes.bool.isRequired,
status: PropTypes.string,
teammateDeletedAt: PropTypes.number,
type: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
unreadMsgs: PropTypes.number.isRequired,
isArchived: PropTypes.bool.isRequired,
isSearchResult: PropTypes.bool,
};
static defaultProps = {
@@ -51,7 +50,8 @@ export default class ChannelItem extends PureComponent {
};
onPress = preventDoubleTap(() => {
const {channelId, currentChannelId, displayName, fake, onSelectChannel, type} = this.props;
const {channelId, currentChannelId, displayName, onSelectChannel, channel} = this.props;
const {type, fake} = channel;
requestAnimationFrame(() => {
onSelectChannel({id: channelId, display_name: displayName, fake, type}, currentChannelId);
});
@@ -91,21 +91,21 @@ export default class ChannelItem extends PureComponent {
currentChannelId,
displayName,
isChannelMuted,
isMyUser,
currentUserId,
isUnread,
hasDraft,
mentions,
shouldHideChannel,
status,
teammateDeletedAt,
theme,
type,
isArchived,
isSearchResult,
channel,
} = this.props;
const isArchived = channel.delete_at > 0;
// Only ever show an archived channel if it's the currently viewed channel.
// It should disappear as soon as one navigates to another channel.
if (isArchived && (currentChannelId !== channelId)) {
if (isArchived && (currentChannelId !== channelId) && !isSearchResult) {
return null;
}
@@ -120,7 +120,16 @@ export default class ChannelItem extends PureComponent {
const {intl} = this.context;
let channelDisplayName = displayName;
if (isMyUser) {
let isCurrenUser = false;
if (channel.type === General.DM_CHANNEL) {
if (isSearchResult) {
isCurrenUser = channel.id === currentUserId;
} else {
isCurrenUser = channel.teammate_id === currentUserId;
}
}
if (isCurrenUser) {
channelDisplayName = intl.formatMessage({
id: 'channel_header.directchannel.you',
defaultMessage: '{displayName} (you)',
@@ -172,10 +181,9 @@ export default class ChannelItem extends PureComponent {
hasDraft={hasDraft && channelId !== currentChannelId}
membersCount={displayName.split(',').length}
size={16}
status={status}
teammateDeletedAt={teammateDeletedAt}
status={channel.status}
theme={theme}
type={type}
type={channel.type}
isArchived={isArchived}
/>
);

View File

@@ -3,21 +3,31 @@
import React from 'react';
import {shallow} from 'enzyme';
import {TouchableHighlight} from 'react-native';
import Preferences from 'mattermost-redux/constants/preferences';
import ChannelItem from './channel_item.js';
jest.useFakeTimers();
jest.mock('react-intl');
describe('ChannelItem', () => {
const channel = {
id: 'channel_id',
delete_at: 0,
type: 'O',
fake: false,
status: 'online',
};
const baseProps = {
channelId: 'channel_id',
channel,
currentChannelId: 'current_channel_id',
displayName: 'display_name',
fake: false,
isChannelMuted: false,
isMyUser: true,
currentUserId: 'currentUser',
isUnread: true,
hasDraft: false,
mentions: 0,
@@ -25,12 +35,9 @@ describe('ChannelItem', () => {
onSelectChannel: () => {}, // eslint-disable-line no-empty-function
shouldHideChannel: false,
showUnreadForMsgs: true,
status: 'online',
teammateDeletedAt: 0,
type: 'O',
theme: Preferences.THEMES.default,
unreadMsgs: 1,
isArchived: false,
isSearchResult: false,
};
test('should match snapshot', () => {
@@ -42,16 +49,132 @@ describe('ChannelItem', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for deactivated user', () => {
test('should match snapshot with mentions and muted', () => {
const newProps = {
...baseProps,
teammateDeletedAt: 100,
type: 'D',
mentions: 1,
isChannelMuted: true,
};
const wrapper = shallow(
<ChannelItem {...newProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for deactivated user and not searchResults or currentChannel', () => {
const channelObj = {
...channel,
type: 'D',
delete_at: 123,
};
const newProps = {
...baseProps,
channel: channelObj,
};
const wrapper = shallow(
<ChannelItem {...newProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for deactivated user and is searchResult', () => {
const channelObj = {
...channel,
type: 'D',
delete_at: 123,
};
const newProps = {
...baseProps,
isSearchResult: true,
channel: channelObj,
};
const wrapper = shallow(
<ChannelItem {...newProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for deactivated user and is currentChannel', () => {
const channelObj = {
...channel,
type: 'D',
delete_at: 123,
};
const newProps = {
...baseProps,
channel: channelObj,
currentChannelId: 'channel_id',
};
const wrapper = shallow(
<ChannelItem {...newProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for no displayName', () => {
const newProps = {
...baseProps,
displayName: '',
};
const wrapper = shallow(
<ChannelItem {...newProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for current user i.e currentUser (you)', () => {
const channelObj = {
...channel,
type: 'D',
teammate_id: 'currentUser',
};
const newProps = {
...baseProps,
channel: channelObj,
currentChannelId: 'channel_id',
};
const wrapper = shallow(
<ChannelItem {...newProps}/>,
{context: {intl: {formatMessage: (intlId) => intlId.defaultMessage}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for current user i.e currentUser (you) when isSearchResult', () => {
const channelObj = {
...channel,
id: 'currentUser',
type: 'D',
teammate_id: 'somethingElse',
};
const newProps = {
...baseProps,
channel: channelObj,
currentChannelId: 'channel_id',
isSearchResult: true,
};
const wrapper = shallow(
<ChannelItem {...newProps}/>,
{context: {intl: {formatMessage: (intlId) => intlId.defaultMessage}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
@@ -66,4 +189,36 @@ describe('ChannelItem', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for showUnreadForMsgs', () => {
const wrapper = shallow(
<ChannelItem
{...baseProps}
hasDraft={true}
shouldHideChannel={true}
unreadMsgs={0}
/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('Should call onPress', () => {
const onSelectChannel = jest.fn();
const wrapper = shallow(
<ChannelItem
{...baseProps}
onSelectChannel={onSelectChannel}
/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
wrapper.find(TouchableHighlight).simulate('press');
jest.runAllTimers();
const expectedChannelParams = {id: baseProps.channelId, display_name: baseProps.displayName, fake: channel.fake, type: channel.type};
expect(onSelectChannel).toHaveBeenCalledWith(expectedChannelParams, baseProps.currentChannelId);
});
});

View File

@@ -28,23 +28,13 @@ function makeMapStateToProps() {
const currentUserId = getCurrentUserId(state);
const channelDraft = getDraftForChannel(state, channel.id);
let isMyUser = false;
let teammateDeletedAt = 0;
let displayName = channel.display_name;
let isArchived = false;
if (channel.type === General.DM_CHANNEL) {
if (ownProps.isSearchResult) {
isMyUser = channel.id === currentUserId;
teammateDeletedAt = channel.delete_at;
} else {
isMyUser = channel.teammate_id === currentUserId;
if (!ownProps.isSearchResult) {
const teammate = getUser(state, channel.teammate_id);
if (teammate && teammate.delete_at) {
teammateDeletedAt = teammate.delete_at;
}
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
displayName = displayUsername(teammate, teammateNameDisplay, false);
isArchived = channel.delete_at > 0;
}
}
@@ -75,19 +65,14 @@ function makeMapStateToProps() {
channel,
currentChannelId,
displayName,
fake: channel.fake,
isChannelMuted: isChannelMuted(member),
isMyUser,
hasDraft: Boolean(channelDraft.draft || channelDraft.files.length),
currentUserId,
hasDraft: Boolean(channelDraft.draft.trim() || channelDraft.files.length),
mentions: member ? member.mention_count : 0,
shouldHideChannel,
showUnreadForMsgs,
status: channel.status,
teammateDeletedAt,
theme: getTheme(state),
type: channel.type,
unreadMsgs,
isArchived,
};
};
}

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

@@ -10,12 +10,12 @@ import {
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import DrawerLayout from 'react-native-drawer-layout';
import {General, WebsocketEvents} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import SafeAreaView from 'app/components/safe_area_view';
import DrawerLayout from 'app/components/sidebars/drawer_layout';
import tracker from 'app/utils/time_tracker';
import {t} from 'app/utils/i18n';
@@ -50,8 +50,6 @@ export default class ChannelSidebar extends Component {
intl: intlShape.isRequired,
};
swiperIndex = 1;
constructor(props) {
super(props);
@@ -60,9 +58,9 @@ export default class ChannelSidebar extends Component {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.swiperIndex = 1;
this.state = {
show: false,
lockMode: 'unlocked',
openDrawerOffset,
drawerOpened: false,
};
@@ -102,7 +100,7 @@ export default class ChannelSidebar extends Component {
return nextProps.currentTeamId !== currentTeamId ||
nextProps.isLandscape !== isLandscape || nextProps.deviceWidth !== deviceWidth ||
nextProps.teamsCount !== teamsCount || this.state.lockMode !== nextState.lockMode;
nextProps.teamsCount !== teamsCount;
}
componentWillUnmount() {
@@ -257,10 +255,13 @@ export default class ChannelSidebar extends Component {
onPageSelected = (index) => {
this.swiperIndex = index;
if (this.swiperIndex === 0) {
this.setState({lockMode: 'locked-open'});
} else {
this.setState({lockMode: 'unlocked'});
if (this.refs.drawer) {
if (this.swiperIndex === 0) {
this.refs.drawer.canClose = false;
} else {
this.refs.drawer.canClose = true;
}
}
};
@@ -272,10 +273,16 @@ export default class ChannelSidebar extends Component {
if (isLandscape || isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
if (this.refs.drawer) {
this.refs.drawer.canClose = true;
}
this.setState({openDrawerOffset});
};
onSearchStart = () => {
if (this.refs.drawer) {
this.refs.drawer.canClose = false;
}
this.setState({openDrawerOffset: 0});
};
@@ -372,16 +379,16 @@ export default class ChannelSidebar extends Component {
render() {
const {children, deviceWidth} = this.props;
const {lockMode, openDrawerOffset} = this.state;
const {openDrawerOffset} = this.state;
return (
<DrawerLayout
drawerLockMode={lockMode}
ref='drawer'
renderNavigationView={this.renderNavigationView}
onDrawerClose={this.handleDrawerClose}
onDrawerOpen={this.handleDrawerOpen}
drawerWidth={deviceWidth - openDrawerOffset}
useNativeAnimations={true}
>
{children}
</DrawerLayout>

View File

@@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getCurrentTeamId, getMySortedTeamIds} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTeamId, getMySortedTeamIds, getJoinableTeamIds} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {handleTeamChange} from 'app/actions/views/select_team';
@@ -20,6 +20,7 @@ function mapStateToProps(state) {
return {
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
hasOtherJoinableTeams: getJoinableTeamIds(state).length > 0,
teamIds: getMySortedTeamIds(state, locale),
theme: getTheme(state),
};

View File

@@ -36,6 +36,7 @@ export default class TeamsList extends PureComponent {
closeChannelDrawer: PropTypes.func.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
hasOtherJoinableTeams: PropTypes.bool,
navigator: PropTypes.object.isRequired,
teamIds: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired,
@@ -109,22 +110,25 @@ export default class TeamsList extends PureComponent {
};
render() {
const {teamIds, theme} = this.props;
const {hasOtherJoinableTeams, teamIds, theme} = this.props;
const styles = getStyleSheet(theme);
const moreAction = (
<TouchableHighlight
style={styles.moreActionContainer}
onPress={this.goToSelectTeam}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<Text
style={styles.moreAction}
let moreAction;
if (hasOtherJoinableTeams) {
moreAction = (
<TouchableHighlight
style={styles.moreActionContainer}
onPress={this.goToSelectTeam}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
{'+'}
</Text>
</TouchableHighlight>
);
<Text
style={styles.moreAction}
>
{'+'}
</Text>
</TouchableHighlight>
);
}
return (
<View style={styles.container}>

View File

@@ -106,13 +106,14 @@ export default class TeamsListItem extends React.PureComponent {
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
teamWrapper: {
marginTop: 20,
marginTop: 10,
},
teamContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
marginHorizontal: 16,
paddingVertical: 10,
},
teamNameContainer: {
flex: 1,

View File

@@ -11,13 +11,13 @@ import {
ScrollView,
View,
} from 'react-native';
import DrawerLayout from 'react-native-drawer-layout';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {General} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import SafeAreaView from 'app/components/safe_area_view';
import DrawerLayout from 'app/components/sidebars/drawer_layout';
import UserStatus from 'app/components/user_status';
import {NavigationTypes} from 'app/constants';
import {confirmOutOfOfficeDisabled} from 'app/utils/status';
@@ -358,6 +358,7 @@ export default class SettingsDrawer extends PureComponent {
onDrawerOpen={this.handleDrawerOpen}
drawerPosition='right'
drawerWidth={deviceWidth - DRAWER_INITIAL_OFFSET}
useNativeAnimations={true}
>
{children}
</DrawerLayout>

View File

@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import {
Image,
Platform,
Text,
View,
} from 'react-native';
@@ -50,12 +49,7 @@ export default class TeamIcon extends React.PureComponent {
}
setImageURL = (teamIcon) => {
let prefix = '';
if (Platform.OS === 'android') {
prefix = 'file://';
}
this.setState({teamIcon: `${prefix}${teamIcon}`});
this.setState({teamIcon});
};
render() {

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

@@ -6,6 +6,7 @@ import RNFetchBlob from 'rn-fetch-blob';
import urlParse from 'url-parse';
import {Client4} from 'mattermost-redux/client';
import {ClientError} from 'mattermost-redux/client/client4';
import mattermostBucket from 'app/mattermost_bucket';
import LocalConfig from 'assets/config';
@@ -28,10 +29,10 @@ const handleRedirectProtocol = (url, response) => {
Client4.doFetchWithResponse = async (url, options) => {
if (!Client4.online) {
throw {
throw new ClientError(Client4.getUrl(), {
message: 'no internet connection',
url,
};
});
}
let response;
@@ -48,26 +49,27 @@ Client4.doFetchWithResponse = async (url, options) => {
data = await response.json();
} catch (err) {
if (response && response.resp && response.resp.data && response.resp.data.includes('SSL certificate')) {
throw {
throw new ClientError(Client4.getUrl(), {
message: 'You need to use a valid client certificate in order to connect to this Mattermost server',
status_code: 401,
url,
};
});
}
throw {
throw new ClientError(Client4.getUrl(), {
message: 'Received invalid response from the server.',
intl: {
id: t('mobile.request.invalid_response'),
defaultMessage: 'Received invalid response from the server.',
},
};
url,
});
}
if (headers[HEADER_X_CLUSTER_ID] || headers[HEADER_X_CLUSTER_ID.toLowerCase()]) {
const clusterId = headers[HEADER_X_CLUSTER_ID] || headers[HEADER_X_CLUSTER_ID.toLowerCase()];
if (clusterId && this.clusterId !== clusterId) {
this.clusterId = clusterId;
if (clusterId && Client4.clusterId !== clusterId) {
Client4.clusterId = clusterId;
}
}
@@ -95,12 +97,12 @@ Client4.doFetchWithResponse = async (url, options) => {
console.error(msg); // eslint-disable-line no-console
}
throw {
throw new ClientError(Client4.getUrl(), {
message: msg,
server_error_id: data.id,
status_code: data.status_code,
url,
};
});
};
const initFetchConfig = async () => {

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,33 @@ function menuAction(state = {}, action) {
}
}
function submittedMenuActions(state = {}, action) {
switch (action.type) {
case ViewTypes.SUBMIT_ATTACHMENT_MENU_ACTION: {
const nextState = {...state};
if (nextState[action.postId]) {
nextState[action.postId] = {
...nextState[action.postId],
...action.data,
};
} else {
nextState[action.postId] = action.data;
}
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
}
export default combineReducers({
menuAction,
// Currently selected menu action
selectedMenuAction,
// Submitted menu actions per post
submittedMenuActions,
});

View File

@@ -9,6 +9,7 @@ import {
AppState,
Dimensions,
Platform,
StyleSheet,
View,
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
@@ -26,7 +27,6 @@ import StatusBar from 'app/components/status_bar';
import {ViewTypes} from 'app/constants';
import mattermostBucket from 'app/mattermost_bucket';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import PostTextbox from 'app/components/post_textbox';
import networkConnectionListener from 'app/utils/network';
import tracker from 'app/utils/time_tracker';
@@ -321,8 +321,6 @@ export default class Channel extends PureComponent {
theme,
} = this.props;
const style = getStyleFromTheme(theme);
if (!currentChannelId) {
if (channelsRequestFailed) {
const PostListRetry = require('app/components/post_list_retry').default;
@@ -337,7 +335,7 @@ export default class Channel extends PureComponent {
const Loading = require('app/components/channel_loader').default;
return (
<SafeAreaView navigator={navigator}>
<View style={style.loading}>
<View style={style.flex}>
<EmptyToolbar
theme={theme}
isLandscape={this.props.isLandscape}
@@ -372,7 +370,7 @@ export default class Channel extends PureComponent {
onPress={this.goToChannelInfo}
/>
<KeyboardLayout>
<View style={style.postList}>
<View style={style.flex}>
<ChannelPostList navigator={navigator}/>
</View>
<PostTextbox
@@ -392,19 +390,13 @@ export default class Channel extends PureComponent {
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
postList: {
flex: 1,
},
loading: {
backgroundColor: theme.centerChannelBg,
flex: 1,
},
channelLoader: {
position: 'absolute',
width: '100%',
flex: 1,
},
};
const style = StyleSheet.create({
flex: {
flex: 1,
},
channelLoader: {
position: 'absolute',
width: '100%',
flex: 1,
},
});

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