Compare commits

...

55 Commits

Author SHA1 Message Date
Mattermost Build
9d520d1909 Bump app build number to 239 (#3422) 2019-10-15 09:55:10 -04:00
Michael Kochell
d15420ae54 [MM-19307] Make LHS channel item display name not overlap with mention count (#3417)
* make channel item display name more narrow, so it does not run into the mention count

* Update snapshot
2019-10-15 09:53:51 -04:00
Mattermost Build
08fe8e529b Automated cherry pick of #3402 (#3420)
* MM-19286 Disable Haptic Feedback

Added a check for iOS version <=9 and for iPhones 5/6/SE versions that don't support haptic feedback.

* MM-19286 Fixed the Fix

Misread the repo for the haptic feedback so fixing the fix.

* MM-19286 Removed line in device.js

Removed line in device.js
2019-10-15 10:35:44 +03:00
Mattermost Build
44669d93bb Patch react-native-image-picker (#3419) 2019-10-15 00:30:06 -07:00
Elias Nahum
cfc65c0250 translations PR 20191014 (#3418) 2019-10-15 05:27:19 +03:00
Mattermost Build
9eb1aa1ad0 disable add reaction for read only channels (#3407) 2019-10-11 08:47:05 -06:00
Mattermost Build
5d923f3c4a Not show the guest badge for system messages (#3406) 2019-10-11 15:27:48 +02:00
Miguel Alatzar
01eb3ce8b6 Manual cherry pick of #3372 (#3400)
* Fixing mention text color (#3372)

* Fixing mention text color

* Updating snapshots

* Update package-lock.json

* Adding highlight link color to other highlight types

* Fix snapshot test
2019-10-09 16:14:56 -04:00
Mattermost Build
0fa96be8ee Bump app build number to 238 (#3399) 2019-10-09 16:09:30 -04:00
Mattermost Build
c983caf583 Bump app build number (#3396) 2019-10-09 14:36:07 -04:00
Mattermost Build
95f321c518 MM-19180 - Updating spacing for mentiosn component (#3394) 2019-10-09 11:33:33 -04:00
Jesús Espino
5ec12c8784 Fixing DMs guest label behavior (#3383) (#3391)
* Fixing DMs guest label behavior

* Adding tests

* Addressing PR review comments
2019-10-09 12:02:41 +02:00
Mattermost Build
993143b0f8 Pass fileInfoContainer style directly to Touchable (#3390) 2019-10-08 19:00:10 -07:00
Elias Nahum
53c4df74c6 translations PR 20191008 (#3387) 2019-10-08 18:25:06 +03:00
Mattermost Build
fdc894c00f Automated cherry pick of #3368 (#3388)
* MM-19185 Fix tab for UnreadIndicator on Android

* update snapshots
2019-10-08 18:21:56 +03:00
Mattermost Build
04bedbc954 Fix running mm-i18n that crashes on conditional operators (#3386) 2019-10-08 15:15:37 +03:00
Mattermost Build
9dd36bf15e Automated cherry pick of #3306 (#3378)
* Update NOTICE.txt

* Update NOTICE.txt
2019-10-07 17:51:26 -04:00
Miguel Alatzar
9d28eb043c Update total to reflect number of children (#3367) (#3369) 2019-10-04 15:20:37 -07:00
Mattermost Build
188bfecf17 Bump app build number to 236 (#3366) 2019-10-02 21:47:59 +03:00
Mattermost Build
3eb8d3857b Set Sidebar channel item display name opacity as 0.6 (same as webapp) (#3364) 2019-10-02 21:34:58 +03:00
Mattermost Build
e9e1dc0541 Automated cherry pick of #3343 (#3363)
* MM-18999 Fix search in: modifier to show the appropriate badge when needed for DM/GM

* feedback review

* fix eslint

* fix eslint disable rule
2019-10-02 21:34:41 +03:00
Mattermost Build
c55dcaf598 Automated cherry pick of #3353 (#3361)
* Use updated react-native-keyboard-tracking-view

* Use updated react-native-device-info

* Use updated react-native-device-info and add new pod dependency
2019-10-02 08:38:39 -07:00
Mattermost Build
d5dd4380d9 MM-18997 Fix More unread overlay preventing interactions (#3354) 2019-10-01 22:55:55 +03:00
Mattermost Build
486917d692 Automated cherry pick of #3344 (#3345)
* Fix channel navbar title displayName variable

* Fix snapshots
2019-09-28 19:44:52 +03:00
Mattermost Build
53657536fc Automated cherry pick of #3332 (#3340)
* MM-18603 Fix post header to prevent overlaps

* Export BotTag and GuestTag
2019-09-28 10:01:30 +03:00
Mattermost Build
cd12480577 Automated cherry pick of #3335 (#3341)
* MM-18464

Updated Dialog Items to support isLandscape for SafeArea View

* MM-18464 Updated SafeAreaView

Updated Autoselector Component

* MM-18464 Resolved Issues

Resolved issues for MM-18464

* MM-18464 Resolved Snapshot

Resolved snapshots
2019-09-27 14:47:21 -07:00
Mattermost Build
643d45b33c Bump app build number to 235 (#3339) 2019-09-27 15:56:58 -04:00
Mattermost Build
d3b5281ecb MM-18176 Fix network indicator stock after re-connect (#3336) 2019-09-27 21:02:51 +03:00
Mattermost Build
d5ea75171c Revert long post as ScrollView instead of View (#3334) 2019-09-27 01:35:49 +03:00
Mattermost Build
73d20fdcdf Automated cherry pick of #3293 (#3330)
* Add (you) suffix to self DM channel title

* Use FormattedText component
2019-09-26 07:54:45 -07:00
Miguel Alatzar
2eb723a6dc Manual cherry pick of #3298 (#3328) 2019-09-25 15:53:05 -07:00
Elias Nahum
5fafe376fa MM-18236 Prevent the post menu from triggering when using the back gesture (#3319)
* MM-18236 Prevent the post menu from triggering when using the back gesture in the thread screen

* Update snapshots

* Update app/components/touchable_with_feedback/touchable_with_feedback.ios.js

Co-Authored-By: Miguel Alatzar <migbot@users.noreply.github.com>

* Update app/components/touchable_with_feedback/touchable_with_feedback.ios.js

Co-Authored-By: Miguel Alatzar <migbot@users.noreply.github.com>

* Update app/components/post/post.js

Co-Authored-By: Miguel Alatzar <migbot@users.noreply.github.com>

* Update app/components/touchable_with_feedback/touchable_with_feedback.ios.js

* Fix eslint
2019-09-26 00:22:37 +03:00
Mattermost Build
0818489b47 MM-18740 Sync app badge number when opening a push notification (#3324) 2019-09-26 00:08:30 +03:00
Dean Whillier
14593339b3 Bump app build number to 234 (#3327) 2019-09-25 16:10:24 -04:00
Harrison Healey
80fad8c11c Switch mattermost-redux to release branch 2019-09-25 15:15:38 -04:00
Harrison Healey
f2e06aa304 MM-16399 Clear current channel when leaving a team (#3288)
* MM-16399 Clear current channel when leaving a team

* Update mattermost-redux
2019-09-25 14:37:02 -04:00
Mattermost Build
41bcc75df9 MM-18542 Fix file attachment scroll on tablets, MM-18732 Slow scroll in landscape & MM-18836 settings sidebar width (#3323) 2019-09-25 20:29:56 +03:00
Mattermost Build
03692f1975 Fix paste files with multiple instances of post textbox (#3320) 2019-09-25 17:23:20 +08:00
Elias Nahum
8927e5921a translations PR 20190923 (#3301) 2019-09-25 11:12:09 +03:00
Mattermost Build
bd4a119c05 Automated cherry pick of #3310 (#3318)
* Reset moment local on logout

* Update app/selectors/i18n.js

Co-Authored-By: Elias Nahum <nahumhbl@gmail.com>
2019-09-25 16:06:06 +08:00
Mattermost Build
6b4b4ce75f Null check on current (#3316) 2019-09-24 17:33:43 -07:00
Mattermost Build
cdc020fc9c add circleci (#3314) 2019-09-24 17:24:21 -07:00
Mattermost Build
5f2d840f27 Automated cherry pick of #3289 (#3304)
* Properly determine if channel is archived

* Remove check on ownProps.channelIsArchived
2019-09-24 12:35:47 -07:00
Mattermost Build
dca0d5e75b Bump app build number to 233 (#3308) 2019-09-24 14:57:32 -04:00
Mattermost Build
4dbdf42ebd MM-18758 Fix channel info row to be toggleable or with an chevron (#3303) 2019-09-24 19:40:25 +03:00
Mattermost Build
cf2262dbc1 MM-18752 Rename constant to handle iPhone X and new iPhone 11 insets (#3300) 2019-09-23 22:19:26 +02:00
Mattermost Build
478bf42b62 Call scrollToIndex only when ref is set and index is in range (#3286) 2019-09-19 12:41:38 -07:00
Mattermost Build
aada9efb2b Bump app build number to 232 (#3278) 2019-09-18 20:52:26 -04:00
Mattermost Build
4ada33b50d Updated Info.plist with new bluetooth usage description key (#3276) 2019-09-18 20:50:24 -04:00
Devin Binnie
b8540b42dd Bump app version number to 1.24.0 (#3274)
* Bump app version number to 1.24.0

* Update build.gradle
2019-09-18 13:28:17 -04:00
Devin Binnie
9bbbf67cc8 Bump app build number to 231 (#3272) 2019-09-18 13:26:12 -04:00
Mattermost Build
54f403c354 Update en.json (#3273) 2019-09-18 10:24:47 -07:00
Mattermost Build
80282c6df1 Ensure onAppStateChange runs only after GlobalEventHandler is configured (#3271) 2019-09-18 10:04:29 -07:00
Mattermost Build
f99d260628 Force drawer to open to channel menu when new teams are added (#3269) 2019-09-18 09:58:44 -04:00
Patrick Kang
2bd67deeea Adds support for 'radio' type in interactive dialogs (#3212) 2019-09-18 15:37:38 +02:00
344 changed files with 8876 additions and 8842 deletions

23
.circleci/config.yml Normal file
View File

@@ -0,0 +1,23 @@
version: 2.1
jobs:
test:
working_directory: ~/mattermost-mobile
docker:
- image: circleci/node:10
steps:
- checkout
- run: |
echo assets/base/config.json
cat assets/base/config.json
# Avoid installing pods
touch .podinstall
# Run tests
make test || exit 1
workflows:
version: 2
pr-test:
jobs:
- test

View File

@@ -95,7 +95,7 @@ post-install:
fi
@sed -i'' -e 's|transform: \[{scaleY: -1}\],|...Platform.select({android: {transform: \[{perspective: 1}, {scaleY: -1}\]}, ios: {transform: \[{scaleY: -1}\]}}),|g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
@./node_modules/.bin/patch-package --patch-dir=native_modules
@./node_modules/.bin/patch-package
start: | pre-run ## Starts the React Native packager server
$(call start_packager)

View File

@@ -1245,6 +1245,19 @@ SOFTWARE.
---
## react-native-android-open-settings
This product contains 'react-native-android-open-settings' by Leonardo Velasquez.
Open Android settings from your React Native app
* HOMEPAGE:
* https://github.com/levelasquez/react-native-android-open-settings
* LICENSE: ISC
---
## react-native-animatable
This product contains 'react-native-animatable' by Joel Arvidsson.
@@ -1600,6 +1613,41 @@ SOFTWARE.
---
## react-native-haptic-feedback
This product contains 'react-native-haptic-feedback' by Milk and Cookies.
React-Native Haptic Feedback for iOS with a similar behaviour for Android
* HOMEPAGE:
* https://github.com/milk-and-cookies-io/react-native-haptic-feedback
* LICENSE: MIT
MIT License
Copyright (c) 2018 Michael Kuczera
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-image-gallery
This product contains a modified version of 'react-native-image-gallery' by Archriss.

View File

@@ -123,8 +123,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5"
versionCode 230
versionName "1.23.0"
versionCode 239
versionName "1.24.0"
multiDexEnabled = true
ndk {
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'

View File

@@ -1,394 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform} from 'react-native';
import {Navigation} from 'react-native-navigation';
import merge from 'deepmerge';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import EphemeralStore from 'app/store/ephemeral_store';
export function resetToChannel(passProps = {}) {
return (dispatch, getState) => {
const theme = getTheme(getState());
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Channel',
passProps,
options: {
layout: {
backgroundColor: 'transparent',
},
statusBar: {
visible: true,
},
topBar: {
visible: false,
height: 0,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
},
},
},
}],
},
},
});
};
}
export function resetToSelectServer(allowOtherServers) {
return (dispatch, getState) => {
const theme = getTheme(getState());
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'SelectServer',
passProps: {
allowOtherServers,
},
options: {
statusBar: {
visible: true,
},
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
visible: false,
height: 0,
},
},
},
}],
},
},
});
};
}
export function resetToTeams(name, title, passProps = {}, options = {}) {
return (dispatch, getState) => {
const theme = getTheme(getState());
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
visible: true,
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
},
};
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
},
});
};
}
export function goToScreen(name, title, passProps = {}, options = {}) {
return (dispatch, getState) => {
const state = getState();
const componentId = EphemeralStore.getNavigationTopComponentId();
const theme = getTheme(state);
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
},
};
Navigation.push(componentId, {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
};
}
export function popTopScreen(screenId) {
return () => {
if (screenId) {
Navigation.pop(screenId);
} else {
const componentId = EphemeralStore.getNavigationTopComponentId();
Navigation.pop(componentId);
}
};
}
export function popToRoot() {
return () => {
const componentId = EphemeralStore.getNavigationTopComponentId();
Navigation.popToRoot(componentId).catch(() => {
// RNN returns a promise rejection if there are no screens
// atop the root screen to pop. We'll do nothing in this
// case but we will catch the rejection here so that the
// caller doesn't have to.
});
};
}
export function showModal(name, title, passProps = {}, options = {}) {
return (dispatch, getState) => {
const theme = getTheme(getState());
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
leftButtonColor: theme.sidebarHeaderTextColor,
rightButtonColor: theme.sidebarHeaderTextColor,
},
};
Navigation.showModal({
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
});
};
}
export function showModalOverCurrentContext(name, passProps = {}, options = {}) {
return (dispatch) => {
const title = '';
const animationsEnabled = (Platform.OS === 'android').toString();
const defaultOptions = {
modalPresentationStyle: 'overCurrentContext',
layout: {
backgroundColor: 'transparent',
},
topBar: {
visible: false,
height: 0,
},
animations: {
showModal: {
enabled: animationsEnabled,
alpha: {
from: 0,
to: 1,
duration: 250,
},
},
dismissModal: {
enabled: animationsEnabled,
alpha: {
from: 1,
to: 0,
duration: 250,
},
},
},
};
const mergeOptions = merge(defaultOptions, options);
dispatch(showModal(name, title, passProps, mergeOptions));
};
}
export function showSearchModal(initialValue = '') {
return (dispatch) => {
const name = 'Search';
const title = '';
const passProps = {initialValue};
const options = {
topBar: {
visible: false,
height: 0,
},
};
dispatch(showModal(name, title, passProps, options));
};
}
export function dismissModal(options = {}) {
return () => {
const componentId = EphemeralStore.getNavigationTopComponentId();
Navigation.dismissModal(componentId, options).catch(() => {
// RNN returns a promise rejection if there is no modal to
// dismiss. We'll do nothing in this case but we will catch
// the rejection here so that the caller doesn't have to.
});
};
}
export function dismissAllModals(options = {}) {
return () => {
Navigation.dismissAllModals(options).catch(() => {
// RNN returns a promise rejection if there are no modals to
// dismiss. We'll do nothing in this case but we will catch
// the rejection here so that the caller doesn't have to.
});
};
}
export function peek(name, passProps = {}, options = {}) {
return () => {
const componentId = EphemeralStore.getNavigationTopComponentId();
const defaultOptions = {
preview: {
commit: false,
},
};
Navigation.push(componentId, {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
};
}
export function setButtons(componentId, buttons = {leftButtons: [], rightButtons: []}) {
return () => {
Navigation.mergeOptions(componentId, {
topBar: {
...buttons,
},
});
};
}
export function showOverlay(name, passProps, options = {}) {
return () => {
const defaultOptions = {
overlay: {
interceptTouchOutside: false,
},
};
Navigation.showOverlay({
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
};
}
export function dismissOverlay(componentId) {
return () => {
return Navigation.dismissOverlay(componentId).catch(() => {
// RNN returns a promise rejection if there is no modal with
// this componentId to dismiss. We'll do nothing in this case
// but we will catch the rejection here so that the caller
// doesn't have to.
});
};
}
export function applyTheme(componentId, skipBackButtonStyle = false) {
return (dispatch, getState) => {
const theme = getTheme(getState());
let backButton = {
color: theme.sidebarHeaderTextColor,
};
if (skipBackButtonStyle && Platform.OS === 'android') {
backButton = null;
}
Navigation.mergeOptions(componentId, {
topBar: {
backButton,
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
},
});
};
}

View File

@@ -0,0 +1,352 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform} from 'react-native';
import {Navigation} from 'react-native-navigation';
import merge from 'deepmerge';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import store from 'app/store';
import EphemeralStore from 'app/store/ephemeral_store';
function getThemeFromState() {
const state = store.getState();
return getTheme(state);
}
export function resetToChannel(passProps = {}) {
const theme = getThemeFromState();
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Channel',
passProps,
options: {
layout: {
backgroundColor: 'transparent',
},
statusBar: {
visible: true,
},
topBar: {
visible: false,
height: 0,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
},
},
},
}],
},
},
});
}
export function resetToSelectServer(allowOtherServers) {
const theme = getThemeFromState();
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'SelectServer',
passProps: {
allowOtherServers,
},
options: {
statusBar: {
visible: true,
},
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
visible: false,
height: 0,
},
},
},
}],
},
},
});
}
export function resetToTeams(name, title, passProps = {}, options = {}) {
const theme = getThemeFromState();
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
visible: true,
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
},
};
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
},
});
}
export function goToScreen(name, title, passProps = {}, options = {}) {
const theme = getThemeFromState();
const componentId = EphemeralStore.getNavigationTopComponentId();
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
},
};
Navigation.push(componentId, {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
}
export function popTopScreen(screenId) {
if (screenId) {
Navigation.pop(screenId);
} else {
const componentId = EphemeralStore.getNavigationTopComponentId();
Navigation.pop(componentId);
}
}
export async function popToRoot() {
const componentId = EphemeralStore.getNavigationTopComponentId();
try {
await Navigation.popToRoot(componentId);
} catch (error) {
// RNN returns a promise rejection if there are no screens
// atop the root screen to pop. We'll do nothing in this case.
}
}
export function showModal(name, title, passProps = {}, options = {}) {
const theme = getThemeFromState();
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
leftButtonColor: theme.sidebarHeaderTextColor,
rightButtonColor: theme.sidebarHeaderTextColor,
},
};
Navigation.showModal({
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
});
}
export function showModalOverCurrentContext(name, passProps = {}, options = {}) {
const title = '';
const animationsEnabled = (Platform.OS === 'android').toString();
const defaultOptions = {
modalPresentationStyle: 'overCurrentContext',
layout: {
backgroundColor: 'transparent',
},
topBar: {
visible: false,
height: 0,
},
animations: {
showModal: {
enabled: animationsEnabled,
alpha: {
from: 0,
to: 1,
duration: 250,
},
},
dismissModal: {
enabled: animationsEnabled,
alpha: {
from: 1,
to: 0,
duration: 250,
},
},
},
};
const mergeOptions = merge(defaultOptions, options);
showModal(name, title, passProps, mergeOptions);
}
export function showSearchModal(initialValue = '') {
const name = 'Search';
const title = '';
const passProps = {initialValue};
const options = {
topBar: {
visible: false,
height: 0,
},
};
showModal(name, title, passProps, options);
}
export async function dismissModal(options = {}) {
const componentId = EphemeralStore.getNavigationTopComponentId();
try {
await Navigation.dismissModal(componentId, options);
} catch (error) {
// RNN returns a promise rejection if there is no modal to
// dismiss. We'll do nothing in this case.
}
}
export async function dismissAllModals(options = {}) {
try {
await Navigation.dismissAllModals(options);
} catch (error) {
// RNN returns a promise rejection if there are no modals to
// dismiss. We'll do nothing in this case.
}
}
export function peek(name, passProps = {}, options = {}) {
const componentId = EphemeralStore.getNavigationTopComponentId();
const defaultOptions = {
preview: {
commit: false,
},
};
Navigation.push(componentId, {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
}
export function setButtons(componentId, buttons = {leftButtons: [], rightButtons: []}) {
const options = {
topBar: {
...buttons,
},
};
mergeNavigationOptions(componentId, options);
}
export function mergeNavigationOptions(componentId, options) {
Navigation.mergeOptions(componentId, options);
}
export function showOverlay(name, passProps, options = {}) {
const defaultOptions = {
overlay: {
interceptTouchOutside: false,
},
};
Navigation.showOverlay({
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
}
export async function dismissOverlay(componentId) {
try {
await Navigation.dismissOverlay(componentId);
} catch (error) {
// RNN returns a promise rejection if there is no modal with
// this componentId to dismiss. We'll do nothing in this case.
}
}

View File

@@ -0,0 +1,473 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform} from 'react-native';
import {Navigation} from 'react-native-navigation';
import merge from 'deepmerge';
import Preferences from 'mattermost-redux/constants/preferences';
import EphemeralStore from 'app/store/ephemeral_store';
import * as NavigationActions from 'app/actions/navigation';
jest.unmock('app/actions/navigation');
jest.mock('app/store/ephemeral_store', () => ({
getNavigationTopComponentId: jest.fn(),
}));
describe('app/actions/navigation', () => {
const topComponentId = 'top-component-id';
const name = 'name';
const title = 'title';
const theme = Preferences.THEMES.default;
const passProps = {
testProp: 'prop',
};
const options = {
testOption: 'test',
};
EphemeralStore.getNavigationTopComponentId.mockReturnValue(topComponentId);
test('resetToChannel should call Navigation.setRoot', () => {
const setRoot = jest.spyOn(Navigation, 'setRoot');
const expectedLayout = {
root: {
stack: {
children: [{
component: {
name: 'Channel',
passProps,
options: {
layout: {
backgroundColor: 'transparent',
},
statusBar: {
visible: true,
},
topBar: {
visible: false,
height: 0,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
},
},
},
}],
},
},
};
NavigationActions.resetToChannel(passProps);
expect(setRoot).toHaveBeenCalledWith(expectedLayout);
});
test('resetToSelectServer should call Navigation.setRoot', () => {
const setRoot = jest.spyOn(Navigation, 'setRoot');
const allowOtherServers = false;
const expectedLayout = {
root: {
stack: {
children: [{
component: {
name: 'SelectServer',
passProps: {
allowOtherServers,
},
options: {
statusBar: {
visible: true,
},
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
visible: false,
height: 0,
},
},
},
}],
},
},
};
NavigationActions.resetToSelectServer(allowOtherServers);
expect(setRoot).toHaveBeenCalledWith(expectedLayout);
});
test('resetToTeams should call Navigation.setRoot', () => {
const setRoot = jest.spyOn(Navigation, 'setRoot');
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
visible: true,
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
},
};
const expectedLayout = {
root: {
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
},
};
NavigationActions.resetToTeams(name, title, passProps, options);
expect(setRoot).toHaveBeenCalledWith(expectedLayout);
});
test('goToScreen should call Navigation.push', () => {
const push = jest.spyOn(Navigation, 'push');
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
},
};
const expectedLayout = {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
};
NavigationActions.goToScreen(name, title, passProps, options);
expect(push).toHaveBeenCalledWith(topComponentId, expectedLayout);
});
test('popTopScreen should call Navigation.pop', () => {
const pop = jest.spyOn(Navigation, 'pop');
NavigationActions.popTopScreen();
expect(pop).toHaveBeenCalledWith(topComponentId);
const otherComponentId = `other-${topComponentId}`;
NavigationActions.popTopScreen(otherComponentId);
expect(pop).toHaveBeenCalledWith(otherComponentId);
});
test('popToRoot should call Navigation.popToRoot', async () => {
const popToRoot = jest.spyOn(Navigation, 'popToRoot');
await NavigationActions.popToRoot();
expect(popToRoot).toHaveBeenCalledWith(topComponentId);
});
test('showModal should call Navigation.showModal', () => {
const showModal = jest.spyOn(Navigation, 'showModal');
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
leftButtonColor: theme.sidebarHeaderTextColor,
rightButtonColor: theme.sidebarHeaderTextColor,
},
};
const expectedLayout = {
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
};
NavigationActions.showModal(name, title, passProps, options);
expect(showModal).toHaveBeenCalledWith(expectedLayout);
});
test('showModalOverCurrentContext should call Navigation.showModal', () => {
const showModal = jest.spyOn(Navigation, 'showModal');
const animationsEnabled = (Platform.OS === 'android').toString();
const showModalOverCurrentContextTitle = '';
const showModalOverCurrentContextOptions = {
modalPresentationStyle: 'overCurrentContext',
layout: {
backgroundColor: 'transparent',
},
topBar: {
visible: false,
height: 0,
},
animations: {
showModal: {
enabled: animationsEnabled,
alpha: {
from: 0,
to: 1,
duration: 250,
},
},
dismissModal: {
enabled: animationsEnabled,
alpha: {
from: 1,
to: 0,
duration: 250,
},
},
},
};
const showModalOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: showModalOverCurrentContextTitle,
},
leftButtonColor: theme.sidebarHeaderTextColor,
rightButtonColor: theme.sidebarHeaderTextColor,
},
};
const defaultOptions = merge(showModalOverCurrentContextOptions, options);
const expectedLayout = {
stack: {
children: [{
component: {
name,
passProps,
options: merge(showModalOptions, defaultOptions),
},
}],
},
};
NavigationActions.showModalOverCurrentContext(name, passProps, options);
expect(showModal).toHaveBeenCalledWith(expectedLayout);
});
test('showSearchModal should call Navigation.showModal', () => {
const showModal = jest.spyOn(Navigation, 'showModal');
const showSearchModalName = 'Search';
const showSearchModalTitle = '';
const initialValue = 'initial-value';
const showSearchModalPassProps = {initialValue};
const showSearchModalOptions = {
topBar: {
visible: false,
height: 0,
},
};
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: showSearchModalTitle,
},
leftButtonColor: theme.sidebarHeaderTextColor,
rightButtonColor: theme.sidebarHeaderTextColor,
},
};
const expectedLayout = {
stack: {
children: [{
component: {
name: showSearchModalName,
passProps: showSearchModalPassProps,
options: merge(defaultOptions, showSearchModalOptions),
},
}],
},
};
NavigationActions.showSearchModal(initialValue);
expect(showModal).toHaveBeenCalledWith(expectedLayout);
});
test('dismissModal should call Navigation.dismissModal', async () => {
const dismissModal = jest.spyOn(Navigation, 'dismissModal');
await NavigationActions.dismissModal(options);
expect(dismissModal).toHaveBeenCalledWith(topComponentId, options);
});
test('dismissAllModals should call Navigation.dismissAllModals', async () => {
const dismissAllModals = jest.spyOn(Navigation, 'dismissAllModals');
await NavigationActions.dismissAllModals(options);
expect(dismissAllModals).toHaveBeenCalledWith(options);
});
test('peek should call Navigation.push', async () => {
const push = jest.spyOn(Navigation, 'push');
const defaultOptions = {
preview: {
commit: false,
},
};
const expectedLayout = {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
};
await NavigationActions.peek(name, passProps, options);
expect(push).toHaveBeenCalledWith(topComponentId, expectedLayout);
});
test('mergeNavigationOptions should call Navigation.mergeOptions', () => {
const mergeOptions = jest.spyOn(Navigation, 'mergeOptions');
NavigationActions.mergeNavigationOptions(topComponentId, options);
expect(mergeOptions).toHaveBeenCalledWith(topComponentId, options);
});
test('setButtons should call Navigation.mergeOptions', () => {
const mergeOptions = jest.spyOn(Navigation, 'mergeOptions');
const buttons = {
leftButtons: ['left-button'],
rightButtons: ['right-button'],
};
const setButtonsOptions = {
topBar: {
...buttons,
},
};
NavigationActions.setButtons(topComponentId, buttons);
expect(mergeOptions).toHaveBeenCalledWith(topComponentId, setButtonsOptions);
});
test('showOverlay should call Navigation.showOverlay', () => {
const showOverlay = jest.spyOn(Navigation, 'showOverlay');
const defaultOptions = {
overlay: {
interceptTouchOutside: false,
},
};
const expectedLayout = {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
};
NavigationActions.showOverlay(name, passProps, options);
expect(showOverlay).toHaveBeenCalledWith(expectedLayout);
});
test('dismissOverlay should call Navigation.dismissOverlay', async () => {
const dismissOverlay = jest.spyOn(Navigation, 'dismissOverlay');
await NavigationActions.dismissOverlay(topComponentId);
expect(dismissOverlay).toHaveBeenCalledWith(topComponentId);
});
});

View File

@@ -10,7 +10,8 @@ import {
fetchMyChannelsAndMembers,
getChannelByNameAndTeamName,
markChannelAsRead,
leaveChannel as serviceLeaveChannel, markChannelAsViewed,
markChannelAsViewed,
leaveChannel as serviceLeaveChannel,
selectChannel,
getChannelStats,
} from 'mattermost-redux/actions/channels';

View File

@@ -20,6 +20,23 @@ jest.mock('mattermost-redux/selectors/entities/channels', () => ({
getMyChannelMember: () => ({data: {member: {}}}),
}));
jest.mock('mattermost-redux/actions/channels', () => {
const channelActions = require.requireActual('mattermost-redux/actions/channels');
return {
...channelActions,
markChannelAsRead: jest.fn(),
markChannelAsViewed: jest.fn(),
};
});
jest.mock('mattermost-redux/selectors/entities/teams', () => {
const teamSelectors = require.requireActual('mattermost-redux/selectors/entities/teams');
return {
...teamSelectors,
getTeamByName: jest.fn(() => ({name: 'current-team-name'})),
};
});
const mockStore = configureStore([thunk]);
describe('Actions.Views.Channel', () => {

View File

@@ -2,6 +2,7 @@
exports[`Fade should render {opacity: 0} 1`] = `
<AnimatedComponent
pointerEvents="box-none"
style={
Object {
"opacity": 0,
@@ -21,6 +22,7 @@ exports[`Fade should render {opacity: 0} 1`] = `
exports[`Fade should render {opacity: 1} 1`] = `
<AnimatedComponent
pointerEvents="box-none"
style={
Object {
"opacity": 1,

View File

@@ -1,13 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`profile_picture_button should match snapshot 1`] = `
<Connect(AttachmentButton)
<AttachmentButton
blurTextBox={[MockFunction]}
browseFileTypes="public.item"
canBrowseFiles={true}
canBrowsePhotoLibrary={true}
canBrowseVideoLibrary={true}
canTakePhoto={true}
canTakeVideo={true}
extraOptions={
Array [
null,
]
}
maxFileCount={5}
maxFileSize={20971520}
theme={
Object {
@@ -38,5 +45,6 @@ exports[`profile_picture_button should match snapshot 1`] = `
}
}
uploadFiles={[MockFunction]}
validMimeTypes={Array []}
/>
`;

View File

@@ -37,8 +37,7 @@ exports[`SendButton should change theme backgroundColor to 0.3 opacity 1`] = `
`;
exports[`SendButton should match snapshot 1`] = `
<TouchableOpacity
activeOpacity={0.2}
<TouchableWithFeedbackIOS
onPress={[MockFunction]}
style={
Object {
@@ -47,6 +46,7 @@ exports[`SendButton should match snapshot 1`] = `
"paddingVertical": 3,
}
}
type="opacity"
>
<View
style={
@@ -66,12 +66,11 @@ exports[`SendButton should match snapshot 1`] = `
width={15}
/>
</View>
</TouchableOpacity>
</TouchableWithFeedbackIOS>
`;
exports[`SendButton should render theme backgroundColor 1`] = `
<TouchableOpacity
activeOpacity={0.2}
<TouchableWithFeedbackIOS
onPress={[MockFunction]}
style={
Object {
@@ -80,6 +79,7 @@ exports[`SendButton should render theme backgroundColor 1`] = `
"paddingVertical": 3,
}
}
type="opacity"
>
<View
style={
@@ -99,5 +99,5 @@ exports[`SendButton should render theme backgroundColor 1`] = `
width={15}
/>
</View>
</TouchableOpacity>
</TouchableWithFeedbackIOS>
`;

View File

@@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Swiper should match snapshot 1`] = `
<View
onLayout={[Function]}
style={
Array [
Object {
"backgroundColor": "transparent",
"flex": 1,
"position": "relative",
},
]
}
>
<ScrollViewMock
automaticallyAdjustContentInsets={true}
bounces={false}
contentContainerStyle={
Array [
Object {
"backgroundColor": "transparent",
},
undefined,
]
}
horizontal={true}
keyboardShouldPersistTaps="handled"
onMomentumScrollEnd={[Function]}
onScrollBeginDrag={[Function]}
pagingEnabled={true}
removeClippedSubviews={true}
scrollEnabled={true}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
/>
</View>
`;

View File

@@ -11,16 +11,15 @@ import {
} from 'react-native';
import {intlShape} from 'react-intl';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import RemoveMarkdown from 'app/components/remove_markdown';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {goToScreen} from 'app/actions/navigation';
const {View: AnimatedView} = Animated;
export default class AnnouncementBanner extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
bannerColor: PropTypes.string,
bannerDismissed: PropTypes.bool,
bannerEnabled: PropTypes.bool,
@@ -55,7 +54,6 @@ export default class AnnouncementBanner extends PureComponent {
}
handlePress = () => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'ExpandedAnnouncementBanner';
@@ -64,7 +62,7 @@ export default class AnnouncementBanner extends PureComponent {
defaultMessage: 'Announcement',
});
actions.goToScreen(screen, title);
goToScreen(screen, title);
};
toggleBanner = (show = true) => {

View File

@@ -12,9 +12,6 @@ jest.useFakeTimers();
describe('AnnouncementBanner', () => {
const baseProps = {
actions: {
goToScreen: jest.fn(),
},
bannerColor: '#ddd',
bannerDismissed: false,
bannerEnabled: true,

View File

@@ -1,13 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import {isLandscape} from 'app/selectors/device';
import AnnouncementBanner from './announcement_banner';
@@ -28,12 +26,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AnnouncementBanner);
export default connect(mapStateToProps)(AnnouncementBanner);

View File

@@ -11,12 +11,10 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils';
import CustomPropTypes from 'app/constants/custom_prop_types';
import mattermostManaged from 'app/mattermost_managed';
import BottomSheet from 'app/utils/bottom_sheet';
import {goToScreen} from 'app/actions/navigation';
export default class AtMention extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
isSearchResult: PropTypes.bool,
mentionName: PropTypes.string.isRequired,
mentionStyle: CustomPropTypes.Style,
@@ -50,7 +48,6 @@ export default class AtMention extends React.PureComponent {
}
goToUserProfile = () => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'UserProfile';
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
@@ -58,7 +55,7 @@ export default class AtMention extends React.PureComponent {
userId: this.state.user.id,
};
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
};
getUserDetailsFromMentionName(props) {

View File

@@ -1,15 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import AtMention from './at_mention';
function mapStateToProps(state) {
@@ -20,12 +17,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AtMention);
export default connect(mapStateToProps)(AtMention);

View File

@@ -1,8 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AttachmentButton should match snapshot 1`] = `
<TouchableOpacity
activeOpacity={0.2}
<TouchableWithFeedbackIOS
onPress={[Function]}
style={
Object {
@@ -12,6 +11,7 @@ exports[`AttachmentButton should match snapshot 1`] = `
"width": 45,
}
}
type="opacity"
>
<Icon
allowFontScaling={false}
@@ -24,5 +24,5 @@ exports[`AttachmentButton should match snapshot 1`] = `
}
}
/>
</TouchableOpacity>
</TouchableWithFeedbackIOS>
`;

View File

@@ -1,536 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
Alert,
NativeModules,
Platform,
StyleSheet,
TouchableOpacity,
} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
import DeviceInfo from 'react-native-device-info';
import AndroidOpenSettings from 'react-native-android-open-settings';
import Icon from 'react-native-vector-icons/Ionicons';
import {DocumentPicker} from 'react-native-document-picker';
import ImagePicker from 'react-native-image-picker';
import Permissions from 'react-native-permissions';
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
import {PermissionTypes} from 'app/constants';
import {changeOpacity} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
const ShareExtension = NativeModules.MattermostShare;
export default class AttachmentButton extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
blurTextBox: PropTypes.func.isRequired,
browseFileTypes: PropTypes.string,
validMimeTypes: PropTypes.array,
canBrowseFiles: PropTypes.bool,
canBrowsePhotoLibrary: PropTypes.bool,
canBrowseVideoLibrary: PropTypes.bool,
canTakePhoto: PropTypes.bool,
canTakeVideo: PropTypes.bool,
children: PropTypes.node,
fileCount: PropTypes.number,
maxFileCount: PropTypes.number.isRequired,
maxFileSize: PropTypes.number.isRequired,
onShowFileMaxWarning: PropTypes.func,
onShowFileSizeWarning: PropTypes.func,
onShowUnsupportedMimeTypeWarning: PropTypes.func,
theme: PropTypes.object.isRequired,
uploadFiles: PropTypes.func.isRequired,
wrapper: PropTypes.bool,
extraOptions: PropTypes.arrayOf(PropTypes.object),
};
static defaultProps = {
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
validMimeTypes: [],
canBrowseFiles: true,
canBrowsePhotoLibrary: true,
canBrowseVideoLibrary: true,
canTakePhoto: true,
canTakeVideo: true,
maxFileCount: 5,
extraOptions: null,
};
static contextTypes = {
intl: intlShape.isRequired,
};
getPermissionDeniedMessage = (source, mediaType = '') => {
const {formatMessage} = this.context.intl;
const applicationName = DeviceInfo.getApplicationName();
switch (source) {
case 'camera': {
if (mediaType === 'video') {
return {
title: formatMessage({
id: 'mobile.camera_video_permission_denied_title',
defaultMessage: '{applicationName} would like to access your camera',
}, {applicationName}),
text: formatMessage({
id: 'mobile.camera_video_permission_denied_description',
defaultMessage: 'Take videos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
}),
};
}
return {
title: formatMessage({
id: 'mobile.camera_photo_permission_denied_title',
defaultMessage: '{applicationName} would like to access your camera',
}, {applicationName}),
text: formatMessage({
id: 'mobile.camera_photo_permission_denied_description',
defaultMessage: 'Take photos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
}),
};
}
case 'storage':
return {
title: formatMessage({
id: 'mobile.storage_permission_denied_title',
defaultMessage: '{applicationName} would like to access your files',
}, {applicationName}),
text: formatMessage({
id: 'mobile.storage_permission_denied_description',
defaultMessage: 'Upload files to your Mattermost instance. Open Settings to grant Mattermost Read and Write access to files on this device.',
}),
};
case 'video':
return {
title: formatMessage({
id: 'mobile.android.videos_permission_denied_title',
defaultMessage: '{applicationName} would like to access your videos',
}, {applicationName}),
text: formatMessage({
id: 'mobile.android.videos_permission_denied_description',
defaultMessage: 'Upload videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your video library.',
}),
};
case 'photo':
default: {
if (Platform.OS === 'android') {
return {
title: formatMessage({
id: 'mobile.android.photos_permission_denied_title',
defaultMessage: '{applicationName} would like to access your photos',
}, {applicationName}),
text: formatMessage({
id: 'mobile.android.photos_permission_denied_description',
defaultMessage: 'Upload photos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo library.',
}),
};
}
return {
title: formatMessage({
id: 'mobile.ios.photos_permission_denied_title',
defaultMessage: '{applicationName} would like to access your photos',
}, {applicationName}),
text: formatMessage({
id: 'mobile.ios.photos_permission_denied_description',
defaultMessage: 'Upload photos and videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo and video library.',
}),
};
}
}
}
attachPhotoFromCamera = () => {
return this.attachFileFromCamera('camera', 'photo');
};
attachFileFromCamera = async (source, mediaType) => {
const {formatMessage} = this.context.intl;
const {title, text} = this.getPermissionDeniedMessage('camera', mediaType);
const options = {
quality: 0.8,
videoQuality: 'high',
noData: true,
mediaType,
storageOptions: {
cameraRoll: true,
waitUntilSaved: true,
},
permissionDenied: {
title,
text,
reTryTitle: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
},
};
const hasCameraPermission = await this.hasPhotoPermission(source, mediaType);
if (hasCameraPermission) {
ImagePicker.launchCamera(options, (response) => {
if (response.error || response.didCancel) {
return;
}
this.uploadFiles([response]);
});
}
};
attachFileFromLibrary = async () => {
const {formatMessage} = this.context.intl;
const {title, text} = this.getPermissionDeniedMessage('photo');
const options = {
quality: 0.8,
noData: true,
permissionDenied: {
title,
text,
reTryTitle: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
},
};
if (Platform.OS === 'ios') {
options.mediaType = 'mixed';
}
const hasPhotoPermission = await this.hasPhotoPermission('photo');
if (hasPhotoPermission) {
ImagePicker.launchImageLibrary(options, (response) => {
if (response.error || response.didCancel) {
return;
}
this.uploadFiles([response]);
});
}
};
attachVideoFromCamera = () => {
return this.attachFileFromCamera('camera', 'video');
};
attachVideoFromLibraryAndroid = () => {
const {formatMessage} = this.context.intl;
const {title, text} = this.getPermissionDeniedMessage('video');
const options = {
videoQuality: 'high',
mediaType: 'video',
noData: true,
permissionDenied: {
title,
text,
reTryTitle: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
},
};
ImagePicker.launchImageLibrary(options, (response) => {
if (response.error || response.didCancel) {
return;
}
this.uploadFiles([response]);
});
};
attachFileFromFiles = async () => {
const {browseFileTypes} = this.props;
const hasPermission = await this.hasStoragePermission();
if (hasPermission) {
DocumentPicker.show({
filetype: [browseFileTypes],
}, async (error, res) => {
if (error) {
return;
}
if (Platform.OS === 'android') {
// For android we need to retrieve the realPath in case the file being imported is from the cloud
const newUri = await ShareExtension.getFilePath(res.uri);
if (newUri.filePath) {
res.uri = newUri.filePath;
} else {
return;
}
}
// Decode file uri to get the actual path
res.uri = decodeURIComponent(res.uri);
this.uploadFiles([res]);
});
}
};
hasPhotoPermission = async (source, mediaType = '') => {
if (Platform.OS === 'ios') {
const {formatMessage} = this.context.intl;
let permissionRequest;
const targetSource = source || 'photo';
const hasPermissionToStorage = await Permissions.check(targetSource);
switch (hasPermissionToStorage) {
case PermissionTypes.UNDETERMINED:
permissionRequest = await Permissions.request(targetSource);
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
return false;
}
break;
case PermissionTypes.DENIED: {
const canOpenSettings = await Permissions.canOpenSettings();
let grantOption = null;
if (canOpenSettings) {
grantOption = {
text: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
onPress: () => Permissions.openSettings(),
};
}
const {title, text} = this.getPermissionDeniedMessage(source, mediaType);
Alert.alert(
title,
text,
[
grantOption,
{
text: formatMessage({
id: 'mobile.permission_denied_dismiss',
defaultMessage: 'Don\'t Allow',
}),
},
]
);
return false;
}
}
}
return true;
};
hasStoragePermission = async () => {
if (Platform.OS === 'android') {
const {formatMessage} = this.context.intl;
let permissionRequest;
const hasPermissionToStorage = await Permissions.check('storage');
switch (hasPermissionToStorage) {
case PermissionTypes.UNDETERMINED:
permissionRequest = await Permissions.request('storage');
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
return false;
}
break;
case PermissionTypes.DENIED: {
const {title, text} = this.getPermissionDeniedMessage('storage');
Alert.alert(
title,
text,
[
{
text: formatMessage({
id: 'mobile.permission_denied_dismiss',
defaultMessage: 'Don\'t Allow',
}),
},
{
text: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
onPress: () => AndroidOpenSettings.appDetailsSettings(),
},
]
);
return false;
}
}
}
return true;
};
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.type) {
file.type = lookupMimeType(file.fileName);
}
const {validMimeTypes} = this.props;
if (validMimeTypes.length && !validMimeTypes.includes(file.type)) {
this.props.onShowUnsupportedMimeTypeWarning();
} else if (file.fileSize > this.props.maxFileSize) {
this.props.onShowFileSizeWarning(file.fileName);
} else {
this.props.uploadFiles(files);
}
};
showFileAttachmentOptions = () => {
const {
canBrowseFiles,
canBrowsePhotoLibrary,
canBrowseVideoLibrary,
canTakePhoto,
canTakeVideo,
fileCount,
maxFileCount,
onShowFileMaxWarning,
extraOptions,
actions,
} = this.props;
if (fileCount === maxFileCount) {
onShowFileMaxWarning();
return;
}
this.props.blurTextBox();
const items = [];
if (canTakePhoto) {
items.push({
action: this.attachPhotoFromCamera,
text: {
id: t('mobile.file_upload.camera_photo'),
defaultMessage: 'Take Photo',
},
icon: 'camera',
});
}
if (canTakeVideo) {
items.push({
action: this.attachVideoFromCamera,
text: {
id: t('mobile.file_upload.camera_video'),
defaultMessage: 'Take Video',
},
icon: 'video-camera',
});
}
if (canBrowsePhotoLibrary) {
items.push({
action: this.attachFileFromLibrary,
text: {
id: t('mobile.file_upload.library'),
defaultMessage: 'Photo Library',
},
icon: 'photo',
});
}
if (canBrowseVideoLibrary && Platform.OS === 'android') {
items.push({
action: this.attachVideoFromLibraryAndroid,
text: {
id: t('mobile.file_upload.video'),
defaultMessage: 'Video Library',
},
icon: 'file-video-o',
});
}
if (canBrowseFiles) {
items.push({
action: this.attachFileFromFiles,
text: {
id: t('mobile.file_upload.browse'),
defaultMessage: 'Browse Files',
},
icon: 'file',
});
}
if (extraOptions) {
extraOptions.forEach((option) => {
if (option !== null) {
items.push(option);
}
});
}
actions.showModalOverCurrentContext('OptionsModal', {items});
};
render() {
const {theme, wrapper, children} = this.props;
if (wrapper) {
return (
<TouchableOpacity
onPress={this.showFileAttachmentOptions}
>
{children}
</TouchableOpacity>
);
}
return (
<TouchableOpacity
onPress={this.showFileAttachmentOptions}
style={style.buttonContainer}
>
<Icon
size={30}
style={style.attachIcon}
color={changeOpacity(theme.centerChannelColor, 0.9)}
name='md-add'
/>
</TouchableOpacity>
);
}
}
const style = StyleSheet.create({
attachIcon: {
marginTop: Platform.select({
ios: 2,
android: 0,
}),
},
buttonContainer: {
height: Platform.select({
ios: 34,
android: 36,
}),
width: 45,
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@@ -1,19 +1,535 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
Alert,
NativeModules,
Platform,
StyleSheet,
} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
import DeviceInfo from 'react-native-device-info';
import AndroidOpenSettings from 'react-native-android-open-settings';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import Icon from 'react-native-vector-icons/Ionicons';
import {DocumentPicker} from 'react-native-document-picker';
import ImagePicker from 'react-native-image-picker';
import Permissions from 'react-native-permissions';
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {PermissionTypes} from 'app/constants';
import {changeOpacity} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import AttachmentButton from './attachment_button';
const ShareExtension = NativeModules.MattermostShare;
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showModalOverCurrentContext,
}, dispatch),
export default class AttachmentButton extends PureComponent {
static propTypes = {
blurTextBox: PropTypes.func.isRequired,
browseFileTypes: PropTypes.string,
validMimeTypes: PropTypes.array,
canBrowseFiles: PropTypes.bool,
canBrowsePhotoLibrary: PropTypes.bool,
canBrowseVideoLibrary: PropTypes.bool,
canTakePhoto: PropTypes.bool,
canTakeVideo: PropTypes.bool,
children: PropTypes.node,
fileCount: PropTypes.number,
maxFileCount: PropTypes.number.isRequired,
maxFileSize: PropTypes.number.isRequired,
onShowFileMaxWarning: PropTypes.func,
onShowFileSizeWarning: PropTypes.func,
onShowUnsupportedMimeTypeWarning: PropTypes.func,
theme: PropTypes.object.isRequired,
uploadFiles: PropTypes.func.isRequired,
wrapper: PropTypes.bool,
extraOptions: PropTypes.arrayOf(PropTypes.object),
};
static defaultProps = {
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
validMimeTypes: [],
canBrowseFiles: true,
canBrowsePhotoLibrary: true,
canBrowseVideoLibrary: true,
canTakePhoto: true,
canTakeVideo: true,
maxFileCount: 5,
extraOptions: null,
};
static contextTypes = {
intl: intlShape.isRequired,
};
getPermissionDeniedMessage = (source, mediaType = '') => {
const {formatMessage} = this.context.intl;
const applicationName = DeviceInfo.getApplicationName();
switch (source) {
case 'camera': {
if (mediaType === 'video') {
return {
title: formatMessage({
id: 'mobile.camera_video_permission_denied_title',
defaultMessage: '{applicationName} would like to access your camera',
}, {applicationName}),
text: formatMessage({
id: 'mobile.camera_video_permission_denied_description',
defaultMessage: 'Take videos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
}),
};
}
return {
title: formatMessage({
id: 'mobile.camera_photo_permission_denied_title',
defaultMessage: '{applicationName} would like to access your camera',
}, {applicationName}),
text: formatMessage({
id: 'mobile.camera_photo_permission_denied_description',
defaultMessage: 'Take photos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
}),
};
}
case 'storage':
return {
title: formatMessage({
id: 'mobile.storage_permission_denied_title',
defaultMessage: '{applicationName} would like to access your files',
}, {applicationName}),
text: formatMessage({
id: 'mobile.storage_permission_denied_description',
defaultMessage: 'Upload files to your Mattermost instance. Open Settings to grant Mattermost Read and Write access to files on this device.',
}),
};
case 'video':
return {
title: formatMessage({
id: 'mobile.android.videos_permission_denied_title',
defaultMessage: '{applicationName} would like to access your videos',
}, {applicationName}),
text: formatMessage({
id: 'mobile.android.videos_permission_denied_description',
defaultMessage: 'Upload videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your video library.',
}),
};
case 'photo':
default: {
if (Platform.OS === 'android') {
return {
title: formatMessage({
id: 'mobile.android.photos_permission_denied_title',
defaultMessage: '{applicationName} would like to access your photos',
}, {applicationName}),
text: formatMessage({
id: 'mobile.android.photos_permission_denied_description',
defaultMessage: 'Upload photos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo library.',
}),
};
}
return {
title: formatMessage({
id: 'mobile.ios.photos_permission_denied_title',
defaultMessage: '{applicationName} would like to access your photos',
}, {applicationName}),
text: formatMessage({
id: 'mobile.ios.photos_permission_denied_description',
defaultMessage: 'Upload photos and videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo and video library.',
}),
};
}
}
}
attachPhotoFromCamera = () => {
return this.attachFileFromCamera('camera', 'photo');
};
attachFileFromCamera = async (source, mediaType) => {
const {formatMessage} = this.context.intl;
const {title, text} = this.getPermissionDeniedMessage('camera', mediaType);
const options = {
quality: 0.8,
videoQuality: 'high',
noData: true,
mediaType,
storageOptions: {
cameraRoll: true,
waitUntilSaved: true,
},
permissionDenied: {
title,
text,
reTryTitle: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
},
};
const hasCameraPermission = await this.hasPhotoPermission(source, mediaType);
if (hasCameraPermission) {
ImagePicker.launchCamera(options, (response) => {
if (response.error || response.didCancel) {
return;
}
this.uploadFiles([response]);
});
}
};
attachFileFromLibrary = async () => {
const {formatMessage} = this.context.intl;
const {title, text} = this.getPermissionDeniedMessage('photo');
const options = {
quality: 0.8,
noData: true,
permissionDenied: {
title,
text,
reTryTitle: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
},
};
if (Platform.OS === 'ios') {
options.mediaType = 'mixed';
}
const hasPhotoPermission = await this.hasPhotoPermission('photo');
if (hasPhotoPermission) {
ImagePicker.launchImageLibrary(options, (response) => {
if (response.error || response.didCancel) {
return;
}
this.uploadFiles([response]);
});
}
};
attachVideoFromCamera = () => {
return this.attachFileFromCamera('camera', 'video');
};
attachVideoFromLibraryAndroid = () => {
const {formatMessage} = this.context.intl;
const {title, text} = this.getPermissionDeniedMessage('video');
const options = {
videoQuality: 'high',
mediaType: 'video',
noData: true,
permissionDenied: {
title,
text,
reTryTitle: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
},
};
ImagePicker.launchImageLibrary(options, (response) => {
if (response.error || response.didCancel) {
return;
}
this.uploadFiles([response]);
});
};
attachFileFromFiles = async () => {
const {browseFileTypes} = this.props;
const hasPermission = await this.hasStoragePermission();
if (hasPermission) {
DocumentPicker.show({
filetype: [browseFileTypes],
}, async (error, res) => {
if (error) {
return;
}
if (Platform.OS === 'android') {
// For android we need to retrieve the realPath in case the file being imported is from the cloud
const newUri = await ShareExtension.getFilePath(res.uri);
if (newUri.filePath) {
res.uri = newUri.filePath;
} else {
return;
}
}
// Decode file uri to get the actual path
res.uri = decodeURIComponent(res.uri);
this.uploadFiles([res]);
});
}
};
hasPhotoPermission = async (source, mediaType = '') => {
if (Platform.OS === 'ios') {
const {formatMessage} = this.context.intl;
let permissionRequest;
const targetSource = source || 'photo';
const hasPermissionToStorage = await Permissions.check(targetSource);
switch (hasPermissionToStorage) {
case PermissionTypes.UNDETERMINED:
permissionRequest = await Permissions.request(targetSource);
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
return false;
}
break;
case PermissionTypes.DENIED: {
const canOpenSettings = await Permissions.canOpenSettings();
let grantOption = null;
if (canOpenSettings) {
grantOption = {
text: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
onPress: () => Permissions.openSettings(),
};
}
const {title, text} = this.getPermissionDeniedMessage(source, mediaType);
Alert.alert(
title,
text,
[
grantOption,
{
text: formatMessage({
id: 'mobile.permission_denied_dismiss',
defaultMessage: 'Don\'t Allow',
}),
},
]
);
return false;
}
}
}
return true;
};
hasStoragePermission = async () => {
if (Platform.OS === 'android') {
const {formatMessage} = this.context.intl;
let permissionRequest;
const hasPermissionToStorage = await Permissions.check('storage');
switch (hasPermissionToStorage) {
case PermissionTypes.UNDETERMINED:
permissionRequest = await Permissions.request('storage');
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
return false;
}
break;
case PermissionTypes.DENIED: {
const {title, text} = this.getPermissionDeniedMessage('storage');
Alert.alert(
title,
text,
[
{
text: formatMessage({
id: 'mobile.permission_denied_dismiss',
defaultMessage: 'Don\'t Allow',
}),
},
{
text: formatMessage({
id: 'mobile.permission_denied_retry',
defaultMessage: 'Settings',
}),
onPress: () => AndroidOpenSettings.appDetailsSettings(),
},
]
);
return false;
}
}
}
return true;
};
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.type) {
file.type = lookupMimeType(file.fileName);
}
const {validMimeTypes} = this.props;
if (validMimeTypes.length && !validMimeTypes.includes(file.type)) {
this.props.onShowUnsupportedMimeTypeWarning();
} else if (file.fileSize > this.props.maxFileSize) {
this.props.onShowFileSizeWarning(file.fileName);
} else {
this.props.uploadFiles(files);
}
};
showFileAttachmentOptions = () => {
const {
canBrowseFiles,
canBrowsePhotoLibrary,
canBrowseVideoLibrary,
canTakePhoto,
canTakeVideo,
fileCount,
maxFileCount,
onShowFileMaxWarning,
extraOptions,
} = this.props;
if (fileCount === maxFileCount) {
onShowFileMaxWarning();
return;
}
this.props.blurTextBox();
const items = [];
if (canTakePhoto) {
items.push({
action: this.attachPhotoFromCamera,
text: {
id: t('mobile.file_upload.camera_photo'),
defaultMessage: 'Take Photo',
},
icon: 'camera',
});
}
if (canTakeVideo) {
items.push({
action: this.attachVideoFromCamera,
text: {
id: t('mobile.file_upload.camera_video'),
defaultMessage: 'Take Video',
},
icon: 'video-camera',
});
}
if (canBrowsePhotoLibrary) {
items.push({
action: this.attachFileFromLibrary,
text: {
id: t('mobile.file_upload.library'),
defaultMessage: 'Photo Library',
},
icon: 'photo',
});
}
if (canBrowseVideoLibrary && Platform.OS === 'android') {
items.push({
action: this.attachVideoFromLibraryAndroid,
text: {
id: t('mobile.file_upload.video'),
defaultMessage: 'Video Library',
},
icon: 'file-video-o',
});
}
if (canBrowseFiles) {
items.push({
action: this.attachFileFromFiles,
text: {
id: t('mobile.file_upload.browse'),
defaultMessage: 'Browse Files',
},
icon: 'file',
});
}
if (extraOptions) {
extraOptions.forEach((option) => {
if (option !== null) {
items.push(option);
}
});
}
showModalOverCurrentContext('OptionsModal', {items});
};
render() {
const {theme, wrapper, children} = this.props;
if (wrapper) {
return (
<TouchableWithFeedback
onPress={this.showFileAttachmentOptions}
type={'opacity'}
>
{children}
</TouchableWithFeedback>
);
}
return (
<TouchableWithFeedback
onPress={this.showFileAttachmentOptions}
style={style.buttonContainer}
type={'opacity'}
>
<Icon
size={30}
style={style.attachIcon}
color={changeOpacity(theme.centerChannelColor, 0.9)}
name='md-add'
/>
</TouchableWithFeedback>
);
}
}
export default connect(null, mapDispatchToProps)(AttachmentButton);
const style = StyleSheet.create({
attachIcon: {
marginTop: Platform.select({
ios: 2,
android: 0,
}),
},
buttonContainer: {
height: Platform.select({
ios: 34,
android: 36,
}),
width: 45,
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@@ -9,9 +9,10 @@ import {Alert} from 'react-native';
import Preferences from 'mattermost-redux/constants/preferences';
import {VALID_MIME_TYPES} from 'app/screens/edit_profile/edit_profile';
import AttachmentButton from './attachment_button';
import {PermissionTypes} from 'app/constants';
import AttachmentButton from './index';
jest.mock('react-intl');
jest.mock('Platform', () => {
@@ -23,9 +24,6 @@ jest.mock('Platform', () => {
describe('AttachmentButton', () => {
const formatMessage = jest.fn();
const baseProps = {
actions: {
showModalOverCurrentContext: jest.fn(),
},
theme: Preferences.THEMES.default,
blurTextBox: jest.fn(),
maxFileSize: 10,
@@ -99,4 +97,4 @@ describe('AttachmentButton', () => {
await wrapper.instance().hasPhotoPermission('camera');
expect(Alert.alert).toBeCalled();
});
});
});

View File

@@ -5,15 +5,14 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
View,
} from 'react-native';
import ProfilePicture from 'app/components/profile_picture';
import BotTag from 'app/components/bot_tag';
import GuestTag from 'app/components/guest_tag';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {BotTag, GuestTag} from 'app/components/tag';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class AtMentionItem extends PureComponent {
static propTypes = {
@@ -54,10 +53,11 @@ export default class AtMentionItem extends PureComponent {
const hasFullName = firstName.length > 0 && lastName.length > 0;
return (
<TouchableOpacity
<TouchableWithFeedback
key={userId}
onPress={this.completeMention}
style={[style.row, padding(isLandscape)]}
type={'opacity'}
>
<View style={style.rowPicture}>
<ProfilePicture
@@ -78,7 +78,7 @@ export default class AtMentionItem extends PureComponent {
/>
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
{hasFullName && <Text style={style.rowFullname}>{`${firstName} ${lastName}`}</Text>}
</TouchableOpacity>
</TouchableWithFeedback>
);
}
}

View File

@@ -149,7 +149,7 @@ export default class Autocomplete extends PureComponent {
} else {
// List is expanding downwards, likely from the search box
let offset = Platform.select({ios: 65, android: 75});
if (DeviceTypes.IS_IPHONE_X) {
if (DeviceTypes.IS_IPHONE_WITH_INSETS) {
offset = 90;
}

View File

@@ -10,7 +10,6 @@ import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import {debounce} from 'mattermost-redux/actions/helpers';
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import ChannelMentionItem from 'app/components/autocomplete/channel_mention_item';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -231,7 +230,6 @@ export default class ChannelMention extends PureComponent {
sections={sections}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
ItemSeparatorComponent={AutocompleteDivider}
initialNumToRender={10}
nestedScrollEnabled={nestedScrollEnabled}
/>

View File

@@ -5,14 +5,13 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
} from 'react-native';
import {General} from 'mattermost-redux/constants';
import BotTag from 'app/components/bot_tag';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import {BotTag, GuestTag} from 'app/components/tag';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import GuestTag from 'app/components/guest_tag';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelMentionItem extends PureComponent {
@@ -51,15 +50,18 @@ export default class ChannelMentionItem extends PureComponent {
const style = getStyleFromTheme(theme);
let component;
if (type === General.DM_CHANNEL || type === General.GM_CHANNEL) {
if (!displayName) {
return null;
}
return (
<TouchableOpacity
component = (
<TouchableWithFeedback
key={channelId}
onPress={this.completeMention}
style={[style.row, padding(isLandscape)]}
type={'opacity'}
>
<Text style={style.rowDisplayName}>{'@' + displayName}</Text>
<BotTag
@@ -70,18 +72,27 @@ export default class ChannelMentionItem extends PureComponent {
show={isGuest}
theme={theme}
/>
</TouchableOpacity>
</TouchableWithFeedback>
);
} else {
component = (
<TouchableWithFeedback
key={channelId}
onPress={this.completeMention}
style={[style.row, padding(isLandscape)]}
type={'opacity'}
>
<Text style={style.rowDisplayName}>{displayName}</Text>
<Text style={style.rowName}>{` (~${name})`}</Text>
</TouchableWithFeedback>
);
}
return (
<TouchableOpacity
key={channelId}
onPress={this.completeMention}
style={[style.row, padding(isLandscape)]}
>
<Text style={style.rowDisplayName}>{displayName}</Text>
<Text style={style.rowName}>{` (~${name})`}</Text>
</TouchableOpacity>
<React.Fragment>
{component}
<AutocompleteDivider/>
</React.Fragment>
);
}
}

View File

@@ -4,28 +4,28 @@
import {connect} from 'react-redux';
import {General} from 'mattermost-redux/constants';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import {getChannelNameForSearchAutocomplete} from 'app/selectors/channel';
import {isLandscape} from 'app/selectors/device';
import {isGuest as isGuestUser} from 'app/utils/users';
import ChannelMentionItem from './channel_mention_item';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import {isLandscape} from 'app/selectors/device';
function mapStateToProps(state, ownProps) {
const channel = getChannel(state, ownProps.channelId);
const displayName = getChannelNameForSearchAutocomplete(state, ownProps.channelId);
let displayName = getChannelNameForSearchAutocomplete(state, ownProps.channelId);
let isBot = false;
let isGuest = false;
if (channel.type === General.DM_CHANNEL) {
const teammate = getUser(state, channel.teammate_id);
if (teammate && teammate.is_bot) {
isBot = true;
if (teammate) {
displayName = teammate.username;
isBot = teammate.is_bot || false;
isGuest = isGuestUser(teammate) || false;
}
}
@@ -34,6 +34,7 @@ function mapStateToProps(state, ownProps) {
name: channel.name,
type: channel.type,
isBot,
isGuest,
theme: getTheme(state),
isLandscape: isLandscape(state),
};

View File

@@ -7,7 +7,6 @@ import {
FlatList,
Platform,
Text,
TouchableOpacity,
View,
} from 'react-native';
@@ -15,6 +14,7 @@ import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import Emoji from 'app/components/emoji';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
const EMOJI_REGEX = /(^|\s|^\+|^-)(:([^:\s]*))$/i;
@@ -171,9 +171,10 @@ export default class EmojiSuggestion extends Component {
const style = getStyleFromTheme(this.props.theme);
return (
<TouchableOpacity
<TouchableWithFeedback
onPress={() => this.completeSuggestion(item)}
style={style.row}
type={'opacity'}
>
<View style={style.emoji}>
<Emoji
@@ -182,7 +183,7 @@ export default class EmojiSuggestion extends Component {
/>
</View>
<Text style={style.emojiName}>{`:${item}:`}</Text>
</TouchableOpacity>
</TouchableWithFeedback>
);
};

View File

@@ -3,13 +3,11 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
} from 'react-native';
import {Text} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class SlashSuggestionItem extends PureComponent {
static propTypes = {
@@ -38,13 +36,14 @@ export default class SlashSuggestionItem extends PureComponent {
const style = getStyleFromTheme(theme);
return (
<TouchableOpacity
<TouchableWithFeedback
onPress={this.completeSuggestion}
style={[style.row, padding(isLandscape)]}
type={'opacity'}
>
<Text style={style.suggestionName}>{`/${trigger} ${hint}`}</Text>
<Text style={style.suggestionDescription}>{description}</Text>
</TouchableOpacity>
</TouchableWithFeedback>
);
}
}

View File

@@ -5,12 +5,12 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
View,
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import FormattedText from 'app/components/formatted_text';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class SpecialMentionItem extends PureComponent {
@@ -40,9 +40,10 @@ export default class SpecialMentionItem extends PureComponent {
const style = getStyleFromTheme(theme);
return (
<TouchableOpacity
<TouchableWithFeedback
onPress={this.completeMention}
style={style.row}
type={'opacity'}
>
<View style={style.rowPicture}>
<Icon
@@ -60,7 +61,7 @@ export default class SpecialMentionItem extends PureComponent {
style={style.rowFullname}
/>
</Text>
</TouchableOpacity>
</TouchableWithFeedback>
);
}
}

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Text, TouchableOpacity, View} from 'react-native';
import {Text, View} from 'react-native';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import Icon from 'react-native-vector-icons/FontAwesome';
@@ -10,15 +10,17 @@ import Icon from 'react-native-vector-icons/FontAwesome';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import FormattedText from 'app/components/formatted_text';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {ViewTypes} from 'app/constants';
import {goToScreen} from 'app/actions/navigation';
export default class AutocompleteSelector extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
setAutocompleteSelector: PropTypes.func.isRequired,
goToScreen: PropTypes.func.isRequired,
}).isRequired,
label: PropTypes.string,
placeholder: PropTypes.string.isRequired,
@@ -33,6 +35,7 @@ export default class AutocompleteSelector extends PureComponent {
helpText: PropTypes.node,
errorText: PropTypes.node,
roundedBorders: PropTypes.bool,
isLandscape: PropTypes.bool.isRequired,
};
static contextTypes = {
@@ -101,7 +104,7 @@ export default class AutocompleteSelector extends PureComponent {
const title = placeholder || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
actions.setAutocompleteSelector(dataSource, this.handleSelect, options);
actions.goToScreen(screen, title);
goToScreen(screen, title);
});
render() {
@@ -115,6 +118,7 @@ export default class AutocompleteSelector extends PureComponent {
optional,
showRequiredAsterisk,
roundedBorders,
isLandscape,
} = this.props;
const {selectedText} = this.state;
const style = getStyleSheet(theme);
@@ -180,14 +184,17 @@ export default class AutocompleteSelector extends PureComponent {
return (
<View style={style.container}>
{labelContent}
<TouchableOpacity
<View style={padding(isLandscape)}>
{labelContent}
</View>
<TouchableWithFeedback
style={style.flex}
onPress={this.goToSelectorScreen}
type={'opacity'}
>
<View style={inputStyle}>
<Text
style={selectedStyle}
style={[selectedStyle, padding(isLandscape)]}
numberOfLines={1}
>
{text}
@@ -195,12 +202,14 @@ export default class AutocompleteSelector extends PureComponent {
<Icon
name='chevron-down'
color={changeOpacity(theme.centerChannelColor, 0.5)}
style={style.icon}
style={[style.icon, padding(isLandscape)]}
/>
</View>
</TouchableOpacity>
{helpTextContent}
{errorTextContent}
</TouchableWithFeedback>
<View style={padding(isLandscape)}>
{helpTextContent}
{errorTextContent}
</View>
</View>
);
}

View File

@@ -6,15 +6,16 @@ import {connect} from 'react-redux';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import {setAutocompleteSelector} from 'app/actions/views/post';
import AutocompleteSelector from './autocomplete_selector';
import {isLandscape} from 'app/selectors/device';
function mapStateToProps(state) {
return {
teammateNameDisplay: getTeammateNameDisplaySetting(state),
theme: getTheme(state),
isLandscape: isLandscape(state),
};
}
@@ -22,7 +23,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
setAutocompleteSelector,
goToScreen,
}, dispatch),
};
}

View File

@@ -1,61 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Platform, View} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import FormattedText from 'app/components/formatted_text';
export default class BotTag extends PureComponent {
static defaultProps = {
show: true,
};
static propTypes = {
show: PropTypes.bool,
theme: PropTypes.object.isRequired,
};
render() {
if (!this.props.show) {
return null;
}
const style = createStyleSheet(this.props.theme);
return (
<View style={style.bot}>
<FormattedText
id='post_info.bot'
defaultMessage='BOT'
style={style.botText}
/>
</View>
);
}
}
const createStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
bot: {
alignSelf: 'center',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.15),
borderRadius: 2,
marginRight: 2,
marginBottom: 1,
...Platform.select({
android: {
marginBottom: 0,
},
}),
marginLeft: 2,
paddingVertical: 2,
paddingHorizontal: 4,
},
botText: {
color: theme.centerChannelColor,
fontSize: 10,
fontWeight: '600',
},
};
});

View File

@@ -1,60 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {ActivityIndicator, StyleSheet, TouchableHighlight, View} from 'react-native';
import {GlobalStyles} from 'app/styles';
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
},
loading: {
marginLeft: 3,
},
});
export default class Button extends PureComponent {
static propTypes = {
children: PropTypes.node,
loading: PropTypes.bool,
onPress: PropTypes.func.isRequired,
};
onPress = () => {
if (!this.props.loading) {
this.props.onPress();
}
};
render() {
let loading = null;
if (this.props.loading) {
loading = (
<ActivityIndicator
style={styles.loading}
animating={true}
size='small'
/>
);
}
return (
<TouchableHighlight
style={GlobalStyles.button}
underlayColor='#B5B5B5'
onPress={this.onPress}
>
<View style={styles.container}>
{this.props.children}
{loading}
</View>
</TouchableHighlight>
);
}
}

View File

@@ -5,16 +5,18 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
View,
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import {getFullName} from 'mattermost-redux/utils/user_utils';
import {General} from 'mattermost-redux/constants';
import {injectIntl, intlShape} from 'react-intl';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {goToScreen} from 'app/actions/navigation';
import ProfilePicture from 'app/components/profile_picture';
import BotTag from 'app/components/bot_tag';
import GuestTag from 'app/components/guest_tag';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {BotTag, GuestTag} from 'app/components/tag';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
@@ -22,9 +24,6 @@ import {isGuest} from 'app/utils/users';
class ChannelIntro extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
creator: PropTypes.object,
currentChannel: PropTypes.object.isRequired,
currentChannelMembers: PropTypes.array.isRequired,
@@ -38,14 +37,14 @@ class ChannelIntro extends PureComponent {
};
goToUserProfile = (userId) => {
const {actions, intl} = this.props;
const {intl} = this.props;
const screen = 'UserProfile';
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const passProps = {
userId,
};
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
};
getDisplayName = (member) => {
@@ -67,10 +66,11 @@ class ChannelIntro extends PureComponent {
const style = getStyleSheet(theme);
return currentChannelMembers.map((member) => (
<TouchableOpacity
<TouchableWithFeedback
key={member.id}
onPress={preventDoubleTap(() => this.goToUserProfile(member.id))}
style={style.profile}
type={'opacity'}
>
<ProfilePicture
userId={member.id}
@@ -78,7 +78,7 @@ class ChannelIntro extends PureComponent {
statusBorderWidth={2}
statusSize={25}
/>
</TouchableOpacity>
</TouchableWithFeedback>
));
};
@@ -88,9 +88,10 @@ class ChannelIntro extends PureComponent {
return currentChannelMembers.map((member, index) => {
return (
<TouchableOpacity
<TouchableWithFeedback
key={member.id}
onPress={preventDoubleTap(() => this.goToUserProfile(member.id))}
type={'opacity'}
>
<View style={style.indicatorContainer}>
<Text style={style.displayName}>
@@ -108,7 +109,7 @@ class ChannelIntro extends PureComponent {
{index === currentChannelMembers.length - 1 ? '' : ', '}
</Text>
</View>
</TouchableOpacity>
</TouchableWithFeedback>
);
});
};

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
@@ -11,7 +10,6 @@ import {getCurrentUserId, getUser, makeGetProfilesInChannel} from 'mattermost-re
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isLandscape} from 'app/selectors/device';
import {goToScreen} from 'app/actions/navigation';
import {getChannelMembersForDm} from 'app/selectors/channel';
import ChannelIntro from './channel_intro';
@@ -55,12 +53,4 @@ function makeMapStateToProps() {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(makeMapStateToProps, mapDispatchToProps)(ChannelIntro);
export default connect(makeMapStateToProps)(ChannelIntro);

View File

@@ -9,6 +9,7 @@ import {intlShape} from 'react-intl';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {t} from 'app/utils/i18n';
import {alertErrorWithFallback} from 'app/utils/general';
import {popToRoot, dismissAllModals} from 'app/actions/navigation';
import {getChannelFromChannelName} from './channel_link_utils';
@@ -23,10 +24,8 @@ export default class ChannelLink extends React.PureComponent {
textStyle: CustomPropTypes.Style,
channelsByName: PropTypes.object.isRequired,
actions: PropTypes.shape({
dismissAllModals: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
joinChannel: PropTypes.func.isRequired,
popToRoot: PropTypes.func.isRequired,
}).isRequired,
};
@@ -75,10 +74,11 @@ export default class ChannelLink extends React.PureComponent {
}
if (channel.id) {
const {dismissAllModals, handleSelectChannel, popToRoot} = this.props.actions;
const {handleSelectChannel} = this.props.actions;
handleSelectChannel(channel.id);
dismissAllModals();
popToRoot();
await dismissAllModals();
await popToRoot();
if (this.props.onChannelLinkPress) {
this.props.onChannelLinkPress(channel);

View File

@@ -11,9 +11,13 @@ import ChannelLink from './channel_link';
jest.mock('react-intl');
jest.mock('app/utils/general', () => ({
alertErrorWithFallback: jest.fn(),
}));
jest.mock('app/utils/general', () => {
const general = require.requireActual('app/utils/general');
return {
...general,
alertErrorWithFallback: jest.fn(),
};
});
describe('ChannelLink', () => {
const formatMessage = jest.fn();
@@ -30,10 +34,8 @@ describe('ChannelLink', () => {
textStyle: {color: '#3d3c40', fontSize: 15, lineHeight: 20},
channelsByName,
actions: {
dismissAllModals: jest.fn(),
handleSelectChannel: jest.fn(),
joinChannel: jest.fn(),
popToRoot: jest.fn(),
},
};
@@ -71,14 +73,14 @@ describe('ChannelLink', () => {
expect(innerText.props().onPress).not.toBeDefined();
});
test('should call props.actions and onChannelLinkPress on handlePress', () => {
test('should call props.actions and onChannelLinkPress on handlePress', async () => {
const wrapper = shallow(
<ChannelLink {...baseProps}/>,
{context: {intl: {formatMessage}}},
);
const channel = channelsByName.firstChannel;
wrapper.instance().handlePress();
await wrapper.instance().handlePress();
expect(baseProps.actions.handleSelectChannel).toHaveBeenCalledTimes(1);
expect(baseProps.actions.handleSelectChannel).toBeCalledWith(channel.id);
expect(baseProps.onChannelLinkPress).toHaveBeenCalledTimes(1);

View File

@@ -10,7 +10,6 @@ import {getChannelsNameMapInCurrentTeam} from 'mattermost-redux/selectors/entiti
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {dismissAllModals, popToRoot} from 'app/actions/navigation';
import {handleSelectChannel} from 'app/actions/views/channel';
import ChannelLink from './channel_link';
@@ -44,10 +43,8 @@ function makeMapStateToProps() {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
dismissAllModals,
handleSelectChannel,
joinChannel,
popToRoot,
}, dispatch),
};
}

View File

@@ -17,6 +17,7 @@ import FormattedText from 'app/components/formatted_text';
import {DeviceTypes} from 'app/constants';
import {checkUpgradeType, isUpgradeAvailable} from 'app/utils/client_upgrade';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {showModal, dismissModal} from 'app/actions/navigation';
const {View: AnimatedView} = Animated;
@@ -27,8 +28,6 @@ export default class ClientUpgradeListener extends PureComponent {
actions: PropTypes.shape({
logError: PropTypes.func.isRequired,
setLastUpgradeCheck: PropTypes.func.isRequired,
showModal: PropTypes.func.isRequired,
dismissModal: PropTypes.func.isRequired,
}).isRequired,
currentVersion: PropTypes.string,
downloadLink: PropTypes.string,
@@ -71,7 +70,7 @@ export default class ClientUpgradeListener extends PureComponent {
if (versionMismatch && (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT)) {
this.checkUpgrade(minVersion, latestVersion, nextProps.isLandscape);
} else if (this.props.isLandscape !== nextProps.isLandscape &&
isUpgradeAvailable(this.state.upgradeType) && DeviceTypes.IS_IPHONE_X) {
isUpgradeAvailable(this.state.upgradeType) && DeviceTypes.IS_IPHONE_WITH_INSETS) {
const newTop = nextProps.isLandscape ? 45 : 100;
this.setState({top: new Animated.Value(newTop)});
}
@@ -98,10 +97,10 @@ export default class ClientUpgradeListener extends PureComponent {
toggleUpgradeMessage = (show = true, isLandscape) => {
let toValue = -100;
if (show) {
if (DeviceTypes.IS_IPHONE_X && isLandscape) {
if (DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape) {
toValue = 45;
} else {
toValue = DeviceTypes.IS_IPHONE_X ? 100 : 75;
toValue = DeviceTypes.IS_IPHONE_WITH_INSETS ? 100 : 75;
}
}
Animated.timing(this.state.top, {
@@ -141,10 +140,9 @@ export default class ClientUpgradeListener extends PureComponent {
};
handleLearnMore = () => {
const {actions} = this.props;
const {intl} = this.context;
actions.dismissModal();
dismissModal();
const screen = 'ClientUpgrade';
const title = intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Upgrade App'});
@@ -160,7 +158,7 @@ export default class ClientUpgradeListener extends PureComponent {
},
};
actions.showModal(screen, title, passProps, options);
showModal(screen, title, passProps, options);
this.toggleUpgradeMessage(false);
};

View File

@@ -6,7 +6,6 @@ import {connect} from 'react-redux';
import {logError} from 'mattermost-redux/actions/errors';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {showModal, dismissModal} from 'app/actions/navigation';
import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade';
import getClientUpgrade from 'app/selectors/client_upgrade';
import {isLandscape} from 'app/selectors/device';
@@ -33,8 +32,6 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
logError,
setLastUpgradeCheck,
showModal,
dismissModal,
}, dispatch),
};
}

View File

@@ -4,12 +4,13 @@
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isLandscape} from 'app/selectors/device';
import OptionListRow from './option_list_row';
function mapStateToProps(state) {
return {
theme: getTheme(state),
isLandscape: isLandscape(state),
};
}

View File

@@ -17,12 +17,17 @@ export default class OptionListRow extends React.PureComponent {
id: PropTypes.string,
theme: PropTypes.object.isRequired,
...CustomListRow.propTypes,
isLandscape: PropTypes.bool.isRequired,
};
static contextTypes = {
intl: intlShape,
};
static defaultProps = {
isLandscape: false,
};
onPress = () => {
if (this.props.onPress) {
this.props.onPress(this.props.id, this.props.item);
@@ -36,6 +41,7 @@ export default class OptionListRow extends React.PureComponent {
selected,
theme,
item,
isLandscape,
} = this.props;
const {text, value} = item;
@@ -48,6 +54,7 @@ export default class OptionListRow extends React.PureComponent {
enabled={enabled}
selectable={selectable}
selected={selected}
isLandscape={isLandscape}
>
<View style={style.textContainer}>
<View>

View File

@@ -10,12 +10,12 @@ import {
} from 'react-native';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import CustomListRow from 'app/components/custom_list/custom_list_row';
import ProfilePicture from 'app/components/profile_picture';
import BotTag from 'app/components/bot_tag';
import GuestTag from 'app/components/guest_tag';
import {BotTag, GuestTag} from 'app/components/tag';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {isGuest} from 'app/utils/users';
import CustomListRow from 'app/components/custom_list/custom_list_row';
export default class UserListRow extends React.PureComponent {
static propTypes = {

View File

@@ -19,6 +19,7 @@ import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {
changeOpacity,
@@ -27,14 +28,10 @@ import {
} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {popTopScreen, dismissModal} from 'app/actions/navigation';
export default class EditChannelInfo extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
dismissModal: PropTypes.func.isRequired,
popTopScreen: PropTypes.func.isRequired,
}),
theme: PropTypes.object.isRequired,
deviceWidth: PropTypes.number.isRequired,
deviceHeight: PropTypes.number.isRequired,
@@ -97,11 +94,10 @@ export default class EditChannelInfo extends PureComponent {
};
close = (goBack = false) => {
const {actions} = this.props;
if (goBack) {
actions.popTopScreen();
popTopScreen();
} else {
actions.dismissModal();
dismissModal();
}
};

View File

@@ -11,10 +11,6 @@ import EditChannelInfo from './edit_channel_info';
describe('EditChannelInfo', () => {
const baseProps = {
actions: {
dismissModal: jest.fn(),
popTopScreen: jest.fn(),
},
theme: Preferences.THEMES.default,
deviceWidth: 400,
deviceHeight: 600,

View File

@@ -1,13 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {
dismissModal,
popTopScreen,
} from 'app/actions/navigation';
import {isLandscape} from 'app/selectors/device';
import EditChannelInfo from './edit_channel_info';
@@ -17,13 +12,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
dismissModal,
popTopScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(EditChannelInfo);
export default connect(mapStateToProps)(EditChannelInfo);

View File

@@ -27,7 +27,7 @@ export default class EmojiPicker extends EmojiPickerBase {
const {emojis, filteredEmojis, searchTerm} = this.state;
const styles = getStyleSheetFromTheme(theme);
const shorten = DeviceTypes.IS_IPHONE_X && isLandscape ? 6 : 2;
const shorten = DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? 6 : 2;
let listComponent;
if (searchTerm) {
@@ -68,9 +68,9 @@ export default class EmojiPicker extends EmojiPickerBase {
);
}
let keyboardOffset = DeviceTypes.IS_IPHONE_X ? 50 : 30;
let keyboardOffset = DeviceTypes.IS_IPHONE_WITH_INSETS ? 50 : 30;
if (isLandscape) {
keyboardOffset = DeviceTypes.IS_IPHONE_X ? 0 : 10;
keyboardOffset = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 10;
}
const searchBarInput = {

View File

@@ -216,7 +216,7 @@ export default class EmojiPicker extends PureComponent {
};
getNumberOfColumns = (deviceWidth) => {
const shorten = DeviceTypes.IS_IPHONE_X && this.props.isLandscape ? 4 : 2;
const shorten = DeviceTypes.IS_IPHONE_WITH_INSETS && this.props.isLandscape ? 4 : 2;
return Math.floor(Number(((deviceWidth - (SECTION_MARGIN * shorten)) / (EMOJI_SIZE + (EMOJI_GUTTER * shorten)))));
};

View File

@@ -47,6 +47,7 @@ export default class Fade extends PureComponent {
opacity: fadeAnim,
transform: disableScale ? [] : [{scale: fadeAnim}],
}}
pointerEvents={'box-none'}
>
{this.props.children}
</Animated.View>

View File

@@ -0,0 +1,80 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileAttachment should match snapshot 1`] = `
<View
style={
Array [
Object {
"borderColor": "rgba(61,60,64,0.2)",
"borderRadius": 2,
"borderWidth": 1,
"flex": 1,
"flexDirection": "row",
"marginRight": 10,
"marginTop": 10,
"width": 300,
},
]
}
>
<TouchableWithFeedbackIOS
onPress={[Function]}
type="opacity"
>
<FileAttachmentIcon
backgroundColor="#fff"
file={
Object {
"mime_type": "image/png",
}
}
iconHeight={60}
iconWidth={60}
onCaptureRef={[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",
}
}
wrapperHeight={80}
wrapperWidth={80}
/>
</TouchableWithFeedbackIOS>
<TouchableWithFeedbackIOS
onPress={[Function]}
style={
Object {
"borderLeftColor": "rgba(61,60,64,0.2)",
"borderLeftWidth": 1,
"flex": 1,
"paddingHorizontal": 8,
"paddingVertical": 5,
}
}
type="opacity"
/>
</View>
`;

View File

@@ -13,7 +13,6 @@ exports[`PostAttachmentOpenGraph should match snapshot with a single image file
>
<FileAttachment
canDownloadFiles={true}
deviceWidth={660}
file={
Object {
"caption": "image.png",

View File

@@ -5,12 +5,12 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
View,
} from 'react-native';
import * as Utils from 'mattermost-redux/utils/file_utils.js';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {isDocument, isGif} from 'app/utils/file';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -21,7 +21,6 @@ import FileAttachmentImage from './file_attachment_image';
export default class FileAttachment extends PureComponent {
static propTypes = {
canDownloadFiles: PropTypes.bool.isRequired,
deviceWidth: PropTypes.number.isRequired,
file: PropTypes.object.isRequired,
id: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
@@ -89,7 +88,6 @@ export default class FileAttachment extends PureComponent {
render() {
const {
canDownloadFiles,
deviceWidth,
file,
theme,
onLongPress,
@@ -100,17 +98,18 @@ export default class FileAttachment extends PureComponent {
let fileAttachmentComponent;
if ((data && data.has_preview_image) || file.loading || isGif(data)) {
fileAttachmentComponent = (
<TouchableOpacity
<TouchableWithFeedback
key={`${this.props.id}${file.loading}`}
onPress={this.handlePreviewPress}
onLongPress={onLongPress}
type={'opacity'}
>
<FileAttachmentImage
file={data || {}}
onCaptureRef={this.handleCaptureRef}
theme={theme}
/>
</TouchableOpacity>
</TouchableWithFeedback>
);
} else if (isDocument(data)) {
fileAttachmentComponent = (
@@ -124,31 +123,31 @@ export default class FileAttachment extends PureComponent {
);
} else {
fileAttachmentComponent = (
<TouchableOpacity
<TouchableWithFeedback
onPress={this.handlePreviewPress}
onLongPress={onLongPress}
type={'opacity'}
>
<FileAttachmentIcon
file={data}
onCaptureRef={this.handleCaptureRef}
theme={theme}
/>
</TouchableOpacity>
</TouchableWithFeedback>
);
}
const width = deviceWidth * 0.72;
return (
<View style={[style.fileWrapper, {width}]}>
<View style={[style.fileWrapper]}>
{fileAttachmentComponent}
<TouchableOpacity
<TouchableWithFeedback
style={style.fileInfoContainer}
onLongPress={onLongPress}
onPress={this.handlePreviewPress}
style={style.fileInfoContainer}
type={'opacity'}
>
{this.renderFileInfo()}
</TouchableOpacity>
</TouchableWithFeedback>
</View>
);
}
@@ -195,7 +194,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRadius: 2,
maxWidth: 350,
width: 300,
},
circularProgress: {
width: '100%',

View File

@@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import FileAttachment from './file_attachment.js';
import Preferences from 'mattermost-redux/constants/preferences';
jest.mock('react-native-doc-viewer', () => ({
openDoc: jest.fn(),
}));
describe('FileAttachment', () => {
const baseProps = {
canDownloadFiles: true,
file: {
create_at: 1546893090093,
delete_at: 0,
extension: 'png',
has_preview_image: true,
height: 171,
id: 'fileId',
name: 'image.png',
post_id: 'postId',
size: 14894,
update_at: 1546893090093,
user_id: 'userId',
width: 425,
data: {
mime_type: 'image/png',
},
},
id: 'id',
index: 0,
theme: Preferences.THEMES.default,
};
test('should match snapshot', () => {
const wrapper = shallow(
<FileAttachment {...baseProps}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View File

@@ -10,7 +10,6 @@ import {
Platform,
StatusBar,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native';
import OpenFile from 'react-native-doc-viewer';
@@ -21,11 +20,12 @@ import tinyColor from 'tinycolor2';
import {getFileUrl} from 'mattermost-redux/utils/file_utils.js';
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {DeviceTypes} from 'app/constants/';
import mattermostBucket from 'app/mattermost_bucket';
import {changeOpacity} from 'app/utils/theme';
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
import {goToScreen} from 'app/actions/navigation';
const {DOCUMENTS_PATH} = DeviceTypes;
const DOWNLOADING_OFFSET = 28;
@@ -38,9 +38,6 @@ const circularProgressWidth = 4;
export default class FileAttachmentDocument extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
backgroundColor: PropTypes.string,
canDownloadFiles: PropTypes.bool.isRequired,
iconHeight: PropTypes.number,
@@ -194,13 +191,14 @@ export default class FileAttachmentDocument extends PureComponent {
};
previewTextFile = (file, delay = 2000) => {
const {actions} = this.props;
const {data} = file;
const prefix = Platform.OS === 'android' ? 'file:/' : '';
const path = `${DOCUMENTS_PATH}/${data.id}-${file.caption}`;
const readFile = RNFetchBlob.fs.readFile(`${prefix}${path}`, 'utf8');
setTimeout(async () => {
try {
this.setState({downloading: false, progress: 0});
const content = await readFile;
const screen = 'TextPreview';
const title = file.caption;
@@ -208,8 +206,7 @@ export default class FileAttachmentDocument extends PureComponent {
content,
};
actions.goToScreen(screen, title, passProps);
this.setState({downloading: false, progress: 0});
goToScreen(screen, title, passProps);
} catch (error) {
RNFetchBlob.fs.unlink(path);
}
@@ -386,12 +383,13 @@ export default class FileAttachmentDocument extends PureComponent {
}
return (
<TouchableOpacity
<TouchableWithFeedback
onPress={this.handlePreviewPress}
onLongPress={onLongPress}
type={'opacity'}
>
{fileAttachmentComponent}
</TouchableOpacity>
</TouchableWithFeedback>
);
}
}

View File

@@ -1,19 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goToScreen} from 'app/actions/navigation';
import FileAttachmentDocument from './file_attachment_document';
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(null, mapDispatchToProps, null, {forwardRef: true})(FileAttachmentDocument);
export default connect(null, null, null, {forwardRef: true})(FileAttachmentDocument);

View File

@@ -22,11 +22,8 @@ export default class FileAttachmentList extends Component {
static propTypes = {
actions: PropTypes.shape({
loadFilesForPostIfNecessary: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
canDownloadFiles: PropTypes.bool.isRequired,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
fileIds: PropTypes.array.isRequired,
files: PropTypes.array,
isFailed: PropTypes.bool,
@@ -124,19 +121,17 @@ export default class FileAttachmentList extends Component {
};
handlePreviewPress = preventDoubleTap((idx) => {
const {actions} = this.props;
previewImageAtIndex(this.items, idx, this.galleryFiles, actions.showModalOverCurrentContext);
previewImageAtIndex(this.items, idx, this.galleryFiles);
});
renderItems = () => {
const {canDownloadFiles, deviceWidth, fileIds, files} = this.props;
const {canDownloadFiles, fileIds, files} = this.props;
if (!files.length && fileIds.length > 0) {
return fileIds.map((id, idx) => (
<FileAttachment
key={id}
canDownloadFiles={canDownloadFiles}
deviceWidth={deviceWidth}
file={{loading: true}}
id={id}
index={idx}
@@ -155,7 +150,6 @@ export default class FileAttachmentList extends Component {
<FileAttachment
key={file.id}
canDownloadFiles={canDownloadFiles}
deviceWidth={deviceWidth}
file={f}
id={file.id}
index={idx}

View File

@@ -16,7 +16,6 @@ describe('PostAttachmentOpenGraph', () => {
const baseProps = {
actions: {
loadFilesForPostIfNecessary,
showModalOverCurrentContext: jest.fn(),
},
canDownloadFiles: true,
deviceHeight: 680,
@@ -72,7 +71,6 @@ describe('PostAttachmentOpenGraph', () => {
files: [],
actions: {
loadFilesForPostIfNecessary: loadFilesForPostIfNecessaryMock,
showModalOverCurrentContext: jest.fn(),
},
};

View File

@@ -8,9 +8,7 @@ import {canDownloadFilesOnMobile} from 'mattermost-redux/selectors/entities/gene
import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import {loadFilesForPostIfNecessary} from 'app/actions/views/channel';
import {getDimensions} from 'app/selectors/device';
import FileAttachmentList from './file_attachment_list';
@@ -18,7 +16,6 @@ function makeMapStateToProps() {
const getFilesForPost = makeGetFilesForPost();
return function mapStateToProps(state, ownProps) {
return {
...getDimensions(state),
canDownloadFiles: canDownloadFilesOnMobile(state),
files: getFilesForPost(state, ownProps.postId),
theme: getTheme(state),
@@ -30,7 +27,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadFilesForPostIfNecessary,
showModalOverCurrentContext,
}, dispatch),
};
}

View File

@@ -3,9 +3,11 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Platform, StyleSheet, TouchableOpacity} from 'react-native';
import {Platform, StyleSheet} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
export default class FileUploadRemove extends PureComponent {
static propTypes = {
channelId: PropTypes.string,
@@ -22,9 +24,10 @@ export default class FileUploadRemove extends PureComponent {
render() {
return (
<TouchableOpacity
<TouchableWithFeedback
style={style.removeButtonWrapper}
onPress={this.handleOnPress}
type={'opacity'}
>
<Icon
name='md-close'
@@ -32,7 +35,7 @@ export default class FileUploadRemove extends PureComponent {
size={18}
style={style.removeButtonIcon}
/>
</TouchableOpacity>
</TouchableWithFeedback>
);
}
}

View File

@@ -3,9 +3,11 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {StyleSheet, TouchableOpacity} from 'react-native';
import {StyleSheet} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
export default class FileUploadRetry extends PureComponent {
static propTypes = {
file: PropTypes.object.isRequired,
@@ -20,16 +22,17 @@ export default class FileUploadRetry extends PureComponent {
render() {
return (
<TouchableOpacity
<TouchableWithFeedback
style={style.failed}
onPress={this.handleOnPress}
type={'opacity'}
>
<Icon
name='md-refresh'
size={50}
color='#fff'
/>
</TouchableOpacity>
</TouchableWithFeedback>
);
}
}

View File

@@ -6,12 +6,15 @@ import PropTypes from 'prop-types';
import {Text} from 'react-native';
import moment from 'moment-timezone';
import CustomPropTypes from 'app/constants/custom_prop_types';
export default class FormattedTime extends React.PureComponent {
static propTypes = {
value: PropTypes.any.isRequired,
timeZone: PropTypes.string,
children: PropTypes.func,
hour12: PropTypes.bool,
style: CustomPropTypes.Style,
};
getFormattedTime = () => {
@@ -30,13 +33,13 @@ export default class FormattedTime extends React.PureComponent {
};
render() {
const {children} = this.props;
const {children, style} = this.props;
const formattedTime = this.getFormattedTime();
if (typeof children === 'function') {
return children(formattedTime);
}
return <Text>{formattedTime}</Text>;
return <Text style={style}>{formattedTime}</Text>;
}
}

View File

@@ -6,8 +6,6 @@ import {connect} from 'react-redux';
import {submitInteractiveDialog} from 'mattermost-redux/actions/integrations';
import {showModal} from 'app/actions/navigation';
import InteractiveDialogController from './interactive_dialog_controller';
function mapStateToProps(state) {
@@ -20,7 +18,6 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showModal,
submitInteractiveDialog,
}, dispatch),
};

View File

@@ -7,10 +7,11 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {Alert} from 'react-native';
import {intlShape} from 'react-intl';
import {showModal} from 'app/actions/navigation';
export default class InteractiveDialogController extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
showModal: PropTypes.func.isRequired,
submitInteractiveDialog: PropTypes.func.isRequired,
}).isRequired,
triggerId: PropTypes.string,
@@ -88,7 +89,7 @@ export default class InteractiveDialogController extends PureComponent {
},
};
this.props.actions.showModal('InteractiveDialog', dialog.title, null, options);
showModal('InteractiveDialog', dialog.title, null, options);
}
handleCancel = (dialog, url) => {

View File

@@ -73,7 +73,6 @@ function getBaseProps(triggerId, elements, introductionText) {
return {
actions: {
showModal: jest.fn(),
submitInteractiveDialog: jest.fn(),
},
triggerId,

View File

@@ -5,16 +5,15 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
ActivityIndicator,
TouchableOpacity,
View,
ViewPropTypes,
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import FormattedText from 'app/components/formatted_text';
export default class LoadMorePosts extends PureComponent {
static propTypes = {
channelId: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
@@ -56,9 +55,12 @@ export default class LoadMorePosts extends PureComponent {
const style = getStyleSheet(this.props.theme);
return (
<View style={[style.container, this.props.style]}>
<TouchableOpacity onPress={this.loadMore}>
<TouchableWithFeedback
onPress={this.loadMore}
type={'opacity'}
>
{this.renderText(style)}
</TouchableOpacity>
</TouchableWithFeedback>
</View>
);
}

View File

@@ -1,52 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {Text} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
export default class Hashtag extends React.PureComponent {
static propTypes = {
hashtag: PropTypes.string.isRequired,
linkStyle: CustomPropTypes.Style.isRequired,
onHashtagPress: PropTypes.func,
actions: PropTypes.shape({
popToRoot: PropTypes.func.isRequired,
showSearchModal: PropTypes.func.isRequired,
dismissAllModals: PropTypes.func.isRequired,
}).isRequired,
};
handlePress = () => {
const {
onHashtagPress,
hashtag,
actions,
} = this.props;
if (onHashtagPress) {
onHashtagPress(hashtag);
return;
}
// Close thread view, permalink view, etc
actions.dismissAllModals();
actions.popToRoot();
actions.showSearchModal('#' + this.props.hashtag);
};
render() {
return (
<Text
style={this.props.linkStyle}
onPress={this.handlePress}
>
{`#${this.props.hashtag}`}
</Text>
);
}
}

View File

@@ -1,57 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {Text} from 'react-native';
import Hashtag from './hashtag';
describe('Hashtag', () => {
const baseProps = {
hashtag: 'test',
linkStyle: {color: 'red'},
actions: {
showSearchModal: jest.fn(),
dismissAllModals: jest.fn(),
popToRoot: jest.fn(),
},
};
test('should match snapshot', () => {
const wrapper = shallow(<Hashtag {...baseProps}/>);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should open hashtag search on click', () => {
const props = {
...baseProps,
};
const wrapper = shallow(<Hashtag {...props}/>);
wrapper.find(Text).simulate('press');
expect(props.actions.dismissAllModals).toHaveBeenCalled();
expect(props.actions.popToRoot).toHaveBeenCalled();
expect(props.actions.showSearchModal).toHaveBeenCalledWith('#test');
});
test('should call onHashtagPress if provided', () => {
const props = {
...baseProps,
onHashtagPress: jest.fn(),
};
const wrapper = shallow(<Hashtag {...props}/>);
wrapper.find(Text).simulate('press');
expect(props.actions.dismissAllModals).not.toBeCalled();
expect(props.actions.popToRoot).not.toBeCalled();
expect(props.actions.showSearchModal).not.toBeCalled();
expect(props.onHashtagPress).toBeCalled();
});
});

View File

@@ -1,25 +1,47 @@
// 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 PropTypes from 'prop-types';
import React from 'react';
import {Text} from 'react-native';
import {
popToRoot,
showSearchModal,
dismissAllModals,
} from 'app/actions/navigation';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {popToRoot, showSearchModal, dismissAllModals} from 'app/actions/navigation';
import Hashtag from './hashtag';
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
popToRoot,
showSearchModal,
dismissAllModals,
}, dispatch),
export default class Hashtag extends React.PureComponent {
static propTypes = {
hashtag: PropTypes.string.isRequired,
linkStyle: CustomPropTypes.Style.isRequired,
onHashtagPress: PropTypes.func,
};
}
export default connect(null, mapDispatchToProps)(Hashtag);
handlePress = async () => {
const {
onHashtagPress,
hashtag,
} = this.props;
if (onHashtagPress) {
onHashtagPress(hashtag);
return;
}
// Close thread view, permalink view, etc
await dismissAllModals();
await popToRoot();
showSearchModal('#' + this.props.hashtag);
};
render() {
return (
<Text
style={this.props.linkStyle}
onPress={this.handlePress}
>
{`#${this.props.hashtag}`}
</Text>
);
}
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import * as NavigationActions from 'app/actions/navigation';
import Hashtag from './index';
describe('Hashtag', () => {
const baseProps = {
hashtag: 'test',
linkStyle: {color: 'red'},
};
test('should match snapshot', () => {
const wrapper = shallow(<Hashtag {...baseProps}/>);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('handlePress should open hashtag search', async () => {
const dismissAllModals = jest.spyOn(NavigationActions, 'dismissAllModals');
const popToRoot = jest.spyOn(NavigationActions, 'popToRoot');
const showSearchModal = jest.spyOn(NavigationActions, 'showSearchModal');
const props = {
...baseProps,
};
const wrapper = shallow(<Hashtag {...props}/>);
await wrapper.instance().handlePress();
expect(dismissAllModals).toHaveBeenCalled();
expect(popToRoot).toHaveBeenCalled();
expect(showSearchModal).toHaveBeenCalledWith('#test');
});
test('handlePress should call onHashtagPress if provided', async () => {
const dismissAllModals = jest.spyOn(NavigationActions, 'dismissAllModals');
const popToRoot = jest.spyOn(NavigationActions, 'popToRoot');
const showSearchModal = jest.spyOn(NavigationActions, 'showSearchModal');
const props = {
...baseProps,
onHashtagPress: jest.fn(),
};
const wrapper = shallow(<Hashtag {...props}/>);
await wrapper.instance().handlePress();
expect(dismissAllModals).not.toBeCalled();
expect(popToRoot).not.toBeCalled();
expect(showSearchModal).not.toBeCalled();
expect(props.onHashtagPress).toBeCalled();
});
});

View File

@@ -1,13 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import MarkdownCodeBlock from './markdown_code_block';
function mapStateToProps(state) {
@@ -16,12 +13,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MarkdownCodeBlock);
export default connect(mapStateToProps)(MarkdownCodeBlock);

View File

@@ -8,25 +8,23 @@ import {
Clipboard,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
import FormattedText from 'app/components/formatted_text';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import BottomSheet from 'app/utils/bottom_sheet';
import {getDisplayNameForLanguage} from 'app/utils/markdown';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import mattermostManaged from 'app/mattermost_managed';
import {goToScreen} from 'app/actions/navigation';
const MAX_LINES = 4;
export default class MarkdownCodeBlock extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
theme: PropTypes.object.isRequired,
language: PropTypes.string,
content: PropTypes.string.isRequired,
@@ -42,7 +40,7 @@ export default class MarkdownCodeBlock extends React.PureComponent {
};
handlePress = preventDoubleTap(() => {
const {actions, language, content} = this.props;
const {language, content} = this.props;
const {intl} = this.context;
const screen = 'Code';
const passProps = {
@@ -68,7 +66,7 @@ export default class MarkdownCodeBlock extends React.PureComponent {
});
}
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
});
handleLongPress = async () => {
@@ -153,9 +151,10 @@ export default class MarkdownCodeBlock extends React.PureComponent {
}
return (
<TouchableOpacity
<TouchableWithFeedback
onPress={this.handlePress}
onLongPress={this.handleLongPress}
type={'opacity'}
>
<View style={style.container}>
<View style={style.lineNumbers}>
@@ -173,7 +172,7 @@ export default class MarkdownCodeBlock extends React.PureComponent {
</View>
{language}
</View>
</TouchableOpacity>
</TouchableWithFeedback>
);
}
}

View File

@@ -1,13 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import {getDimensions} from 'app/selectors/device';
import MarkdownImage from './markdown_image';
@@ -19,12 +16,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showModalOverCurrentContext,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MarkdownImage);
export default connect(mapStateToProps)(MarkdownImage);

View File

@@ -11,12 +11,12 @@ import {
Platform,
StyleSheet,
Text,
TouchableHighlight,
View,
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import ProgressiveImage from 'app/components/progressive_image';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import CustomPropTypes from 'app/constants/custom_prop_types';
import EphemeralStore from 'app/store/ephemeral_store';
import mattermostManaged from 'app/mattermost_managed';
@@ -34,9 +34,6 @@ const VIEWPORT_IMAGE_REPLY_OFFSET = 13;
export default class MarkdownImage extends React.Component {
static propTypes = {
actions: PropTypes.shape({
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
children: PropTypes.node,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
@@ -177,7 +174,6 @@ export default class MarkdownImage extends React.Component {
originalWidth,
uri,
} = this.state;
const {actions} = this.props;
const link = this.getSource();
let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?'));
const extension = filename.split('.').pop();
@@ -199,7 +195,7 @@ export default class MarkdownImage extends React.Component {
},
}];
previewImageAtIndex([this.refs.item], 0, files, actions.showModalOverCurrentContext);
previewImageAtIndex([this.refs.item], 0, files);
};
loadImageSize = (source) => {
@@ -250,7 +246,7 @@ export default class MarkdownImage extends React.Component {
}
image = (
<TouchableHighlight
<TouchableWithFeedback
onLongPress={this.handleLinkLongPress}
onPress={this.handlePreviewImage}
style={{width, height}}
@@ -261,7 +257,7 @@ export default class MarkdownImage extends React.Component {
resizeMode='contain'
style={{width, height}}
/>
</TouchableHighlight>
</TouchableWithFeedback>
);
}
} else if (this.state.failed) {
@@ -275,12 +271,12 @@ export default class MarkdownImage extends React.Component {
if (image && this.props.linkDestination) {
image = (
<TouchableHighlight
<TouchableWithFeedback
onPress={this.handleLinkPress}
onLongPress={this.handleLinkLongPress}
>
{image}
</TouchableHighlight>
</TouchableWithFeedback>
);
}

View File

@@ -1,13 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import MarkdownTable from './markdown_table';
function mapStateToProps(state) {
@@ -16,12 +13,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MarkdownTable);
export default connect(mapStateToProps)(MarkdownTable);

View File

@@ -6,22 +6,21 @@ import React from 'react';
import {intlShape} from 'react-intl';
import {
ScrollView,
TouchableOpacity,
View,
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import {CELL_WIDTH} from 'app/components/markdown/markdown_table_cell/markdown_table_cell';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {CELL_WIDTH} from 'app/components/markdown/markdown_table_cell/markdown_table_cell';
import {goToScreen} from 'app/actions/navigation';
const MAX_HEIGHT = 300;
export default class MarkdownTable extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
children: PropTypes.node.isRequired,
numColumns: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired,
@@ -46,7 +45,6 @@ export default class MarkdownTable extends React.PureComponent {
};
handlePress = preventDoubleTap(() => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'Table';
const title = intl.formatMessage({
@@ -58,7 +56,7 @@ export default class MarkdownTable extends React.PureComponent {
tableWidth: this.getTableWidth(),
};
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
});
handleContainerLayout = (e) => {
@@ -128,7 +126,10 @@ export default class MarkdownTable extends React.PureComponent {
}
return (
<TouchableOpacity onPress={this.handlePress}>
<TouchableWithFeedback
onPress={this.handlePress}
type={'opacity'}
>
<ScrollView
contentContainerStyle={{width: this.getTableWidth()}}
onContentSizeChange={this.handleContentSizeChange}
@@ -141,7 +142,7 @@ export default class MarkdownTable extends React.PureComponent {
</ScrollView>
{moreRight}
{moreBelow}
</TouchableOpacity>
</TouchableWithFeedback>
);
}
}

View File

@@ -1,14 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import MarkdownTableImage from './markdown_table_image';
function mapStateToProps(state) {
@@ -18,12 +15,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MarkdownTableImage);
export default connect(mapStateToProps)(MarkdownTableImage);

View File

@@ -9,12 +9,10 @@ import {Text} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {getCurrentServerUrl} from 'app/init/credentials';
import {preventDoubleTap} from 'app/utils/tap';
import {goToScreen} from 'app/actions/navigation';
export default class MarkdownTableImage extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
children: PropTypes.node.isRequired,
source: PropTypes.string.isRequired,
textStyle: CustomPropTypes.Style.isRequired,
@@ -27,7 +25,6 @@ export default class MarkdownTableImage extends React.PureComponent {
};
handlePress = preventDoubleTap(() => {
const {actions} = this.props;
const {intl} = this.context;
const screen = 'TableImage';
const title = intl.formatMessage({
@@ -38,7 +35,7 @@ export default class MarkdownTableImage extends React.PureComponent {
imageSource: this.getImageSource(),
};
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
});
getImageSource = async () => {

View File

@@ -258,8 +258,6 @@ function getLastSibling(node) {
export function highlightMentions(ast, mentionKeys) {
const walker = ast.walker();
// console.warn(mentionKeys);
let e;
while ((e = walker.next())) {
if (!e.entering) {

View File

@@ -2966,28 +2966,28 @@ describe('Components.Markdown.transform', () => {
// Confirms that all parent, child, and sibling linkages are correct and go both ways.
function verifyAst(node) {
if (node.prev && node.prev.next !== node) {
console.error('node is not linked properly to prev');
console.error('node is not linked properly to prev'); //eslint-disable-line no-console
return false;
}
if (node.next && node.next.prev !== node) {
console.error('node is not linked properly to next');
console.error('node is not linked properly to next'); //eslint-disable-line no-console
return false;
}
if (!node.firstChild && node.lastChild) {
console.error('node has children, but is not linked to first child');
console.error('node has children, but is not linked to first child'); //eslint-disable-line no-console
return false;
}
if (node.firstChild && !node.lastChild) {
console.error('node has children, but is not linked to last child');
console.error('node has children, but is not linked to last child'); //eslint-disable-line no-console
return false;
}
for (let child = node.firstChild; child; child = child.next) {
if (child.parent !== node) {
console.error('node is not linked properly to child');
console.error('node is not linked properly to child'); //eslint-disable-line no-console
return false;
}
@@ -2996,18 +2996,18 @@ function verifyAst(node) {
}
if (!child.next && child !== node.lastChild) {
console.error('node children are not linked correctly');
console.error('node children are not linked correctly'); //eslint-disable-line no-console
return false;
}
}
if (node.firstChild && node.firstChild.prev) {
console.error('node\'s first child has previous sibling');
console.error('node\'s first child has previous sibling'); //eslint-disable-line no-console
return false;
}
if (node.lastChild && node.lastChild.next) {
console.error('node\'s last child has next sibling');
console.error('node\'s last child has next sibling'); //eslint-disable-line no-console
return false;
}

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AttachmentImage it matches snapshot 1`] = `
<TouchableWithoutFeedback
<TouchableWithFeedbackIOS
onPress={[Function]}
style={
Array [
@@ -13,6 +13,7 @@ exports[`AttachmentImage it matches snapshot 1`] = `
},
]
}
type="none"
>
<View
style={
@@ -42,5 +43,5 @@ exports[`AttachmentImage it matches snapshot 1`] = `
}
/>
</View>
</TouchableWithoutFeedback>
</TouchableWithFeedbackIOS>
`;

View File

@@ -1,176 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Image, TouchableWithoutFeedback, View} from 'react-native';
import ProgressiveImage from 'app/components/progressive_image';
import {isGifTooLarge, previewImageAtIndex, calculateDimensions} from 'app/utils/images';
import ImageCacheManager from 'app/utils/image_cache_manager';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const VIEWPORT_IMAGE_OFFSET = 100;
const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10;
export default class AttachmentImage extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
imageMetadata: PropTypes.object,
imageUrl: PropTypes.string,
theme: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
hasImage: Boolean(props.imageUrl),
imageUri: null,
};
}
componentDidMount() {
this.mounted = true;
const {imageUrl, imageMetadata} = this.props;
this.setViewPortMaxWidth();
if (imageMetadata) {
this.setImageDimensionsFromMeta(null, imageMetadata);
}
if (imageUrl) {
ImageCacheManager.cache(null, imageUrl, this.setImageUrl);
}
}
componentDidUpdate(prevProps) {
if (this.props.imageUrl && (prevProps.imageUrl !== this.props.imageUrl)) {
ImageCacheManager.cache(null, this.props.imageUrl, this.setImageUrl);
}
}
handlePreviewImage = () => {
const {actions, imageUrl} = this.props;
const {
imageUri: uri,
originalHeight,
originalWidth,
} = this.state;
let filename = imageUrl.substring(imageUrl.lastIndexOf('/') + 1, imageUrl.indexOf('?') === -1 ? imageUrl.length : imageUrl.indexOf('?'));
const extension = filename.split('.').pop();
if (extension === filename) {
const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
filename = `${filename}${ext}`;
}
const files = [{
caption: filename,
dimensions: {
height: originalHeight,
width: originalWidth,
},
source: {uri},
data: {
localPath: uri,
},
}];
previewImageAtIndex([this.refs.item], 0, files, actions.showModalOverCurrentContext);
};
setImageDimensions = (imageUri, dimensions, originalWidth, originalHeight) => {
if (this.mounted) {
this.setState({
...dimensions,
originalWidth,
originalHeight,
imageUri,
});
}
};
setImageDimensionsFromMeta = (imageUri, imageMetadata) => {
const dimensions = calculateDimensions(imageMetadata.height, imageMetadata.width, this.maxImageWidth);
this.setImageDimensions(imageUri, dimensions, imageMetadata.width, imageMetadata.height);
};
setImageUrl = (imageURL) => {
const {imageMetadata} = this.props;
if (imageMetadata) {
this.setImageDimensionsFromMeta(imageURL, imageMetadata);
return;
}
Image.getSize(imageURL, (width, height) => {
const dimensions = calculateDimensions(height, width, this.maxImageWidth);
this.setImageDimensions(imageURL, dimensions, width, height);
}, () => null);
};
setViewPortMaxWidth = () => {
const {deviceWidth, deviceHeight} = this.props;
const viewPortWidth = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
this.maxImageWidth = viewPortWidth - VIEWPORT_IMAGE_OFFSET;
};
render() {
const {imageMetadata, theme} = this.props;
const {hasImage, height, imageUri, width} = this.state;
if (!hasImage || isGifTooLarge(imageMetadata)) {
return null;
}
const style = getStyleSheet(theme);
let progressiveImage;
if (imageUri) {
progressiveImage = (
<ProgressiveImage
ref='image'
style={{height, width}}
imageUri={imageUri}
resizeMode='contain'
/>
);
} else {
progressiveImage = (<View style={{width, height}}/>);
}
return (
<TouchableWithoutFeedback
onPress={this.handlePreviewImage}
style={[style.container, {width: this.maxImageWidth + VIEWPORT_IMAGE_CONTAINER_OFFSET}]}
>
<View
ref='item'
style={[style.imageContainer, {width, height}]}
>
{progressiveImage}
</View>
</TouchableWithoutFeedback>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
marginTop: 5,
},
imageContainer: {
borderColor: changeOpacity(theme.centerChannelColor, 0.1),
borderWidth: 1,
borderRadius: 2,
flex: 1,
padding: 5,
},
};
});

View File

@@ -1,19 +1,175 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Image, View} from 'react-native';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import ProgressiveImage from 'app/components/progressive_image';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {isGifTooLarge, previewImageAtIndex, calculateDimensions} from 'app/utils/images';
import ImageCacheManager from 'app/utils/image_cache_manager';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import AttachmentImage from './attachment_image';
const VIEWPORT_IMAGE_OFFSET = 100;
const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10;
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showModalOverCurrentContext,
}, dispatch),
export default class AttachmentImage extends PureComponent {
static propTypes = {
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
imageMetadata: PropTypes.object,
imageUrl: PropTypes.string,
theme: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
hasImage: Boolean(props.imageUrl),
imageUri: null,
};
}
componentDidMount() {
this.mounted = true;
const {imageUrl, imageMetadata} = this.props;
this.setViewPortMaxWidth();
if (imageMetadata) {
this.setImageDimensionsFromMeta(null, imageMetadata);
}
if (imageUrl) {
ImageCacheManager.cache(null, imageUrl, this.setImageUrl);
}
}
componentDidUpdate(prevProps) {
if (this.props.imageUrl && (prevProps.imageUrl !== this.props.imageUrl)) {
ImageCacheManager.cache(null, this.props.imageUrl, this.setImageUrl);
}
}
handlePreviewImage = () => {
const {imageUrl} = this.props;
const {
imageUri: uri,
originalHeight,
originalWidth,
} = this.state;
let filename = imageUrl.substring(imageUrl.lastIndexOf('/') + 1, imageUrl.indexOf('?') === -1 ? imageUrl.length : imageUrl.indexOf('?'));
const extension = filename.split('.').pop();
if (extension === filename) {
const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
filename = `${filename}${ext}`;
}
const files = [{
caption: filename,
dimensions: {
height: originalHeight,
width: originalWidth,
},
source: {uri},
data: {
localPath: uri,
},
}];
previewImageAtIndex([this.refs.item], 0, files);
};
setImageDimensions = (imageUri, dimensions, originalWidth, originalHeight) => {
if (this.mounted) {
this.setState({
...dimensions,
originalWidth,
originalHeight,
imageUri,
});
}
};
setImageDimensionsFromMeta = (imageUri, imageMetadata) => {
const dimensions = calculateDimensions(imageMetadata.height, imageMetadata.width, this.maxImageWidth);
this.setImageDimensions(imageUri, dimensions, imageMetadata.width, imageMetadata.height);
};
setImageUrl = (imageURL) => {
const {imageMetadata} = this.props;
if (imageMetadata) {
this.setImageDimensionsFromMeta(imageURL, imageMetadata);
return;
}
Image.getSize(imageURL, (width, height) => {
const dimensions = calculateDimensions(height, width, this.maxImageWidth);
this.setImageDimensions(imageURL, dimensions, width, height);
}, () => null);
};
setViewPortMaxWidth = () => {
const {deviceWidth, deviceHeight} = this.props;
const viewPortWidth = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
this.maxImageWidth = viewPortWidth - VIEWPORT_IMAGE_OFFSET;
};
render() {
const {imageMetadata, theme} = this.props;
const {hasImage, height, imageUri, width} = this.state;
if (!hasImage || isGifTooLarge(imageMetadata)) {
return null;
}
const style = getStyleSheet(theme);
let progressiveImage;
if (imageUri) {
progressiveImage = (
<ProgressiveImage
ref='image'
style={{height, width}}
imageUri={imageUri}
resizeMode='contain'
/>
);
} else {
progressiveImage = (<View style={{width, height}}/>);
}
return (
<TouchableWithFeedback
onPress={this.handlePreviewImage}
style={[style.container, {width: this.maxImageWidth + VIEWPORT_IMAGE_CONTAINER_OFFSET}]}
type={'none'}
>
<View
ref='item'
style={[style.imageContainer, {width, height}]}
>
{progressiveImage}
</View>
</TouchableWithFeedback>
);
}
}
export default connect(null, mapDispatchToProps)(AttachmentImage);
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
marginTop: 5,
},
imageContainer: {
borderColor: changeOpacity(theme.centerChannelColor, 0.1),
borderWidth: 1,
borderRadius: 2,
flex: 1,
padding: 5,
},
};
});

View File

@@ -11,13 +11,10 @@ const originalGetSizeFn = Image.getSize;
import Preferences from 'mattermost-redux/constants/preferences';
import AttachmentImage from './attachment_image';
import AttachmentImage from './index';
describe('AttachmentImage', () => {
const baseProps = {
actions: {
showModalOverCurrentContext: jest.fn(),
},
deviceHeight: 256,
deviceWidth: 128,
imageMetadata: {width: 32, height: 32},

View File

@@ -113,7 +113,7 @@ export default class NetworkIndicator extends PureComponent {
}
if (this.props.isOnline) {
if (previousWebsocketStatus === RequestStatus.STARTED && websocketStatus === RequestStatus.SUCCESS) {
if (previousWebsocketStatus !== RequestStatus.SUCCESS && websocketStatus === RequestStatus.SUCCESS) {
// Show the connected animation only if we had a previous network status
this.connected();
clearTimeout(this.connectionRetryTimeout);
@@ -201,11 +201,11 @@ export default class NetworkIndicator extends PureComponent {
return ANDROID_TOP_PORTRAIT;
}
const isX = DeviceTypes.IS_IPHONE_X;
const iPhoneWithInsets = DeviceTypes.IS_IPHONE_WITH_INSETS;
if (isX && isLandscape) {
if (iPhoneWithInsets && isLandscape) {
return IOS_TOP_LANDSCAPE;
} else if (isX) {
} else if (iPhoneWithInsets) {
return IOSX_TOP_PORTRAIT;
} else if (isLandscape && !DeviceTypes.IS_TABLET) {
return IOS_TOP_LANDSCAPE;

View File

@@ -12,7 +12,6 @@ import {getUser, getCurrentUserId} from 'mattermost-redux/selectors/entities/use
import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isPostFlagged, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {goToScreen, showModalOverCurrentContext} from 'app/actions/navigation';
import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel';
import {isLandscape} from 'app/selectors/device';
@@ -96,8 +95,6 @@ function mapDispatchToProps(dispatch) {
removePost,
setPostTooltipVisible,
insertToDraft,
goToScreen,
showModalOverCurrentContext,
}, dispatch),
};
}

View File

@@ -6,26 +6,27 @@ import PropTypes from 'prop-types';
import {
Keyboard,
Platform,
TouchableHighlight,
View,
ViewPropTypes,
} from 'react-native';
import {intlShape} from 'react-intl';
import {Posts} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import PostBody from 'app/components/post_body';
import PostHeader from 'app/components/post_header';
import PostPreHeader from 'app/components/post_header/post_pre_header';
import PostProfilePicture from 'app/components/post_profile_picture';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {NavigationTypes} from 'app/constants';
import {fromAutoResponder} from 'app/utils/general';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {Posts} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {goToScreen, showModalOverCurrentContext} from 'app/actions/navigation';
import Config from 'assets/config';
@@ -35,8 +36,6 @@ export default class Post extends PureComponent {
createPost: PropTypes.func.isRequired,
insertToDraft: PropTypes.func.isRequired,
removePost: PropTypes.func.isRequired,
goToScreen: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
channelIsReadOnly: PropTypes.bool,
currentUserId: PropTypes.string.isRequired,
@@ -91,7 +90,7 @@ export default class Post extends PureComponent {
goToUserProfile = () => {
const {intl} = this.context;
const {actions, post} = this.props;
const {post} = this.props;
const screen = 'UserProfile';
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const passProps = {
@@ -100,7 +99,7 @@ export default class Post extends PureComponent {
Keyboard.dismiss();
requestAnimationFrame(() => {
actions.goToScreen(screen, title, passProps);
goToScreen(screen, title, passProps);
});
};
@@ -141,10 +140,11 @@ export default class Post extends PureComponent {
}],
};
this.props.actions.showModalOverCurrentContext(screen, passProps);
showModalOverCurrentContext(screen, passProps);
};
handlePress = preventDoubleTap(() => {
this.onPressDetected = true;
const {
onPress,
post,
@@ -157,6 +157,10 @@ export default class Post extends PureComponent {
} else if ((isPostEphemeral(post) || post.state === Posts.POST_DELETED) && !showLongPost) {
this.onRemovePost(post);
}
setTimeout(() => {
this.onPressDetected = false;
}, 300);
});
handleReply = preventDoubleTap(() => {
@@ -214,7 +218,7 @@ export default class Post extends PureComponent {
});
showPostOptions = () => {
if (this.postBodyRef?.current) {
if (this.postBodyRef?.current && !this.onPressDetected) {
this.postBodyRef.current.showPostOptions();
}
};
@@ -302,50 +306,52 @@ export default class Post extends PureComponent {
const rightColumnStyle = [style.rightColumn, (commentedOnPost && isLastReply && style.rightColumnPadding)];
return (
<TouchableHighlight
style={[style.postStyle, highlighted, padding(isLandscape)]}
onPress={this.handlePress}
onLongPress={this.showPostOptions}
delayLongPress={75}
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
>
<React.Fragment>
<PostPreHeader
isConsecutive={mergeMessage}
isFlagged={isFlagged}
isPinned={post.is_pinned}
rightColumnStyle={style.rightColumn}
skipFlaggedHeader={skipFlaggedHeader}
skipPinnedHeader={skipPinnedHeader}
theme={theme}
/>
<View style={[style.container, this.props.style, consecutiveStyle]}>
{userProfile}
<View style={rightColumnStyle}>
{postHeader}
<PostBody
ref={this.postBodyRef}
highlight={highlight}
channelIsReadOnly={channelIsReadOnly}
isLastPost={isLastPost}
isSearchResult={isSearchResult}
onFailedPostPress={this.handleFailedPostPress}
onHashtagPress={onHashtagPress}
onPermalinkPress={onPermalinkPress}
onPress={this.handlePress}
post={post}
replyBarStyle={replyBarStyle}
managedConfig={managedConfig}
isFlagged={isFlagged}
isReplyPost={isReplyPost}
showAddReaction={showAddReaction}
showLongPost={showLongPost}
location={location}
/>
<View style={[style.postStyle, highlighted, padding(isLandscape)]}>
<TouchableWithFeedback
onPress={this.handlePress}
onLongPress={this.showPostOptions}
delayLongPress={100}
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
cancelTouchOnPanning={true}
>
<React.Fragment>
<PostPreHeader
isConsecutive={mergeMessage}
isFlagged={isFlagged}
isPinned={post.is_pinned}
rightColumnStyle={style.preHeaderRightColumn}
skipFlaggedHeader={skipFlaggedHeader}
skipPinnedHeader={skipPinnedHeader}
theme={theme}
/>
<View style={[style.container, this.props.style, consecutiveStyle]}>
{userProfile}
<View style={rightColumnStyle}>
{postHeader}
<PostBody
ref={this.postBodyRef}
highlight={highlight}
channelIsReadOnly={channelIsReadOnly}
isLastPost={isLastPost}
isSearchResult={isSearchResult}
onFailedPostPress={this.handleFailedPostPress}
onHashtagPress={onHashtagPress}
onPermalinkPress={onPermalinkPress}
onPress={this.handlePress}
post={post}
replyBarStyle={replyBarStyle}
managedConfig={managedConfig}
isFlagged={isFlagged}
isReplyPost={isReplyPost}
showAddReaction={showAddReaction}
showLongPost={showLongPost}
location={location}
/>
</View>
</View>
</View>
</React.Fragment>
</TouchableHighlight>
</React.Fragment>
</TouchableWithFeedback>
</View>
);
}
}
@@ -354,6 +360,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
postStyle: {
overflow: 'hidden',
flex: 1,
},
container: {
flexDirection: 'row',
@@ -361,6 +368,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
pendingPost: {
opacity: 0.5,
},
preHeaderRightColumn: {
flex: 1,
flexDirection: 'column',
marginLeft: 2,
},
rightColumn: {
flex: 1,
flexDirection: 'column',
@@ -393,6 +405,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
replyBar: {
backgroundColor: theme.centerChannelColor,
opacity: 0.1,
marginLeft: 1,
marginRight: 7,
width: 3,
flexBasis: 3,

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PostAttachmentImage should match snapshot 1`] = `
<TouchableWithoutFeedback
<TouchableWithFeedbackIOS
onPress={[Function]}
style={
Array [
@@ -16,6 +16,7 @@ exports[`PostAttachmentImage should match snapshot 1`] = `
},
]
}
type="none"
>
<View>
<ForwardRef(forwardConnectRef)
@@ -38,5 +39,5 @@ exports[`PostAttachmentImage should match snapshot 1`] = `
}
/>
</View>
</TouchableWithoutFeedback>
</TouchableWithFeedbackIOS>
`;

View File

@@ -3,9 +3,10 @@
import PropTypes from 'prop-types';
import React from 'react';
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native';
import {StyleSheet, View} from 'react-native';
import ProgressiveImage from 'app/components/progressive_image';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {isGifTooLarge} from 'app/utils/images';
export default class PostAttachmentImage extends React.PureComponent {
@@ -40,9 +41,10 @@ export default class PostAttachmentImage extends React.PureComponent {
// Note that TouchableWithoutFeedback only works if its child is a View
return (
<TouchableWithoutFeedback
<TouchableWithFeedback
onPress={this.handlePress}
style={[styles.imageContainer, {height: this.props.height}]}
type={'none'}
>
<View ref={this.image}>
<ProgressiveImage
@@ -52,7 +54,7 @@ export default class PostAttachmentImage extends React.PureComponent {
onError={this.props.onError}
/>
</View>
</TouchableWithoutFeedback>
</TouchableWithFeedback>
);
}
}

View File

@@ -44,14 +44,14 @@ exports[`PostAttachmentOpenGraph should match snapshot, without image and descri
}
}
>
<TouchableOpacity
activeOpacity={0.2}
<TouchableWithFeedbackIOS
onPress={[Function]}
style={
Object {
"flex": 1,
}
}
type="opacity"
>
<Text
ellipsizeMode="tail"
@@ -71,7 +71,7 @@ exports[`PostAttachmentOpenGraph should match snapshot, without image and descri
>
Title
</Text>
</TouchableOpacity>
</TouchableWithFeedbackIOS>
</View>
<View
style={
@@ -90,8 +90,9 @@ exports[`PostAttachmentOpenGraph should match snapshot, without image and descri
]
}
>
<TouchableWithoutFeedback
<TouchableWithFeedbackIOS
onPress={[Function]}
type="none"
>
<Image
resizeMode="contain"
@@ -107,7 +108,7 @@ exports[`PostAttachmentOpenGraph should match snapshot, without image and descri
]
}
/>
</TouchableWithoutFeedback>
</TouchableWithFeedbackIOS>
</View>
</View>
`;
@@ -133,14 +134,14 @@ exports[`PostAttachmentOpenGraph should match snapshot, without site_name 1`] =
}
}
>
<TouchableOpacity
activeOpacity={0.2}
<TouchableWithFeedbackIOS
onPress={[Function]}
style={
Object {
"flex": 1,
}
}
type="opacity"
>
<Text
ellipsizeMode="tail"
@@ -160,7 +161,7 @@ exports[`PostAttachmentOpenGraph should match snapshot, without site_name 1`] =
>
Title
</Text>
</TouchableOpacity>
</TouchableWithFeedbackIOS>
</View>
</View>
`;
@@ -186,14 +187,14 @@ exports[`PostAttachmentOpenGraph should match snapshot, without title and url 1`
}
}
>
<TouchableOpacity
activeOpacity={0.2}
<TouchableWithFeedbackIOS
onPress={[Function]}
style={
Object {
"flex": 1,
}
}
type="opacity"
>
<Text
ellipsizeMode="tail"
@@ -213,7 +214,7 @@ exports[`PostAttachmentOpenGraph should match snapshot, without title and url 1`
>
https://mattermost.com/
</Text>
</TouchableOpacity>
</TouchableWithFeedbackIOS>
</View>
</View>
`;
@@ -264,8 +265,9 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage
]
}
>
<TouchableWithoutFeedback
<TouchableWithFeedbackIOS
onPress={[Function]}
type="none"
>
<Image
resizeMode="contain"
@@ -281,6 +283,6 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage
]
}
/>
</TouchableWithoutFeedback>
</TouchableWithFeedbackIOS>
</View>
`;

View File

@@ -6,8 +6,6 @@ import {bindActionCreators} from 'redux';
import {getOpenGraphMetadata} from 'mattermost-redux/actions/posts';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import {getDimensions} from 'app/selectors/device';
import PostAttachmentOpenGraph from './post_attachment_opengraph';
@@ -22,7 +20,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getOpenGraphMetadata,
showModalOverCurrentContext,
}, dispatch),
};
}

View File

@@ -7,12 +7,11 @@ import {
Image,
Linking,
Text,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native';
import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {DeviceTypes} from 'app/constants';
import ImageCacheManager from 'app/utils/image_cache_manager';
@@ -28,7 +27,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getOpenGraphMetadata: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
@@ -195,7 +193,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
originalWidth,
originalHeight,
} = this.state;
const {actions} = this.props;
const filename = this.getFilename(link);
const files = [{
@@ -210,7 +207,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
},
}];
previewImageAtIndex([this.refs.item], 0, files, actions.showModalOverCurrentContext);
previewImageAtIndex([this.refs.item], 0, files);
};
renderDescription = () => {
@@ -255,15 +252,16 @@ export default class PostAttachmentOpenGraph extends PureComponent {
ref='item'
style={[style.imageContainer, {width, height}]}
>
<TouchableWithoutFeedback
<TouchableWithFeedback
onPress={this.handlePreviewImage}
type={'none'}
>
<Image
style={[style.image, {width, height}]}
source={source}
resizeMode='contain'
/>
</TouchableWithoutFeedback>
</TouchableWithFeedback>
</View>
);
};
@@ -302,9 +300,10 @@ export default class PostAttachmentOpenGraph extends PureComponent {
if (title) {
siteTitle = (
<View style={style.wrapper}>
<TouchableOpacity
<TouchableWithFeedback
style={style.flex}
onPress={this.goToLink}
type={'opacity'}
>
<Text
style={[style.siteSubtitle, {marginRight: isReplyPost ? 10 : 0}]}
@@ -313,7 +312,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
>
{title}
</Text>
</TouchableOpacity>
</TouchableWithFeedback>
</View>
);
}

View File

@@ -3,12 +3,9 @@
import React from 'react';
import {shallow} from 'enzyme';
import {
Image,
TouchableWithoutFeedback,
TouchableOpacity,
} from 'react-native';
import {Image} from 'react-native';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import Preferences from 'mattermost-redux/constants/preferences';
import PostAttachmentOpenGraph from './post_attachment_opengraph';
@@ -25,7 +22,6 @@ describe('PostAttachmentOpenGraph', () => {
const baseProps = {
actions: {
getOpenGraphMetadata: jest.fn(),
showModalOverCurrentContext: jest.fn(),
},
deviceHeight: 600,
deviceWidth: 400,
@@ -50,7 +46,7 @@ describe('PostAttachmentOpenGraph', () => {
wrapper.setProps({openGraphData});
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper.find(TouchableOpacity).exists()).toEqual(true);
expect(wrapper.find(TouchableWithFeedback).exists()).toEqual(true);
});
test('should match snapshot, without site_name', () => {
@@ -86,7 +82,7 @@ describe('PostAttachmentOpenGraph', () => {
expect(wrapper.instance().renderImage()).toMatchSnapshot();
expect(wrapper.state('hasImage')).toEqual(false);
expect(wrapper.find(Image).exists()).toEqual(false);
expect(wrapper.find(TouchableWithoutFeedback).exists()).toEqual(false);
expect(wrapper.find(TouchableWithFeedback).exists()).toEqual(false);
const images = [{height: 440, width: 1200, url: 'https://mattermost.com/logo.png'}];
const openGraphDataWithImage = {...openGraphData, images};
@@ -95,7 +91,7 @@ describe('PostAttachmentOpenGraph', () => {
expect(wrapper.instance().renderImage()).toMatchSnapshot();
expect(wrapper.state('hasImage')).toEqual(true);
expect(wrapper.find(Image).exists()).toEqual(true);
expect(wrapper.find(TouchableWithoutFeedback).exists()).toEqual(true);
expect(wrapper.find(TouchableWithFeedback).exists()).toEqual(true);
});
test('should match state and snapshot, on renderDescription', () => {

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {General, Posts} from 'mattermost-redux/constants';
@@ -22,8 +21,6 @@ import {
} from 'mattermost-redux/utils/post_utils';
import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import {getDimensions} from 'app/selectors/device';
import {hasEmojisOnly} from 'app/utils/emoji_utils';
@@ -32,7 +29,7 @@ import PostBody from './post_body';
const POST_TIMEOUT = 20000;
function makeMapStateToProps() {
export function makeMapStateToProps() {
const memoizeHasEmojisOnly = memoizeResult((message, customEmojis) => hasEmojisOnly(message, customEmojis));
const getReactionsForPost = makeGetReactionsForPost();
@@ -61,9 +58,10 @@ function makeMapStateToProps() {
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
const isAdmin = checkIsAdmin(roles);
const isSystemAdmin = checkIsSystemAdmin(roles);
const channelIsArchived = channel?.delete_at !== 0; //eslint-disable-line camelcase
let canDelete = false;
if (post && !ownProps.channelIsArchived) {
if (post && !channelIsArchived) {
canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin);
}
@@ -105,12 +103,4 @@ function makeMapStateToProps() {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showModalOverCurrentContext,
}, dispatch),
};
}
export default connect(makeMapStateToProps, mapDispatchToProps, null, {forwardRef: true})(PostBody);
export default connect(makeMapStateToProps, null, null, {forwardRef: true})(PostBody);

View File

@@ -0,0 +1,116 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import * as PostUtils from 'mattermost-redux/utils/post_utils';
import {makeMapStateToProps} from './index.js';
jest.mock('mattermost-redux/selectors/entities/channels', () => {
const channels = require.requireActual('mattermost-redux/selectors/entities/channels');
return {
...channels,
getChannel: jest.fn(),
canManageChannelMembers: jest.fn(),
getCurrentChannelId: jest.fn(),
};
});
jest.mock('mattermost-redux/selectors/entities/preferences', () => {
const preferences = require.requireActual('mattermost-redux/selectors/entities/preferences');
return {
...preferences,
getTheme: jest.fn(),
};
});
jest.mock('mattermost-redux/selectors/entities/general', () => {
const general = require.requireActual('mattermost-redux/selectors/entities/general');
return {
...general,
getConfig: jest.fn(),
getLicense: jest.fn().mockReturnValue({}),
};
});
jest.mock('mattermost-redux/selectors/entities/users', () => {
const users = require.requireActual('mattermost-redux/selectors/entities/users');
return {
...users,
getCurrentUserId: jest.fn(),
getCurrentUserRoles: jest.fn(),
};
});
jest.mock('mattermost-redux/selectors/entities/teams', () => {
const teams = require.requireActual('mattermost-redux/selectors/entities/teams');
return {
...teams,
getCurrentTeamId: jest.fn(),
};
});
jest.mock('mattermost-redux/selectors/entities/emojis', () => {
const emojis = require.requireActual('mattermost-redux/selectors/entities/emojis');
return {
...emojis,
getCustomEmojisByName: jest.fn(),
};
});
jest.mock('mattermost-redux/selectors/entities/posts', () => {
const posts = require.requireActual('mattermost-redux/selectors/entities/posts');
return {
...posts,
makeGetReactionsForPost: () => jest.fn(),
};
});
jest.mock('app/selectors/device', () => ({
getDimensions: jest.fn(),
}));
describe('makeMapStateToProps', () => {
const defaultState = {
entities: {
general: {
serverVersion: '',
},
},
};
const defaultOwnProps = {
post: {},
};
test('should not call canDeletePost if post is not defined', () => {
const canDeletePost = jest.spyOn(PostUtils, 'canDeletePost');
const mapStateToProps = makeMapStateToProps();
const ownProps = {
post: '',
};
const props = mapStateToProps(defaultState, ownProps);
expect(props.canDelete).toBe(false);
expect(canDeletePost).not.toHaveBeenCalled();
});
test('should not call canDeletePost if post is defined and channel is archived', () => {
const canDeletePost = jest.spyOn(PostUtils, 'canDeletePost');
const mapStateToProps = makeMapStateToProps();
getChannel.mockReturnValueOnce({delete_at: 1}); //eslint-disable-line camelcase
const props = mapStateToProps(defaultState, defaultOwnProps);
expect(props.canDelete).toBe(false);
expect(canDeletePost).not.toHaveBeenCalled();
});
test('should call canDeletePost if post is defined and channel is not archived', () => {
const canDeletePost = jest.spyOn(PostUtils, 'canDeletePost');
const mapStateToProps = makeMapStateToProps();
getChannel.mockReturnValue({delete_at: 0}); //eslint-disable-line camelcase
mapStateToProps(defaultState, defaultOwnProps);
expect(canDeletePost).toHaveBeenCalledTimes(1);
});
});

View File

@@ -6,12 +6,10 @@ import PropTypes from 'prop-types';
import {
Keyboard,
ScrollView,
TouchableOpacity,
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import Icon from 'react-native-vector-icons/Ionicons';
import {Posts} from 'mattermost-redux/constants';
import CombinedSystemMessage from 'app/components/combined_system_message';
@@ -19,11 +17,13 @@ import FormattedText from 'app/components/formatted_text';
import Markdown from 'app/components/markdown';
import MarkdownEmoji from 'app/components/markdown/markdown_emoji';
import ShowMoreButton from 'app/components/show_more_button';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {emptyFunction} from 'app/utils/general';
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import telemetry from 'app/telemetry';
@@ -36,9 +36,6 @@ const SHOW_MORE_HEIGHT = 60;
export default class PostBody extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
canDelete: PropTypes.bool,
channelIsReadOnly: PropTypes.bool.isRequired,
deviceHeight: PropTypes.number.isRequired,
@@ -135,7 +132,6 @@ export default class PostBody extends PureComponent {
onHashtagPress,
onPermalinkPress,
post,
actions,
} = this.props;
const screen = 'LongPost';
const passProps = {
@@ -150,7 +146,7 @@ export default class PostBody extends PureComponent {
},
};
actions.showModalOverCurrentContext(screen, passProps, options);
showModalOverCurrentContext(screen, passProps, options);
});
showPostOptions = () => {
@@ -167,7 +163,6 @@ export default class PostBody extends PureComponent {
post,
showAddReaction,
location,
actions,
} = this.props;
if (isSystemMessage && (!canDelete || hasBeenDeleted)) {
@@ -193,7 +188,7 @@ export default class PostBody extends PureComponent {
Keyboard.dismiss();
requestAnimationFrame(() => {
actions.showModalOverCurrentContext(screen, passProps);
showModalOverCurrentContext(screen, passProps);
});
};
@@ -441,16 +436,17 @@ export default class PostBody extends PureComponent {
<View style={replyBarStyle}/>
{body}
{isFailed &&
<TouchableOpacity
<TouchableWithFeedback
onPress={onFailedPostPress}
style={style.retry}
type={'opacity'}
>
<Icon
name='ios-information-circle-outline'
size={26}
color={theme.errorTextColor}
/>
</TouchableOpacity>
</TouchableWithFeedback>
}
</View>
);

View File

@@ -12,9 +12,6 @@ import PostBody from './post_body.js';
describe('PostBody', () => {
const baseProps = {
actions: {
showModalOverCurrentContext: jest.fn(),
},
canDelete: true,
channelIsReadOnly: false,
deviceHeight: 1920,

View File

@@ -10,7 +10,6 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getOpenGraphMetadataForUrl} from 'mattermost-redux/selectors/entities/posts';
import {getBool, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import {ViewTypes} from 'app/constants';
import {getDimensions} from 'app/selectors/device';
import {extractFirstLink} from 'app/utils/url';
@@ -76,7 +75,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getRedirectLocation,
showModalOverCurrentContext,
}, dispatch),
};
}

View File

@@ -9,7 +9,6 @@ import {
Linking,
Platform,
StyleSheet,
TouchableOpacity,
StatusBar,
} from 'react-native';
import {YouTubeStandaloneAndroid, YouTubeStandaloneIOS} from 'react-native-youtube';
@@ -20,6 +19,7 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
import PostAttachmentImage from 'app/components/post_attachment_image';
import ProgressiveImage from 'app/components/progressive_image';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {DeviceTypes} from 'app/constants';
import CustomPropTypes from 'app/constants/custom_prop_types';
@@ -38,7 +38,6 @@ export default class PostBodyAdditionalContent extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getRedirectLocation: PropTypes.func.isRequired,
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
baseTextStyle: CustomPropTypes.Style,
blockStyles: PropTypes.object,
@@ -231,9 +230,10 @@ export default class PostBodyAdditionalContent extends PureComponent {
const thumbUrl = `https://i.ytimg.com/vi/${videoId}/default.jpg`;
return (
<TouchableOpacity
<TouchableWithFeedback
style={[styles.imageContainer, {height: height || MAX_YOUTUBE_IMAGE_HEIGHT}]}
onPress={this.playYouTubeVideo}
type={'opacity'}
>
<ProgressiveImage
isBackgroundImage={true}
@@ -243,17 +243,18 @@ export default class PostBodyAdditionalContent extends PureComponent {
resizeMode='cover'
onError={this.handleLinkLoadError}
>
<TouchableOpacity
<TouchableWithFeedback
style={styles.playButton}
onPress={this.playYouTubeVideo}
type={'opacity'}
>
<Image
source={require('assets/images/icons/youtube-play-icon.png')}
onPress={this.playYouTubeVideo}
/>
</TouchableOpacity>
</TouchableWithFeedback>
</ProgressiveImage>
</TouchableOpacity>
</TouchableWithFeedback>
);
}
@@ -410,7 +411,6 @@ export default class PostBodyAdditionalContent extends PureComponent {
handlePreviewImage = (imageRef) => {
const {shortenedLink} = this.state;
let {link} = this.props;
const {actions} = this.props;
if (shortenedLink) {
link = shortenedLink;
}
@@ -432,7 +432,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
},
}];
previewImageAtIndex([imageRef], 0, files, actions.showModalOverCurrentContext);
previewImageAtIndex([imageRef], 0, files);
};
playYouTubeVideo = () => {

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