forked from Ivasoft/mattermost-mobile
Compare commits
55 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d520d1909 | ||
|
|
d15420ae54 | ||
|
|
08fe8e529b | ||
|
|
44669d93bb | ||
|
|
cfc65c0250 | ||
|
|
9eb1aa1ad0 | ||
|
|
5d923f3c4a | ||
|
|
01eb3ce8b6 | ||
|
|
0fa96be8ee | ||
|
|
c983caf583 | ||
|
|
95f321c518 | ||
|
|
5ec12c8784 | ||
|
|
993143b0f8 | ||
|
|
53c4df74c6 | ||
|
|
fdc894c00f | ||
|
|
04bedbc954 | ||
|
|
9dd36bf15e | ||
|
|
9d28eb043c | ||
|
|
188bfecf17 | ||
|
|
3eb8d3857b | ||
|
|
e9e1dc0541 | ||
|
|
c55dcaf598 | ||
|
|
d5dd4380d9 | ||
|
|
486917d692 | ||
|
|
53657536fc | ||
|
|
cd12480577 | ||
|
|
643d45b33c | ||
|
|
d3b5281ecb | ||
|
|
d5ea75171c | ||
|
|
73d20fdcdf | ||
|
|
2eb723a6dc | ||
|
|
5fafe376fa | ||
|
|
0818489b47 | ||
|
|
14593339b3 | ||
|
|
80fad8c11c | ||
|
|
f2e06aa304 | ||
|
|
41bcc75df9 | ||
|
|
03692f1975 | ||
|
|
8927e5921a | ||
|
|
bd4a119c05 | ||
|
|
6b4b4ce75f | ||
|
|
cdc020fc9c | ||
|
|
5f2d840f27 | ||
|
|
dca0d5e75b | ||
|
|
4dbdf42ebd | ||
|
|
cf2262dbc1 | ||
|
|
478bf42b62 | ||
|
|
aada9efb2b | ||
|
|
4ada33b50d | ||
|
|
b8540b42dd | ||
|
|
9bbbf67cc8 | ||
|
|
54f403c354 | ||
|
|
80282c6df1 | ||
|
|
f99d260628 | ||
|
|
2bd67deeea |
23
.circleci/config.yml
Normal file
23
.circleci/config.yml
Normal 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
|
||||
2
Makefile
2
Makefile
@@ -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)
|
||||
|
||||
48
NOTICE.txt
48
NOTICE.txt
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
352
app/actions/navigation/index.js
Normal file
352
app/actions/navigation/index.js
Normal 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.
|
||||
}
|
||||
}
|
||||
473
app/actions/navigation/index.test.js
Normal file
473
app/actions/navigation/index.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
fetchMyChannelsAndMembers,
|
||||
getChannelByNameAndTeamName,
|
||||
markChannelAsRead,
|
||||
leaveChannel as serviceLeaveChannel, markChannelAsViewed,
|
||||
markChannelAsViewed,
|
||||
leaveChannel as serviceLeaveChannel,
|
||||
selectChannel,
|
||||
getChannelStats,
|
||||
} from 'mattermost-redux/actions/channels';
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 []}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
38
app/components/__snapshots__/swiper.test.js.snap
Normal file
38
app/components/__snapshots__/swiper.test.js.snap
Normal 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>
|
||||
`;
|
||||
@@ -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) => {
|
||||
|
||||
@@ -12,9 +12,6 @@ jest.useFakeTimers();
|
||||
|
||||
describe('AnnouncementBanner', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
goToScreen: jest.fn(),
|
||||
},
|
||||
bannerColor: '#ddd',
|
||||
bannerDismissed: false,
|
||||
bannerEnabled: true,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)))));
|
||||
};
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ export default class Fade extends PureComponent {
|
||||
opacity: fadeAnim,
|
||||
transform: disableScale ? [] : [{scale: fadeAnim}],
|
||||
}}
|
||||
pointerEvents={'box-none'}
|
||||
>
|
||||
{this.props.children}
|
||||
</Animated.View>
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -13,7 +13,6 @@ exports[`PostAttachmentOpenGraph should match snapshot with a single image file
|
||||
>
|
||||
<FileAttachment
|
||||
canDownloadFiles={true}
|
||||
deviceWidth={660}
|
||||
file={
|
||||
Object {
|
||||
"caption": "image.png",
|
||||
|
||||
@@ -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%',
|
||||
|
||||
45
app/components/file_attachment_list/file_attachment.test.js
Normal file
45
app/components/file_attachment_list/file_attachment.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -73,7 +73,6 @@ function getBaseProps(triggerId, elements, introductionText) {
|
||||
|
||||
return {
|
||||
actions: {
|
||||
showModal: jest.fn(),
|
||||
submitInteractiveDialog: jest.fn(),
|
||||
},
|
||||
triggerId,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
61
app/components/markdown/hashtag/index.test.js
Normal file
61
app/components/markdown/hashtag/index.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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},
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
116
app/components/post_body/index.test.js
Normal file
116
app/components/post_body/index.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -12,9 +12,6 @@ import PostBody from './post_body.js';
|
||||
|
||||
describe('PostBody', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
showModalOverCurrentContext: jest.fn(),
|
||||
},
|
||||
canDelete: true,
|
||||
channelIsReadOnly: false,
|
||||
deviceHeight: 1920,
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user