MM-30164 fix safe area insets (#4979)

* MM-30164 fix safe area insets

* Fix unit test setup mock for react-native-device-info

* Add insets for edit profile screen

* Fix about screen

* Fix theme screen

* Lock phone screen to portrait

* fix unit tests

* Fix autocomplete layout
This commit is contained in:
Elias Nahum 2020-11-23 20:10:09 -03:00 committed by GitHub
parent 05beb6c64b
commit dcaaaee44c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
316 changed files with 3800 additions and 5091 deletions

View file

@ -9,9 +9,15 @@ import {Preferences} from '@mm-redux/constants';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import EventEmmiter from '@mm-redux/utils/event_emitter';
import {DeviceTypes, NavigationTypes} from '@constants';
import EphemeralStore from '@store/ephemeral_store';
import Store from '@store/store';
import {NavigationTypes} from '@constants';
Navigation.setDefaultOptions({
layout: {
orientation: [DeviceTypes.IS_TABLET ? undefined : 'portrait'],
},
});
function getThemeFromState() {
const state = Store.redux?.getState() || {};

View file

@ -1,121 +1,133 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnnouncementBanner should match snapshot 1`] = `
<ForwardRef(AnimatedComponentWrapper)
style={
Array [
Object {
"overflow": "hidden",
"paddingHorizontal": 10,
"position": "absolute",
"top": 0,
"width": "100%",
<Unknown
bannerColor="#ddd"
bannerDismissed={false}
bannerEnabled={true}
bannerText="Banner Text"
bannerTextColor="#fff"
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
Object {
"backgroundColor": "#ddd",
"height": 0,
},
]
}
>
<ForwardRef
onPress={[Function]}
style={
Array [
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
},
null,
]
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": "span",
"timeZone": null,
}
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
]
}
>
<RemoveMarkdown
value="Banner Text"
/>
</Text>
<CompassIcon
color="#fff"
name="info-outline"
size={16}
/>
</ForwardRef>
</ForwardRef(AnimatedComponentWrapper)>
}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"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",
}
}
/>
`;
exports[`AnnouncementBanner should match snapshot 2`] = `
<ForwardRef(AnimatedComponentWrapper)
style={
Array [
Object {
"overflow": "hidden",
"paddingHorizontal": 10,
"position": "absolute",
"top": 0,
"width": "100%",
<Unknown
bannerColor="#ddd"
bannerDismissed={false}
bannerEnabled={false}
bannerText="Banner Text"
bannerTextColor="#fff"
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
Object {
"backgroundColor": "#ddd",
"height": 0,
},
]
}
>
<ForwardRef
onPress={[Function]}
style={
Array [
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
},
null,
]
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": "span",
"timeZone": null,
}
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
]
}
>
<RemoveMarkdown
value="Banner Text"
/>
</Text>
<CompassIcon
color="#fff"
name="info-outline"
size={16}
/>
</ForwardRef>
</ForwardRef(AnimatedComponentWrapper)>
}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
`;

View file

@ -1,62 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import React, {useEffect, useRef, useState} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
InteractionManager,
StyleSheet,
Text,
TouchableOpacity,
} from 'react-native';
import {intlShape} from 'react-intl';
import {injectIntl} from 'react-intl';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {goToScreen} from '@actions/navigation';
import CompassIcon from '@components/compass_icon';
import RemoveMarkdown from '@components/remove_markdown';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
import {goToScreen} from '@actions/navigation';
import {ViewTypes} from '@constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
const {View: AnimatedView} = Animated;
export default class AnnouncementBanner extends PureComponent {
static propTypes = {
bannerColor: PropTypes.string,
bannerDismissed: PropTypes.bool,
bannerEnabled: PropTypes.bool,
bannerText: PropTypes.string,
bannerTextColor: PropTypes.string,
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
};
static contextTypes = {
intl: intlShape,
};
state = {
bannerHeight: new Animated.Value(0),
};
componentDidMount() {
const {bannerDismissed, bannerEnabled, bannerText} = this.props;
const showBanner = bannerEnabled && !bannerDismissed && Boolean(bannerText);
this.toggleBanner(showBanner);
}
componentDidUpdate(prevProps) {
if (this.props.bannerText !== prevProps.bannerText ||
this.props.bannerEnabled !== prevProps.bannerEnabled ||
this.props.bannerDismissed !== prevProps.bannerDismissed
) {
const showBanner = this.props.bannerEnabled && !this.props.bannerDismissed && Boolean(this.props.bannerText);
this.toggleBanner(showBanner);
}
}
handlePress = () => {
const {intl} = this.context;
const AnnouncementBanner = injectIntl((props) => {
const {bannerColor, bannerDismissed, bannerEnabled, bannerText, bannerTextColor, intl} = props;
const insets = useSafeAreaInsets();
const translateY = useRef(new Animated.Value(0)).current;
const [visible, setVisible] = useState(false);
const [navHeight, setNavHeight] = useState(0);
const handlePress = () => {
const screen = 'ExpandedAnnouncementBanner';
const title = intl.formatMessage({
id: 'mobile.announcement_banner.title',
@ -66,80 +37,88 @@ export default class AnnouncementBanner extends PureComponent {
goToScreen(screen, title);
};
toggleBanner = (show = true) => {
const value = show ? 38 : 0;
if (show && !this.state.visible) {
this.setState({visible: show});
}
useEffect(() => {
const handleNavbarHeight = (height) => {
setNavHeight(height);
};
InteractionManager.runAfterInteractions(() => {
Animated.timing(this.state.bannerHeight, {
toValue: value,
duration: 350,
useNativeDriver: false,
}).start(() => {
if (this.state.visible !== show) {
this.setState({visible: show});
}
});
});
EventEmitter.on(ViewTypes.CHANNEL_NAV_BAR_CHANGED, handleNavbarHeight);
return () => EventEmitter.off(ViewTypes.CHANNEL_NAV_BAR_CHANGED, handleNavbarHeight);
}, [insets]);
useEffect(() => {
const showBanner = bannerEnabled && !bannerDismissed && Boolean(bannerText);
setVisible(showBanner);
EventEmitter.emit(ViewTypes.INDICATOR_BAR_VISIBLE, showBanner);
}, [bannerDismissed, bannerEnabled, bannerText]);
useEffect(() => {
Animated.timing(translateY, {
toValue: visible ? navHeight : insets.top,
duration: 50,
useNativeDriver: true,
}).start();
}, [visible, navHeight]);
if (!visible) {
return null;
}
const bannerStyle = {
backgroundColor: bannerColor,
height: ViewTypes.INDICATOR_BAR_HEIGHT,
transform: [{translateY}],
};
render() {
if (!this.state.visible) {
return null;
}
const bannerTextStyle = {
color: bannerTextColor,
};
const {bannerHeight} = this.state;
const {
bannerColor,
bannerText,
bannerTextColor,
isLandscape,
} = this.props;
const bannerStyle = {
backgroundColor: bannerColor,
height: bannerHeight,
};
const bannerTextStyle = {
color: bannerTextColor,
};
return (
<AnimatedView
style={[style.bannerContainer, bannerStyle]}
return (
<AnimatedView
style={[style.bannerContainer, bannerStyle]}
>
<TouchableOpacity
onPress={handlePress}
style={[style.wrapper, {marginLeft: insets.left, marginRight: insets.right}]}
>
<TouchableOpacity
onPress={this.handlePress}
style={[style.wrapper, padding(isLandscape)]}
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={[style.bannerText, bannerTextStyle]}
>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={[style.bannerText, bannerTextStyle]}
>
<RemoveMarkdown value={bannerText}/>
</Text>
<CompassIcon
color={bannerTextColor}
name='info-outline'
size={16}
/>
</TouchableOpacity>
</AnimatedView>
);
}
}
<RemoveMarkdown value={bannerText}/>
</Text>
<CompassIcon
color={bannerTextColor}
name='information-outline'
size={16}
/>
</TouchableOpacity>
</AnimatedView>
);
});
AnnouncementBanner.propTypes = {
bannerColor: PropTypes.string,
bannerDismissed: PropTypes.bool,
bannerEnabled: PropTypes.bool,
bannerText: PropTypes.string,
bannerTextColor: PropTypes.string,
};
export default AnnouncementBanner;
const style = StyleSheet.create({
bannerContainer: {
elevation: 2,
paddingHorizontal: 10,
position: 'absolute',
top: 0,
overflow: 'hidden',
width: '100%',
zIndex: 2,
},
wrapper: {
alignItems: 'center',

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
@ -18,11 +18,10 @@ describe('AnnouncementBanner', () => {
bannerText: 'Banner Text',
bannerTextColor: '#fff',
theme: Preferences.THEMES.default,
isLandscape: false,
};
test('should match snapshot', () => {
const wrapper = shallow(
const wrapper = shallowWithIntl(
<AnnouncementBanner {...baseProps}/>,
);

View file

@ -4,7 +4,6 @@
import {connect} from 'react-redux';
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {isLandscape} from 'app/selectors/device';
@ -21,7 +20,6 @@ function mapStateToProps(state) {
bannerEnabled: config.EnableBanner === 'true' && license.IsLicensed === 'true',
bannerText: config.BannerText,
bannerTextColor: config.BannerTextColor || '#000',
theme: getTheme(state),
isLandscape: isLandscape(state),
};
}

View file

@ -35,7 +35,6 @@ export default class AtMention extends PureComponent {
teamMembers: PropTypes.array,
theme: PropTypes.object.isRequired,
value: PropTypes.string,
isLandscape: PropTypes.bool.isRequired,
nestedScrollEnabled: PropTypes.bool,
useChannelMentions: PropTypes.bool.isRequired,
groups: PropTypes.array,
@ -213,7 +212,6 @@ export default class AtMention extends PureComponent {
id={section.id}
defaultMessage={section.defaultMessage}
theme={this.props.theme}
isLandscape={this.props.isLandscape}
isFirstSection={isFirstSection}
/>
);

View file

@ -10,7 +10,6 @@ import {getLicense} from '@mm-redux/selectors/entities/general';
import {getCurrentChannelId, getDefaultChannel} from '@mm-redux/selectors/entities/channels';
import {getAssociatedGroupsForReference, searchAssociatedGroupsForReferenceLocal} from '@mm-redux/selectors/entities/groups';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {isLandscape} from 'app/selectors/device';
import {
filterMembersInChannel,
@ -76,7 +75,6 @@ function mapStateToProps(state, ownProps) {
outChannel,
requestStatus: state.requests.users.autocompleteUsers.status,
theme: getTheme(state),
isLandscape: isLandscape(state),
useChannelMentions,
groups,
};

View file

@ -1,56 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {
Text,
View,
} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import CompassIcon from '@components/compass_icon';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
export default class GroupMentionItem extends PureComponent {
static propTypes = {
completeHandle: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
};
completeMention = () => {
const {onPress, completeHandle} = this.props;
onPress(completeHandle);
};
render() {
const {
completeHandle,
theme,
} = this.props;
const style = getStyleFromTheme(theme);
return (
<TouchableWithFeedback
onPress={this.completeMention}
style={style.row}
type={'opacity'}
>
<View style={style.rowPicture}>
<CompassIcon
name='account-group-outline'
style={style.rowIcon}
/>
</View>
<Text style={style.rowUsername}>{`@${completeHandle}`}</Text>
<Text style={style.rowUsername}>{' - '}</Text>
<Text style={style.rowFullname}>{`${completeHandle}`}</Text>
</TouchableWithFeedback>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
@ -85,3 +47,40 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
},
};
});
const GroupMentionItem = (props) => {
const insets = useSafeAreaInsets();
const {onPress, completeHandle, theme} = props;
const completeMention = () => {
onPress(completeHandle);
};
const style = getStyleFromTheme(theme);
return (
<TouchableWithFeedback
onPress={completeMention}
style={[style.row, {marginLeft: insets.left, marginRight: insets.right}]}
type={'opacity'}
>
<View style={style.rowPicture}>
<CompassIcon
name='account-group-outline'
style={style.rowIcon}
/>
</View>
<Text style={style.rowUsername}>{`@${completeHandle}`}</Text>
<Text style={style.rowUsername}>{' - '}</Text>
<Text style={style.rowFullname}>{`${completeHandle}`}</Text>
</TouchableWithFeedback>
);
};
GroupMentionItem.propTypes = {
completeHandle: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
};
export default GroupMentionItem;

View file

@ -1,129 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {
Text,
View,
} from 'react-native';
import {Text, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import ProfilePicture from 'app/components/profile_picture';
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, changeOpacity} from 'app/utils/theme';
import FormattedText from 'app/components/formatted_text';
export default class AtMentionItem extends PureComponent {
static propTypes = {
firstName: PropTypes.string,
lastName: PropTypes.string,
nickname: PropTypes.string,
onPress: PropTypes.func.isRequired,
userId: PropTypes.string.isRequired,
username: PropTypes.string,
isGuest: PropTypes.bool,
isBot: PropTypes.bool,
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
isCurrentUser: PropTypes.bool.isRequired,
showFullName: PropTypes.string,
testID: PropTypes.string,
};
static defaultProps = {
firstName: '',
lastName: '',
};
completeMention = () => {
const {onPress, username} = this.props;
onPress(username);
};
renderNameBlock = () => {
let name = '';
const {showFullName, firstName, lastName, nickname} = this.props;
const hasNickname = nickname.length > 0;
if (showFullName === 'true') {
name += `${firstName} ${lastName} `;
}
if (hasNickname) {
name += `(${nickname})`;
}
return name.trim();
}
render() {
const {
userId,
username,
theme,
isBot,
isLandscape,
isGuest,
isCurrentUser,
testID,
} = this.props;
const style = getStyleFromTheme(theme);
const name = this.renderNameBlock();
return (
<TouchableWithFeedback
testID={testID}
key={userId}
onPress={this.completeMention}
style={padding(isLandscape)}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
type={'native'}
>
<View style={style.row}>
<View style={style.rowPicture}>
<ProfilePicture
userId={userId}
theme={theme}
size={24}
status={null}
showStatus={false}
/>
</View>
<BotTag
show={isBot}
theme={theme}
/>
<GuestTag
show={isGuest}
theme={theme}
/>
{Boolean(name.length) &&
<Text
style={style.rowFullname}
numberOfLines={1}
>
{name}
{isCurrentUser &&
<FormattedText
id='suggestion.mention.you'
defaultMessage='(you)'
/>}
</Text>
}
<Text
style={style.rowUsername}
numberOfLines={1}
>
{` @${username}`}
</Text>
</View>
</TouchableWithFeedback>
);
}
}
import FormattedText from '@components/formatted_text';
import ProfilePicture from '@components/profile_picture';
import {BotTag, GuestTag} from '@components/tag';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
@ -155,3 +42,115 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
},
};
});
const AtMentionItem = (props) => {
const insets = useSafeAreaInsets();
const {
firstName,
isBot,
isCurrentUser,
isGuest,
lastName,
nickname,
onPress,
showFullName,
testID,
theme,
userId,
username,
} = props;
const completeMention = () => {
onPress(username);
};
const renderNameBlock = () => {
let name = '';
const hasNickname = nickname.length > 0;
if (showFullName === 'true') {
name += `${firstName} ${lastName} `;
}
if (hasNickname) {
name += `(${nickname})`;
}
return name.trim();
};
const style = getStyleFromTheme(theme);
const name = renderNameBlock();
return (
<TouchableWithFeedback
testID={testID}
key={userId}
onPress={completeMention}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
style={{marginLeft: insets.left, marginRight: insets.right}}
type={'native'}
>
<View style={style.row}>
<View style={style.rowPicture}>
<ProfilePicture
userId={userId}
theme={theme}
size={24}
status={null}
showStatus={false}
/>
</View>
<BotTag
show={isBot}
theme={theme}
/>
<GuestTag
show={isGuest}
theme={theme}
/>
{Boolean(name.length) &&
<Text
style={style.rowFullname}
numberOfLines={1}
>
{name}
{isCurrentUser &&
<FormattedText
id='suggestion.mention.you'
defaultMessage='(you)'
/>}
</Text>
}
<Text
style={style.rowUsername}
numberOfLines={1}
>
{` @${username}`}
</Text>
</View>
</TouchableWithFeedback>
);
};
AtMentionItem.propTypes = {
firstName: PropTypes.string,
lastName: PropTypes.string,
nickname: PropTypes.string,
onPress: PropTypes.func.isRequired,
userId: PropTypes.string.isRequired,
username: PropTypes.string,
isGuest: PropTypes.bool,
isBot: PropTypes.bool,
theme: PropTypes.object.isRequired,
isCurrentUser: PropTypes.bool.isRequired,
showFullName: PropTypes.string,
testID: PropTypes.string,
};
AtMentionItem.defaultProps = {
firstName: '',
lastName: '',
};
export default AtMentionItem;

View file

@ -3,16 +3,13 @@
import {connect} from 'react-redux';
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
import {isGuest} from '@utils/users';
import AtMentionItem from './at_mention_item';
import {isLandscape} from 'app/selectors/device';
import {isGuest} from 'app/utils/users';
function mapStateToProps(state, ownProps) {
const user = getUser(state, ownProps.userId);
const config = getConfig(state);
@ -25,7 +22,6 @@ function mapStateToProps(state, ownProps) {
isBot: Boolean(user.is_bot),
isGuest: isGuest(user),
theme: getTheme(state),
isLandscape: isLandscape(state),
isCurrentUser: getCurrentUserId(state) === user.id,
};
}

View file

@ -10,12 +10,11 @@ import {
ViewPropTypes,
} from 'react-native';
import {DeviceTypes} from '@constants';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {emptyFunction} from '@utils/general';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {DeviceTypes} from 'app/constants';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {emptyFunction} from 'app/utils/general';
import AtMention from './at_mention';
import ChannelMention from './channel_mention';
import EmojiSuggestion from './emoji_suggestion';
@ -203,7 +202,10 @@ export default class Autocomplete extends PureComponent {
}
return (
<View style={wrapperStyles}>
<View
style={wrapperStyles}
edges={['left', 'right']}
>
<View
testID='autocomplete'
ref={this.containerRef}

View file

@ -1,56 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {ActivityIndicator, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import FormattedText from 'app/components/formatted_text';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
export default class AutocompleteSectionHeader extends PureComponent {
static propTypes = {
defaultMessage: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
loading: PropTypes.bool,
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
isFirstSection: PropTypes.bool,
};
static defaultProps = {
isLandscape: false,
};
render() {
const {defaultMessage, id, loading, theme, isLandscape, isFirstSection} = this.props;
const style = getStyleFromTheme(theme);
const sectionStyles = [style.section, padding(isLandscape)];
if (!isFirstSection) {
sectionStyles.push(style.borderTop);
}
return (
<View style={style.sectionWrapper}>
<View style={sectionStyles}>
<FormattedText
id={id}
defaultMessage={defaultMessage}
style={style.sectionText}
/>
{loading &&
<ActivityIndicator
color={theme.centerChannelColor}
size='small'
/>
}
</View>
</View>
);
}
}
import FormattedText from '@components/formatted_text';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
@ -79,3 +36,43 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
},
};
});
const AutocompleteSectionHeader = (props) => {
const insets = useSafeAreaInsets();
const {defaultMessage, id, loading, theme, isFirstSection} = props;
const style = getStyleFromTheme(theme);
const sectionStyles = [style.section, {marginLeft: insets.left, marginRight: insets.right}];
if (!isFirstSection) {
sectionStyles.push(style.borderTop);
}
return (
<View style={style.sectionWrapper}>
<View style={sectionStyles}>
<FormattedText
id={id}
defaultMessage={defaultMessage}
style={style.sectionText}
/>
{loading &&
<ActivityIndicator
color={theme.centerChannelColor}
size='small'
/>
}
</View>
</View>
);
};
AutocompleteSectionHeader.propTypes = {
defaultMessage: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
loading: PropTypes.bool,
theme: PropTypes.object.isRequired,
isFirstSection: PropTypes.bool,
};
export default AutocompleteSectionHeader;

View file

@ -36,7 +36,6 @@ export default class ChannelMention extends PureComponent {
requestStatus: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
value: PropTypes.string,
isLandscape: PropTypes.bool.isRequired,
nestedScrollEnabled: PropTypes.bool,
};
@ -191,7 +190,6 @@ export default class ChannelMention extends PureComponent {
defaultMessage={section.defaultMessage}
loading={!section.hideLoadingIndicator && this.props.requestStatus === RequestStatus.STARTED}
theme={this.props.theme}
isLandscape={this.props.isLandscape}
isFirstSection={isFirstSection}
/>
);

View file

@ -6,9 +6,8 @@ import {connect} from 'react-redux';
import {searchChannels, autocompleteChannelsForSearch} from '@mm-redux/actions/channels';
import {getMyChannelMemberships} from '@mm-redux/selectors/entities/channels';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {isLandscape} from 'app/selectors/device';
import {
filterMyChannels,
filterOtherChannels,
@ -16,8 +15,7 @@ import {
filterPrivateChannels,
filterDirectAndGroupMessages,
getMatchTermForChannelMention,
} from 'app/selectors/autocomplete';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
} from '@selectors/autocomplete';
import ChannelMention from './channel_mention';
@ -52,7 +50,6 @@ function mapStateToProps(state, ownProps) {
matchTerm,
requestStatus: state.requests.channels.getChannels.status,
theme: getTheme(state),
isLandscape: isLandscape(state),
};
}

View file

@ -1,113 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {
Text,
View,
} from 'react-native';
import {Text, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {General} from '@mm-redux/constants';
import CompassIcon from '@components/compass_icon';
import {BotTag, GuestTag} from '@components/tag';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
export default class ChannelMentionItem extends PureComponent {
static propTypes = {
channelId: PropTypes.string.isRequired,
displayName: PropTypes.string,
name: PropTypes.string,
type: PropTypes.string,
isBot: PropTypes.bool.isRequired,
isGuest: PropTypes.bool.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
};
completeMention = () => {
const {onPress, displayName, name, type} = this.props;
if (type === General.DM_CHANNEL || type === General.GM_CHANNEL) {
onPress('@' + displayName.replace(/ /g, ''));
} else {
onPress(name);
}
};
render() {
const {
channelId,
displayName,
name,
theme,
type,
isBot,
isLandscape,
isGuest,
} = this.props;
const style = getStyleFromTheme(theme);
let iconName = 'globe';
let component;
if (type === General.PRIVATE_CHANNEL) {
iconName = 'lock';
}
if (type === General.DM_CHANNEL || type === General.GM_CHANNEL) {
if (!displayName) {
return null;
}
component = (
<TouchableWithFeedback
key={channelId}
onPress={this.completeMention}
style={[style.row, padding(isLandscape)]}
type={'opacity'}
>
<Text style={style.rowDisplayName}>{'@' + displayName}</Text>
<BotTag
show={isBot}
theme={theme}
/>
<GuestTag
show={isGuest}
theme={theme}
/>
</TouchableWithFeedback>
);
} else {
component = (
<TouchableWithFeedback
key={channelId}
onPress={this.completeMention}
style={padding(isLandscape)}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
type={'native'}
>
<View style={style.row}>
<CompassIcon
name={iconName}
style={style.icon}
/>
<Text style={style.rowDisplayName}>{displayName}</Text>
<Text style={style.rowName}>{` ~${name}`}</Text>
</View>
</TouchableWithFeedback>
);
}
return (
<React.Fragment>
{component}
</React.Fragment>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
icon: {
@ -133,3 +37,92 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
},
};
});
const ChannelMentionItem = (props) => {
const insets = useSafeAreaInsets();
const {
channelId,
displayName,
isBot,
isGuest,
name,
onPress,
theme,
type,
} = props;
const completeMention = () => {
if (type === General.DM_CHANNEL || type === General.GM_CHANNEL) {
onPress('@' + displayName.replace(/ /g, ''));
} else {
onPress(name);
}
};
const style = getStyleFromTheme(theme);
const margins = {marginLeft: insets.left, marginRight: insets.right};
let iconName = 'globe';
let component;
if (type === General.PRIVATE_CHANNEL) {
iconName = 'lock';
}
if (type === General.DM_CHANNEL || type === General.GM_CHANNEL) {
if (!displayName) {
return null;
}
component = (
<TouchableWithFeedback
key={channelId}
onPress={completeMention}
style={[style.row, margins]}
type={'opacity'}
>
<Text style={style.rowDisplayName}>{'@' + displayName}</Text>
<BotTag
show={isBot}
theme={theme}
/>
<GuestTag
show={isGuest}
theme={theme}
/>
</TouchableWithFeedback>
);
} else {
component = (
<TouchableWithFeedback
key={channelId}
onPress={this.completeMention}
style={margins}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
type={'native'}
>
<View style={style.row}>
<CompassIcon
name={iconName}
style={style.icon}
/>
<Text style={style.rowDisplayName}>{displayName}</Text>
<Text style={style.rowName}>{` ~${name}`}</Text>
</View>
</TouchableWithFeedback>
);
}
return component;
};
ChannelMentionItem.propTypes = {
channelId: PropTypes.string.isRequired,
displayName: PropTypes.string,
name: PropTypes.string,
type: PropTypes.string,
isBot: PropTypes.bool.isRequired,
isGuest: PropTypes.bool.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
};
export default ChannelMentionItem;

View file

@ -7,10 +7,8 @@ import {General} from '@mm-redux/constants';
import {getChannel} from '@mm-redux/selectors/entities/channels';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getUser} from '@mm-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 {getChannelNameForSearchAutocomplete} from '@selectors/channel';
import {isGuest as isGuestUser} from '@utils/users';
import ChannelMentionItem from './channel_mention_item';
@ -36,7 +34,6 @@ function mapStateToProps(state, ownProps) {
isBot,
isGuest,
theme: getTheme(state),
isLandscape: isLandscape(state),
};
}

View file

@ -9,7 +9,6 @@ import {getAutocompleteCommands, getCommandAutocompleteSuggestions} from '@mm-re
import {getAutocompleteCommandsList, getCommandAutocompleteSuggestionsList} from '@mm-redux/selectors/entities/integrations';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {isLandscape} from 'app/selectors/device';
import SlashSuggestion from './slash_suggestion';
@ -31,7 +30,6 @@ function mapStateToProps(state) {
commands: mobileCommandsSelector(state),
currentTeamId: getCurrentTeamId(state),
theme: getTheme(state),
isLandscape: isLandscape(state),
suggestions: getCommandAutocompleteSuggestionsList(state),
};
}

View file

@ -8,12 +8,12 @@ import {
Platform,
} from 'react-native';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import SlashSuggestionItem from './slash_suggestion_item';
import {analytics} from '@init/analytics.ts';
import {Client4} from '@mm-redux/client';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {analytics} from '@init/analytics.ts';
import {makeStyleSheetFromTheme} from '@utils/theme';
import SlashSuggestionItem from './slash_suggestion_item';
const TIME_BEFORE_NEXT_COMMAND_REQUEST = 1000 * 60 * 5;
@ -31,7 +31,6 @@ export default class SlashSuggestion extends PureComponent {
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
value: PropTypes.string,
isLandscape: PropTypes.bool.isRequired,
nestedScrollEnabled: PropTypes.bool,
suggestions: PropTypes.array,
rootId: PropTypes.string,
@ -187,7 +186,6 @@ export default class SlashSuggestion extends PureComponent {
theme={this.props.theme}
suggestion={item.Suggestion}
complete={item.Complete}
isLandscape={this.props.isLandscape}
/>
)

View file

@ -1,79 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {Image, Text, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
import slashIcon from '@assets/images/autocomplete/slash_command.png';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import slashIcon from '@assets/images/autocomplete/slash_command.png';
export default class SlashSuggestionItem extends PureComponent {
static propTypes = {
description: PropTypes.string,
hint: PropTypes.string,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
suggestion: PropTypes.string,
complete: PropTypes.string,
isLandscape: PropTypes.bool.isRequired,
};
completeSuggestion = () => {
const {onPress, complete} = this.props;
onPress(complete);
};
render() {
const {
description,
hint,
theme,
suggestion,
complete,
isLandscape,
} = this.props;
const style = getStyleFromTheme(theme);
let suggestionText = suggestion;
if (suggestionText[0] === '/' && complete.split(' ').length === 1) {
suggestionText = suggestionText.substring(1);
}
return (
<TouchableWithFeedback
onPress={this.completeSuggestion}
style={padding(isLandscape)}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
type={'native'}
>
<View style={style.container}>
<View style={style.icon}>
<Image
style={style.iconColor}
width={10}
height={16}
source={slashIcon}
/>
</View>
<View style={style.suggestionContainer}>
<Text style={style.suggestionName}>{`${suggestionText} ${hint}`}</Text>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={style.suggestionDescription}
>
{description}
</Text>
</View>
</View>
</TouchableWithFeedback>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
@ -112,3 +47,67 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
},
};
});
const SlashSuggestionItem = (props) => {
const insets = useSafeAreaInsets();
const {
complete,
description,
hint,
onPress,
suggestion,
theme,
} = props;
const completeSuggestion = () => {
onPress(complete);
};
const style = getStyleFromTheme(theme);
let suggestionText = suggestion;
if (suggestionText[0] === '/' && complete.split(' ').length === 1) {
suggestionText = suggestionText.substring(1);
}
return (
<TouchableWithFeedback
onPress={completeSuggestion}
style={{marginLeft: insets.left, marginRight: insets.right}}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
type={'native'}
>
<View style={style.container}>
<View style={style.icon}>
<Image
style={style.iconColor}
width={10}
height={16}
source={slashIcon}
/>
</View>
<View style={style.suggestionContainer}>
<Text style={style.suggestionName}>{`${suggestionText} ${hint}`}</Text>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={style.suggestionDescription}
>
{description}
</Text>
</View>
</View>
</TouchableWithFeedback>
);
};
SlashSuggestionItem.propTypes = {
description: PropTypes.string,
hint: PropTypes.string,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
suggestion: PropTypes.string,
complete: PropTypes.string,
};
export default SlashSuggestionItem;

View file

@ -10,7 +10,6 @@ import {displayUsername} from '@mm-redux/utils/user_utils';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {preventDoubleTap} from '@utils/tap';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
@ -35,7 +34,6 @@ export default class AutocompleteSelector extends PureComponent {
helpText: PropTypes.node,
errorText: PropTypes.node,
roundedBorders: PropTypes.bool,
isLandscape: PropTypes.bool.isRequired,
disabled: PropTypes.bool,
};
@ -119,7 +117,6 @@ export default class AutocompleteSelector extends PureComponent {
optional,
showRequiredAsterisk,
roundedBorders,
isLandscape,
disabled,
} = this.props;
const {selectedText} = this.state;
@ -186,9 +183,7 @@ export default class AutocompleteSelector extends PureComponent {
return (
<View style={style.container}>
<View style={padding(isLandscape)}>
{labelContent}
</View>
{labelContent}
<TouchableWithFeedback
style={disabled ? style.disabled : null}
onPress={this.goToSelectorScreen}
@ -197,7 +192,7 @@ export default class AutocompleteSelector extends PureComponent {
>
<View style={inputStyle}>
<Text
style={[selectedStyle, padding(isLandscape)]}
style={selectedStyle}
numberOfLines={1}
>
{text}
@ -205,14 +200,12 @@ export default class AutocompleteSelector extends PureComponent {
<CompassIcon
name='chevron-down'
color={changeOpacity(theme.centerChannelColor, 0.5)}
style={[style.icon, padding(isLandscape)]}
style={style.icon}
/>
</View>
</TouchableWithFeedback>
<View style={padding(isLandscape)}>
{helpTextContent}
{errorTextContent}
</View>
{helpTextContent}
{errorTextContent}
</View>
);
}

View file

@ -9,13 +9,11 @@ import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entit
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),
};
}

View file

@ -15,7 +15,6 @@ import {General} from '@mm-redux/constants';
import {showModal} from '@actions/navigation';
import CompassIcon from '@components/compass_icon';
import ProfilePicture from '@components/profile_picture';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
import {BotTag, GuestTag} from '@components/tag';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {preventDoubleTap} from '@utils/tap';
@ -30,7 +29,6 @@ class ChannelIntro extends PureComponent {
currentChannelMembers: PropTypes.array.isRequired,
intl: intlShape.isRequired,
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
teammateNameDisplay: PropTypes.string.isRequired,
};
@ -329,7 +327,7 @@ class ChannelIntro extends PureComponent {
};
render() {
const {currentChannel, theme, isLandscape} = this.props;
const {currentChannel, theme} = this.props;
const style = getStyleSheet(theme);
const channelType = currentChannel.type;
@ -337,10 +335,10 @@ class ChannelIntro extends PureComponent {
if (channelType === General.DM_CHANNEL || channelType === General.GM_CHANNEL) {
profiles = (
<View>
<View style={[style.profilesContainer, padding(isLandscape)]}>
<View style={style.profilesContainer}>
{this.buildProfiles()}
</View>
<View style={[style.namesContainer, padding(isLandscape)]}>
<View style={style.namesContainer}>
{this.buildNames()}
</View>
</View>
@ -350,7 +348,7 @@ class ChannelIntro extends PureComponent {
return (
<View style={style.container}>
{profiles}
<View style={[style.contentContainer, padding(isLandscape)]}>
<View style={style.contentContainer}>
{this.buildContent()}
</View>
</View>

View file

@ -9,7 +9,6 @@ import {makeGetChannel} from '@mm-redux/selectors/entities/channels';
import {getCurrentUserId, getUser, makeGetProfilesInChannel} from '@mm-redux/selectors/entities/users';
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
import {isLandscape} from 'app/selectors/device';
import {getChannelMembersForDm} from 'app/selectors/channel';
import ChannelIntro from './channel_intro';
@ -48,7 +47,6 @@ function makeMapStateToProps() {
currentChannel,
currentChannelMembers,
theme: getTheme(state),
isLandscape: isLandscape(state),
teammateNameDisplay: getTeammateNameDisplaySetting(state),
};
};

View file

@ -10,7 +10,6 @@ exports[`ChannelLoader should match snapshot 1`] = `
"overflow": "hidden",
},
undefined,
null,
Object {
"backgroundColor": "#ffffff",
},

View file

@ -10,9 +10,8 @@ import {
} from 'react-native';
import * as RNPlaceholder from 'rn-placeholder';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import CustomPropTypes from '@constants/custom_prop_types';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
function calculateMaxRows(height) {
return Math.round(height / 100);
@ -36,7 +35,6 @@ export default class ChannelLoader extends PureComponent {
style: CustomPropTypes.Style,
theme: PropTypes.object.isRequired,
height: PropTypes.number,
isLandscape: PropTypes.bool.isRequired,
};
constructor(props) {
@ -107,7 +105,6 @@ export default class ChannelLoader extends PureComponent {
channelIsLoading,
style: styleProp,
theme,
isLandscape,
} = this.props;
if (!channelIsLoading) {
@ -119,7 +116,7 @@ export default class ChannelLoader extends PureComponent {
return (
<View
style={[style.container, styleProp, padding(isLandscape), {backgroundColor: bg}]}
style={[style.container, styleProp, {backgroundColor: bg}]}
onLayout={this.handleLayout}
>
{Array(this.state.maxRows).fill().map((item, index) => this.buildSections({

View file

@ -12,7 +12,6 @@ describe('ChannelLoader', () => {
const baseProps = {
channelIsLoading: true,
theme: Preferences.THEMES.default,
isLandscape: false,
};
test('should match snapshot', () => {

View file

@ -5,8 +5,6 @@ import {connect} from 'react-redux';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {isLandscape} from 'app/selectors/device';
import ChannelLoader from './channel_loader';
function mapStateToProps(state, ownProps) {
@ -15,7 +13,6 @@ function mapStateToProps(state, ownProps) {
return {
channelIsLoading,
theme: getTheme(state),
isLandscape: isLandscape(state),
};
}

View file

@ -117,13 +117,10 @@ exports[`CustomList should match snapshot, renderSectionHeader 1`] = `
>
<Text
style={
Array [
Object {
"color": "#3d3c40",
"fontWeight": "600",
},
null,
]
Object {
"color": "#3d3c40",
"fontWeight": "600",
}
}
>
section_id

View file

@ -19,7 +19,6 @@ export default class ChannelListRow extends React.PureComponent {
theme: PropTypes.object.isRequired,
channel: PropTypes.object.isRequired,
...CustomListRow.propTypes,
isLandscape: PropTypes.bool.isRequired,
};
onPress = () => {
@ -49,7 +48,6 @@ export default class ChannelListRow extends React.PureComponent {
enabled={this.props.enabled}
selectable={this.props.selectable}
selected={this.props.selected}
isLandscape={this.props.isLandscape}
>
<View style={style.container}>
<View style={style.titleContainer}>

View file

@ -6,7 +6,6 @@ import {connect} from 'react-redux';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {makeGetChannel} from '@mm-redux/selectors/entities/channels';
import {isLandscape} from 'app/selectors/device';
import ChannelListRow from './channel_list_row';
function makeMapStateToProps() {
@ -16,7 +15,6 @@ function makeMapStateToProps() {
return {
theme: getTheme(state),
channel: getChannel(state, ownProps),
isLandscape: isLandscape(state),
};
};
}

View file

@ -9,7 +9,6 @@ import {
} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {paddingLeft as padding} from '@components/safe_area_view/iphone_x_spacing';
import ConditionalTouchable from '@components/conditional_touchable';
import CustomPropTypes from '@constants/custom_prop_types';
@ -20,14 +19,11 @@ export default class CustomListRow extends React.PureComponent {
selectable: PropTypes.bool,
selected: PropTypes.bool,
children: CustomPropTypes.Children,
item: PropTypes.object,
isLandscape: PropTypes.bool.isRequired,
testID: PropTypes.string,
};
static defaultProps = {
enabled: true,
isLandscape: false,
};
render() {
@ -38,7 +34,7 @@ export default class CustomListRow extends React.PureComponent {
style={style.touchable}
testID={this.props.testID}
>
<View style={[style.container, padding(this.props.isLandscape)]}>
<View style={style.container}>
{this.props.selectable &&
<View style={style.selectorContainer}>
<View style={[style.selector, (this.props.selected && style.selectorFilled), (!this.props.enabled && style.selectorDisabled)]}>

View file

@ -7,7 +7,6 @@ import {FlatList, Keyboard, Platform, RefreshControl, SectionList, Text, View} f
import {ListTypes} from 'app/constants';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {paddingLeft as padding} from 'app/components/safe_area_view/iphone_x_spacing';
export const FLATLIST = 'flat';
export const SECTIONLIST = 'section';
@ -32,13 +31,11 @@ export default class CustomList extends PureComponent {
selectable: PropTypes.bool,
theme: PropTypes.object.isRequired,
shouldRenderSeparator: PropTypes.bool,
isLandscape: PropTypes.bool.isRequired,
testID: PropTypes.string,
};
static defaultProps = {
canRefresh: true,
isLandscape: false,
listType: FLATLIST,
showNoResults: true,
shouldRenderSeparator: true,
@ -165,13 +162,13 @@ export default class CustomList extends PureComponent {
};
renderSectionHeader = ({section}) => {
const {theme, isLandscape} = this.props;
const {theme} = this.props;
const style = getStyleFromTheme(theme);
return (
<View style={style.sectionWrapper}>
<View style={style.sectionContainer}>
<Text style={[style.sectionText, padding(isLandscape)]}>{section.id}</Text>
<Text style={style.sectionText}>{section.id}</Text>
</View>
</View>
);

View file

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

View file

@ -17,17 +17,12 @@ 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);
@ -41,7 +36,6 @@ export default class OptionListRow extends React.PureComponent {
selected,
theme,
item,
isLandscape,
} = this.props;
const {text, value} = item;
@ -54,7 +48,6 @@ export default class OptionListRow extends React.PureComponent {
enabled={enabled}
selectable={selectable}
selected={selected}
isLandscape={isLandscape}
>
<View style={style.textContainer}>
<View>

View file

@ -14,7 +14,6 @@ exports[`UserListRow should match snapshot 1`] = `
<CustomListRow
enabled={true}
id="21345"
isLandscape={false}
onPress={[Function]}
>
<View
@ -147,7 +146,6 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
<CustomListRow
enabled={true}
id="21345"
isLandscape={false}
onPress={[Function]}
>
<View
@ -278,7 +276,6 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
<CustomListRow
enabled={true}
id="21345"
isLandscape={false}
onPress={[Function]}
>
<View
@ -422,7 +419,6 @@ exports[`UserListRow should match snapshot for guest user 1`] = `
<CustomListRow
enabled={true}
id="21345"
isLandscape={false}
onPress={[Function]}
>
<View

View file

@ -5,7 +5,6 @@ import {connect} from 'react-redux';
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
import {isLandscape} from 'app/selectors/device';
import UserListRow from './user_list_row';
function mapStateToProps(state, ownProps) {
@ -14,7 +13,6 @@ function mapStateToProps(state, ownProps) {
theme: getTheme(state),
user: getUser(state, ownProps.id),
teammateNameDisplay: getTeammateNameDisplaySetting(state),
isLandscape: isLandscape(state),
};
}

View file

@ -48,7 +48,6 @@ export default class UserListRow extends React.PureComponent {
teammateNameDisplay,
theme,
user,
isLandscape,
} = this.props;
const {id, username} = user;
@ -73,7 +72,6 @@ export default class UserListRow extends React.PureComponent {
enabled={enabled}
selectable={selectable}
selected={selected}
isLandscape={isLandscape}
testID={this.props.testID}
>
<View style={style.profileContainer}>

View file

@ -29,7 +29,6 @@ describe('UserListRow', () => {
},
theme: Preferences.THEMES.default,
teammateNameDisplay: 'test',
isLandscape: false,
};
test('should match snapshot', () => {

View file

@ -1,7 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EditChannelInfo should match snapshot 1`] = `
<React.Fragment>
<RNCSafeAreaView
edges={
Array [
"bottom",
"left",
"right",
]
}
style={
Object {
"flex": 1,
}
}
>
<Connect(StatusBar) />
<KeyboardAwareScrollView
enableAutomaticScroll={true}
@ -23,16 +36,11 @@ exports[`EditChannelInfo should match snapshot 1`] = `
>
<View
style={
Array [
Object {
"backgroundColor": "rgba(61,60,64,0.06)",
"flex": 1,
"paddingTop": 30,
},
Object {
"height": 600,
},
]
Object {
"backgroundColor": "rgba(61,60,64,0.06)",
"flex": 1,
"paddingTop": 30,
}
}
>
<View>
@ -41,30 +49,20 @@ exports[`EditChannelInfo should match snapshot 1`] = `
defaultMessage="Name"
id="channel_modal.name"
style={
Array [
Object {
"color": "#3d3c40",
"fontSize": 14,
"marginLeft": 15,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"color": "#3d3c40",
"fontSize": 14,
"marginLeft": 15,
}
}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"marginTop": 10,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"backgroundColor": "#ffffff",
"marginTop": 10,
}
}
>
<TextInputWithLocalizedPlaceholder
@ -98,15 +96,10 @@ exports[`EditChannelInfo should match snapshot 1`] = `
<View>
<View
style={
Array [
Object {
"flexDirection": "row",
"marginTop": 30,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"flexDirection": "row",
"marginTop": 30,
}
}
>
<FormattedText
@ -134,15 +127,10 @@ exports[`EditChannelInfo should match snapshot 1`] = `
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"marginTop": 10,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"backgroundColor": "#ffffff",
"marginTop": 10,
}
}
>
<TextInputWithLocalizedPlaceholder
@ -184,17 +172,12 @@ exports[`EditChannelInfo should match snapshot 1`] = `
defaultMessage="Describe how this channel should be used."
id="channel_modal.descriptionHelp"
style={
Array [
Object {
"color": "rgba(61,60,64,0.5)",
"fontSize": 14,
"marginHorizontal": 15,
"marginTop": 10,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"color": "rgba(61,60,64,0.5)",
"fontSize": 14,
"marginHorizontal": 15,
"marginTop": 10,
}
}
/>
</View>
@ -202,15 +185,10 @@ exports[`EditChannelInfo should match snapshot 1`] = `
<View
onLayout={[Function]}
style={
Array [
Object {
"flexDirection": "row",
"marginTop": 15,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"flexDirection": "row",
"marginTop": 15,
}
}
>
<FormattedText
@ -238,15 +216,10 @@ exports[`EditChannelInfo should match snapshot 1`] = `
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"marginTop": 10,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"backgroundColor": "#ffffff",
"marginTop": 10,
}
}
>
<TextInputWithLocalizedPlaceholder
@ -295,17 +268,12 @@ exports[`EditChannelInfo should match snapshot 1`] = `
defaultMessage="Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com)."
id="channel_modal.headerHelp"
style={
Array [
Object {
"color": "rgba(61,60,64,0.5)",
"fontSize": 14,
"marginHorizontal": 15,
"marginTop": 10,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"color": "rgba(61,60,64,0.5)",
"fontSize": 14,
"marginHorizontal": 15,
"marginTop": 10,
}
}
/>
</View>
@ -342,5 +310,5 @@ exports[`EditChannelInfo should match snapshot 1`] = `
value="header"
/>
</View>
</React.Fragment>
</RNCSafeAreaView>
`;

View file

@ -1,453 +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,
TouchableWithoutFeedback,
View,
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scrollview';
import {General} from '@mm-redux/constants';
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
import ErrorText from 'app/components/error_text';
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,
makeStyleSheetFromTheme,
getKeyboardAppearanceFromTheme,
} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {popTopScreen, dismissModal} from 'app/actions/navigation';
export default class EditChannelInfo extends PureComponent {
static propTypes = {
theme: PropTypes.object.isRequired,
deviceWidth: PropTypes.number.isRequired,
deviceHeight: PropTypes.number.isRequired,
channelType: PropTypes.string,
enableRightButton: PropTypes.func,
saving: PropTypes.bool.isRequired,
editing: PropTypes.bool,
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
displayName: PropTypes.string,
currentTeamUrl: PropTypes.string,
channelURL: PropTypes.string,
purpose: PropTypes.string,
header: PropTypes.string,
onDisplayNameChange: PropTypes.func,
onChannelURLChange: PropTypes.func,
onPurposeChange: PropTypes.func,
onHeaderChange: PropTypes.func,
oldDisplayName: PropTypes.string,
oldChannelURL: PropTypes.string,
oldHeader: PropTypes.string,
oldPurpose: PropTypes.string,
isLandscape: PropTypes.bool.isRequired,
testID: PropTypes.string,
};
static defaultProps = {
editing: false,
};
constructor(props) {
super(props);
this.nameInput = React.createRef();
this.urlInput = React.createRef();
this.purposeInput = React.createRef();
this.headerInput = React.createRef();
this.scroll = React.createRef();
this.state = {
keyboardVisible: false,
keyboardPosition: 0,
};
}
blur = () => {
if (this.nameInput?.current) {
this.nameInput.current.blur();
}
// TODO: uncomment below once the channel URL field is added
// if (this.urlInput?.current) {
// this.urlInput.current.blur();
// }
if (this.purposeInput?.current) {
this.purposeInput.current.blur();
}
if (this.headerInput?.current) {
this.headerInput.current.blur();
}
if (this.scroll?.current) {
this.scroll.current.scrollTo({x: 0, y: 0, animated: true});
}
};
close = (goBack = false) => {
if (goBack) {
popTopScreen();
} else {
dismissModal();
}
};
canUpdate = (displayName, channelURL, purpose, header) => {
const {
oldDisplayName,
oldChannelURL,
oldPurpose,
oldHeader,
} = this.props;
return displayName !== oldDisplayName || channelURL !== oldChannelURL ||
purpose !== oldPurpose || header !== oldHeader;
};
enableRightButton = (enable = false) => {
this.props.enableRightButton(enable);
};
onDisplayNameChangeText = (displayName) => {
const {editing, onDisplayNameChange} = this.props;
onDisplayNameChange(displayName);
if (editing) {
const {channelURL, purpose, header} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
return;
}
const displayNameExists = displayName && displayName.length >= 2;
this.props.enableRightButton(displayNameExists);
};
onPurposeChangeText = (purpose) => {
const {editing, onPurposeChange} = this.props;
onPurposeChange(purpose);
if (editing) {
const {displayName, channelURL, header} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
}
};
onHeaderChangeText = (header) => {
const {editing, onHeaderChange} = this.props;
onHeaderChange(header);
if (editing) {
const {displayName, channelURL, purpose} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
}
};
onHeaderLayout = ({nativeEvent}) => {
this.setState({headerPosition: nativeEvent.layout.y});
}
onKeyboardDidShow = () => {
this.setState({keyboardVisible: true});
if (this.state.headerHasFocus) {
this.setState({headerHasFocus: false});
this.scrollHeaderToTop();
}
}
onKeyboardDidHide = () => {
this.setState({keyboardVisible: false});
}
onKeyboardOffsetChanged = (keyboardPosition) => {
this.setState({keyboardPosition});
}
onHeaderFocus = () => {
if (this.state.keyboardVisible) {
this.scrollHeaderToTop();
} else {
this.setState({headerHasFocus: true});
}
};
scrollHeaderToTop = () => {
if (this.scroll.current) {
this.scroll.current.scrollTo({x: 0, y: this.state.headerPosition});
}
}
render() {
const {
theme,
channelType,
deviceWidth,
deviceHeight,
displayName,
header,
purpose,
isLandscape,
error,
saving,
testID,
} = this.props;
const {keyboardVisible, keyboardPosition} = this.state;
const bottomStyle = {
bottom: Platform.select({
ios: keyboardPosition,
android: 0,
}),
};
const style = getStyleSheet(theme);
const displayHeaderOnly = channelType === General.DM_CHANNEL ||
channelType === General.GM_CHANNEL;
if (saving) {
return (
<View style={style.container}>
<StatusBar/>
<Loading color={theme.centerChannelColor}/>
</View>
);
}
let displayError;
if (error) {
displayError = (
<View style={[style.errorContainer, {width: deviceWidth}]}>
<View style={[style.errorWrapper, padding(isLandscape)]}>
<ErrorText error={error}/>
</View>
</View>
);
}
return (
<React.Fragment>
<StatusBar/>
<KeyboardAwareScrollView
testID={testID}
ref={this.scroll}
style={style.container}
keyboardShouldPersistTaps={'always'}
onKeyboardDidShow={this.onKeyboardDidShow}
onKeyboardDidHide={this.onKeyboardDidHide}
enableAutomaticScroll={!keyboardVisible}
>
{displayError}
<TouchableWithoutFeedback onPress={this.blur}>
<View style={[style.scrollView, {height: deviceHeight + (Platform.OS === 'android' ? 200 : 0)}]}>
{!displayHeaderOnly && (
<View>
<View>
<FormattedText
style={[style.title, padding(isLandscape)]}
id='channel_modal.name'
defaultMessage='Name'
/>
</View>
<View style={[style.inputContainer, padding(isLandscape)]}>
<TextInputWithLocalizedPlaceholder
testID='edit_channel.name.input'
ref={this.nameInput}
value={displayName}
onChangeText={this.onDisplayNameChangeText}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: t('channel_modal.nameEx'), defaultMessage: 'E.g.: "Bugs", "Marketing", "客户支持"'}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
underlineColorAndroid='transparent'
disableFullscreenUI={true}
maxLength={64}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
</View>
</View>
)}
{!displayHeaderOnly && (
<View>
<View style={[style.titleContainer30, padding(isLandscape)]}>
<FormattedText
style={style.title}
id='channel_modal.purpose'
defaultMessage='Purpose'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={[style.inputContainer, padding(isLandscape)]}>
<TextInputWithLocalizedPlaceholder
testID='edit_channel.purpose.input'
ref={this.purposeInput}
value={purpose}
onChangeText={this.onPurposeChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: t('channel_modal.purposeEx'), defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
multiline={true}
blurOnSubmit={false}
textAlignVertical='top'
underlineColorAndroid='transparent'
disableFullscreenUI={true}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
</View>
<View>
<FormattedText
style={[style.helpText, padding(isLandscape)]}
id='channel_modal.descriptionHelp'
defaultMessage='Describe how this channel should be used.'
/>
</View>
</View>
)}
<View
onLayout={this.onHeaderLayout}
style={[style.titleContainer15, padding(isLandscape)]}
>
<FormattedText
style={style.title}
id='channel_modal.header'
defaultMessage='Header'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={[style.inputContainer, padding(isLandscape)]}>
<TextInputWithLocalizedPlaceholder
testID={'edit_channel.header.input'}
ref={this.headerInput}
value={header}
onChangeText={this.onHeaderChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: t('channel_modal.headerEx'), defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
multiline={true}
blurOnSubmit={false}
onFocus={this.onHeaderFocus}
textAlignVertical='top'
underlineColorAndroid='transparent'
disableFullscreenUI={true}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
</View>
<View style={style.headerHelpText}>
<FormattedText
style={[style.helpText, padding(isLandscape)]}
id='channel_modal.headerHelp'
defaultMessage={'Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com).'}
/>
</View>
</View>
</TouchableWithoutFeedback>
</KeyboardAwareScrollView>
<View style={[style.autocompleteContainer, bottomStyle]}>
<Autocomplete
cursorPosition={header.length}
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.onHeaderChangeText}
value={header}
nestedScrollEnabled={true}
onKeyboardOffsetChanged={this.onKeyboardOffsetChanged}
offsetY={8}
style={style.autocomplete}
/>
</View>
</React.Fragment>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
autocomplete: {
position: undefined,
},
autocompleteContainer: {
position: 'absolute',
width: '100%',
flex: 1,
justifyContent: 'flex-end',
},
container: {
flex: 1,
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
paddingTop: 30,
},
errorContainer: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
},
errorWrapper: {
justifyContent: 'center',
alignItems: 'center',
},
inputContainer: {
marginTop: 10,
backgroundColor: theme.centerChannelBg,
},
input: {
color: theme.centerChannelColor,
fontSize: 14,
height: 40,
paddingHorizontal: 15,
},
titleContainer30: {
flexDirection: 'row',
marginTop: 30,
},
titleContainer15: {
flexDirection: 'row',
marginTop: 15,
},
title: {
fontSize: 14,
color: theme.centerChannelColor,
marginLeft: 15,
},
optional: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 14,
marginLeft: 5,
},
helpText: {
fontSize: 14,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginTop: 10,
marginHorizontal: 15,
},
headerHelpText: {
zIndex: -1,
},
};
});

View file

@ -7,7 +7,7 @@ import {shallow} from 'enzyme';
import Preferences from '@mm-redux/constants/preferences';
import Autocomplete from 'app/components/autocomplete';
import EditChannelInfo from './edit_channel_info';
import EditChannelInfo from './index';
describe('EditChannelInfo', () => {
const baseProps = {
@ -32,7 +32,6 @@ describe('EditChannelInfo', () => {
oldChannelURL: '/team_a/channels/channel_old',
oldHeader: 'old_header',
oldPurpose: 'old_purpose',
isLandscape: true,
};
test('should match snapshot', () => {

View file

@ -1,15 +1,452 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
TouchableWithoutFeedback,
View,
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scrollview';
import {SafeAreaView} from 'react-native-safe-area-context';
import {isLandscape} from 'app/selectors/device';
import EditChannelInfo from './edit_channel_info';
import {General} from '@mm-redux/constants';
function mapStateToProps(state) {
return {
isLandscape: isLandscape(state),
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
import ErrorText from 'app/components/error_text';
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 {
changeOpacity,
makeStyleSheetFromTheme,
getKeyboardAppearanceFromTheme,
} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {popTopScreen, dismissModal} from 'app/actions/navigation';
export default class EditChannelInfo extends PureComponent {
static propTypes = {
theme: PropTypes.object.isRequired,
channelType: PropTypes.string,
enableRightButton: PropTypes.func,
saving: PropTypes.bool.isRequired,
editing: PropTypes.bool,
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
displayName: PropTypes.string,
channelURL: PropTypes.string,
purpose: PropTypes.string,
header: PropTypes.string,
onDisplayNameChange: PropTypes.func,
onPurposeChange: PropTypes.func,
onHeaderChange: PropTypes.func,
oldDisplayName: PropTypes.string,
oldChannelURL: PropTypes.string,
oldHeader: PropTypes.string,
oldPurpose: PropTypes.string,
testID: PropTypes.string,
};
static defaultProps = {
editing: false,
};
constructor(props) {
super(props);
this.nameInput = React.createRef();
this.urlInput = React.createRef();
this.purposeInput = React.createRef();
this.headerInput = React.createRef();
this.scroll = React.createRef();
this.state = {
keyboardVisible: false,
keyboardPosition: 0,
};
}
blur = () => {
if (this.nameInput?.current) {
this.nameInput.current.blur();
}
// TODO: uncomment below once the channel URL field is added
// if (this.urlInput?.current) {
// this.urlInput.current.blur();
// }
if (this.purposeInput?.current) {
this.purposeInput.current.blur();
}
if (this.headerInput?.current) {
this.headerInput.current.blur();
}
if (this.scroll?.current) {
this.scroll.current.scrollTo({x: 0, y: 0, animated: true});
}
};
close = (goBack = false) => {
if (goBack) {
popTopScreen();
} else {
dismissModal();
}
};
canUpdate = (displayName, channelURL, purpose, header) => {
const {
oldDisplayName,
oldChannelURL,
oldPurpose,
oldHeader,
} = this.props;
return displayName !== oldDisplayName || channelURL !== oldChannelURL ||
purpose !== oldPurpose || header !== oldHeader;
};
enableRightButton = (enable = false) => {
this.props.enableRightButton(enable);
};
onDisplayNameChangeText = (displayName) => {
const {editing, onDisplayNameChange} = this.props;
onDisplayNameChange(displayName);
if (editing) {
const {channelURL, purpose, header} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
return;
}
const displayNameExists = displayName && displayName.length >= 2;
this.props.enableRightButton(displayNameExists);
};
onPurposeChangeText = (purpose) => {
const {editing, onPurposeChange} = this.props;
onPurposeChange(purpose);
if (editing) {
const {displayName, channelURL, header} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
}
};
onHeaderChangeText = (header) => {
const {editing, onHeaderChange} = this.props;
onHeaderChange(header);
if (editing) {
const {displayName, channelURL, purpose} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
}
};
onHeaderLayout = ({nativeEvent}) => {
this.setState({headerPosition: nativeEvent.layout.y});
}
onKeyboardDidShow = () => {
this.setState({keyboardVisible: true});
if (this.state.headerHasFocus) {
this.setState({headerHasFocus: false});
this.scrollHeaderToTop();
}
}
onKeyboardDidHide = () => {
this.setState({keyboardVisible: false});
}
onKeyboardOffsetChanged = (keyboardPosition) => {
this.setState({keyboardPosition});
}
onHeaderFocus = () => {
if (this.state.keyboardVisible) {
this.scrollHeaderToTop();
} else {
this.setState({headerHasFocus: true});
}
};
scrollHeaderToTop = () => {
if (this.scroll.current) {
this.scroll.current.scrollTo({x: 0, y: this.state.headerPosition});
}
}
render() {
const {
theme,
channelType,
displayName,
header,
purpose,
error,
saving,
testID,
} = this.props;
const {keyboardVisible, keyboardPosition} = this.state;
const bottomStyle = {
bottom: Platform.select({
ios: keyboardPosition,
android: 0,
}),
};
const style = getStyleSheet(theme);
const displayHeaderOnly = channelType === General.DM_CHANNEL ||
channelType === General.GM_CHANNEL;
if (saving) {
return (
<View style={style.container}>
<StatusBar/>
<Loading color={theme.centerChannelColor}/>
</View>
);
}
let displayError;
if (error) {
displayError = (
<SafeAreaView
edges={['bottom', 'left', 'right']}
style={style.errorContainer}
>
<View style={style.errorWrapper}>
<ErrorText error={error}/>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView
edges={['bottom', 'left', 'right']}
style={style.container}
>
<StatusBar/>
<KeyboardAwareScrollView
testID={testID}
ref={this.scroll}
style={style.container}
keyboardShouldPersistTaps={'always'}
onKeyboardDidShow={this.onKeyboardDidShow}
onKeyboardDidHide={this.onKeyboardDidHide}
enableAutomaticScroll={!keyboardVisible}
>
{displayError}
<TouchableWithoutFeedback onPress={this.blur}>
<View style={style.scrollView}>
{!displayHeaderOnly && (
<View>
<View>
<FormattedText
style={style.title}
id='channel_modal.name'
defaultMessage='Name'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
testID='edit_channel.name.input'
ref={this.nameInput}
value={displayName}
onChangeText={this.onDisplayNameChangeText}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: t('channel_modal.nameEx'), defaultMessage: 'E.g.: "Bugs", "Marketing", "客户支持"'}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
underlineColorAndroid='transparent'
disableFullscreenUI={true}
maxLength={64}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
</View>
</View>
)}
{!displayHeaderOnly && (
<View>
<View style={style.titleContainer30}>
<FormattedText
style={style.title}
id='channel_modal.purpose'
defaultMessage='Purpose'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
testID='edit_channel.purpose.input'
ref={this.purposeInput}
value={purpose}
onChangeText={this.onPurposeChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: t('channel_modal.purposeEx'), defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
multiline={true}
blurOnSubmit={false}
textAlignVertical='top'
underlineColorAndroid='transparent'
disableFullscreenUI={true}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
</View>
<View>
<FormattedText
style={style.helpText}
id='channel_modal.descriptionHelp'
defaultMessage='Describe how this channel should be used.'
/>
</View>
</View>
)}
<View
onLayout={this.onHeaderLayout}
style={style.titleContainer15}
>
<FormattedText
style={style.title}
id='channel_modal.header'
defaultMessage='Header'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
testID={'edit_channel.header.input'}
ref={this.headerInput}
value={header}
onChangeText={this.onHeaderChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: t('channel_modal.headerEx'), defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
multiline={true}
blurOnSubmit={false}
onFocus={this.onHeaderFocus}
textAlignVertical='top'
underlineColorAndroid='transparent'
disableFullscreenUI={true}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
</View>
<View style={style.headerHelpText}>
<FormattedText
style={style.helpText}
id='channel_modal.headerHelp'
defaultMessage={'Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com).'}
/>
</View>
</View>
</TouchableWithoutFeedback>
</KeyboardAwareScrollView>
<View style={[style.autocompleteContainer, bottomStyle]}>
<Autocomplete
cursorPosition={header.length}
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.onHeaderChangeText}
value={header}
nestedScrollEnabled={true}
onKeyboardOffsetChanged={this.onKeyboardOffsetChanged}
offsetY={8}
style={style.autocomplete}
/>
</View>
</SafeAreaView>
);
}
}
export default connect(mapStateToProps)(EditChannelInfo);
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
autocomplete: {
position: undefined,
},
autocompleteContainer: {
position: 'absolute',
width: '100%',
flex: 1,
justifyContent: 'flex-end',
},
container: {
flex: 1,
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
paddingTop: 30,
},
errorContainer: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
width: '100%',
},
errorWrapper: {
justifyContent: 'center',
alignItems: 'center',
},
inputContainer: {
marginTop: 10,
backgroundColor: theme.centerChannelBg,
},
input: {
color: theme.centerChannelColor,
fontSize: 14,
height: 40,
paddingHorizontal: 15,
},
titleContainer30: {
flexDirection: 'row',
marginTop: 30,
},
titleContainer15: {
flexDirection: 'row',
marginTop: 15,
},
title: {
fontSize: 14,
color: theme.centerChannelColor,
marginLeft: 15,
},
optional: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 14,
marginLeft: 5,
},
helpText: {
fontSize: 14,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginTop: 10,
marginHorizontal: 15,
},
headerHelpText: {
zIndex: -1,
},
};
});

View file

@ -1,9 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
<Connect(SafeAreaIos)
excludeFooter={true}
excludeHeader={true}
<RNCSafeAreaView
edges={
Array [
"left",
"right",
]
}
style={
Object {
"flex": 1,
}
}
>
<KeyboardAvoidingView
behavior="padding"
@ -25,47 +34,43 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
}
}
>
<View
style={null}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "#ffffff",
"color": "#3d3c40",
"fontSize": 13,
}
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "#ffffff",
"color": "#3d3c40",
"fontSize": 13,
}
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onAnimationComplete={[Function]}
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.8)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
}
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onAnimationComplete={[Function]}
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.8)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
<View
style={
@ -10220,5 +10225,5 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
</KeyboardTrackingView>
</View>
</KeyboardAvoidingView>
</Connect(SafeAreaIos)>
</RNCSafeAreaView>
`;

View file

@ -7,12 +7,11 @@ import {
View,
} from 'react-native';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import {SafeAreaView} from 'react-native-safe-area-context';
import SafeAreaView from 'app/components/safe_area_view';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import SearchBar from 'app/components/search_bar';
import {DeviceTypes} from 'app/constants';
import {changeOpacity, getKeyboardAppearanceFromTheme} from 'app/utils/theme';
import SearchBar from '@components/search_bar';
import {DeviceTypes} from '@constants';
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
import EmojiPickerBase, {getStyleSheetFromTheme, SCROLLVIEW_NATIVE_ID} from './emoji_picker_base';
@ -38,8 +37,8 @@ export default class EmojiPicker extends EmojiPickerBase {
return (
<SafeAreaView
excludeHeader={true}
excludeFooter={true}
style={{flex: 1}}
edges={['left', 'right']}
>
<KeyboardAvoidingView
behavior='padding'
@ -48,26 +47,24 @@ export default class EmojiPicker extends EmojiPickerBase {
style={styles.flex}
>
<View style={styles.searchBar}>
<View style={padding(isLandscape)}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
value={searchTerm}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onAnimationComplete={this.setRebuiltEmojis}
/>
</View>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
value={searchTerm}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onAnimationComplete={this.setRebuiltEmojis}
/>
</View>
<View style={[styles.container]}>
{this.renderListComponent(shorten)}

View file

@ -80,18 +80,6 @@ describe('components/emoji_picker/emoji_picker.ios', () => {
expect(result).toEqual(output);
});
test('should set rebuildEmojis to true when deviceWidth changes', async () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
expect(instance.rebuildEmojis).toBe(undefined);
const newDeviceWidth = baseProps.deviceWidth * 2;
wrapper.setProps({deviceWidth: newDeviceWidth});
expect(instance.rebuildEmojis).toBe(true);
});
test('should rebuild emojis emojis when emojis change', async () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();

View file

@ -19,7 +19,6 @@ import sectionListGetItemLayout from 'react-native-section-list-get-item-layout'
import CompassIcon from '@components/compass_icon';
import Emoji from '@components/emoji';
import FormattedText from '@components/formatted_text';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
import {DeviceTypes} from '@constants';
import {emptyFunction} from '@utils/general';
import {
@ -101,8 +100,9 @@ export default class EmojiPicker extends PureComponent {
if (this.props.emojis !== prevProps.emojis) {
this.rebuildEmojis = true;
this.setRebuiltEmojis();
}
this.setRebuiltEmojis();
}
setSearchBarRef = (ref) => {
@ -305,7 +305,7 @@ export default class EmojiPicker extends PureComponent {
onPress={() => this.props.onEmojiPress(item)}
style={style.flatListRow}
>
<View style={[style.flatListEmoji, padding(this.props.isLandscape)]}>
<View style={style.flatListEmoji}>
<Emoji
emojiName={item}
textStyle={style.emojiText}

View file

@ -9,7 +9,7 @@ import {incrementEmojiPickerPage} from '@actions/views/emoji';
import {getCustomEmojis} from '@mm-redux/actions/emojis';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {getDimensions, isLandscape} from '@selectors/device';
import {isLandscape} from '@selectors/device';
import {selectEmojisByName, selectEmojisBySection} from '@selectors/emojis';
import EmojiPicker from './emoji_picker';
@ -17,7 +17,6 @@ import EmojiPicker from './emoji_picker';
function mapStateToProps(state) {
const emojisBySection = selectEmojisBySection(state);
const emojis = selectEmojisByName(state);
const {deviceWidth} = getDimensions(state);
const options = {
shouldSort: false,
threshold: 0.3,
@ -34,7 +33,6 @@ function mapStateToProps(state) {
fuse,
emojis,
emojisBySection,
deviceWidth,
isLandscape: isLandscape(state),
theme: getTheme(state),
customEmojisEnabled: getConfig(state).EnableCustomEmoji === 'true',

View file

@ -20,6 +20,12 @@ exports[`AttachmentFooter matches snapshot 1`] = `
}
>
<ForwardRef(AnimatedComponentWrapper)
edges={
Array [
"left",
"right",
]
}
style={
Object {
"alignItems": "center",

View file

@ -12,13 +12,14 @@ import {
View,
} from 'react-native';
import NetInfo from '@react-native-community/netinfo';
import {SafeAreaView} from 'react-native-safe-area-context';
import {RequestStatus} from '@mm-redux/constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {DeviceTypes, ViewTypes} from '@constants';
import {ViewTypes} from '@constants';
import {INDICATOR_BAR_HEIGHT} from '@constants/view';
import networkConnectionListener, {checkConnection} from '@utils/network';
import {t} from '@utils/i18n';
@ -33,10 +34,11 @@ const {
ANDROID_TOP_LANDSCAPE,
ANDROID_TOP_PORTRAIT,
IOS_TOP_LANDSCAPE,
IOS_TOP_PORTRAIT,
IOS_INSETS_TOP_PORTRAIT,
} = ViewTypes;
const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
export default class NetworkIndicator extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
@ -64,12 +66,17 @@ export default class NetworkIndicator extends PureComponent {
constructor(props) {
super(props);
const navBarHeight = Platform.select({
android: props.isLandscape ? ANDROID_TOP_LANDSCAPE : ANDROID_TOP_PORTRAIT,
ios: props.isLandscape ? IOS_TOP_LANDSCAPE : IOS_INSETS_TOP_PORTRAIT,
});
this.state = {
opacity: 0,
navBarHeight,
};
const navBar = this.getNavBarHeight(props.isLandscape);
this.top = new Animated.Value(navBar - INDICATOR_BAR_HEIGHT);
this.top = new Animated.Value(navBarHeight - INDICATOR_BAR_HEIGHT);
this.clearNotificationTimeout = null;
this.backgroundColor = new Animated.Value(0);
@ -83,29 +90,28 @@ export default class NetworkIndicator extends PureComponent {
this.mounted = true;
AppState.addEventListener('change', this.handleAppStateChange);
EventEmitter.on(ViewTypes.CHANNEL_NAV_BAR_CHANGED, this.getNavBarHeight);
// Attempt to connect when this component mounts
// if the websocket is already connected it does not try and connect again
this.connect(true);
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps, prevState) {
const {
currentChannelId: prevChannelId,
isLandscape: prevIsLandscape,
websocketStatus: previousWebsocketStatus,
} = prevProps;
const {currentChannelId, isLandscape, websocketErrorCount, websocketStatus} = this.props;
const {currentChannelId, websocketErrorCount, websocketStatus} = this.props;
if (currentChannelId !== prevChannelId && this.clearNotificationTimeout) {
clearTimeout(this.clearNotificationTimeout);
this.clearNotificationTimeout = null;
}
if (isLandscape !== prevIsLandscape) {
const navBar = this.getNavBarHeight(isLandscape);
if (prevState.navBarHeight !== this.state.navBarHeight) {
const initialTop = websocketErrorCount || previousWebsocketStatus === RequestStatus.FAILURE || previousWebsocketStatus === RequestStatus.NOT_STARTED ? 0 : INDICATOR_BAR_HEIGHT;
this.top.setValue(navBar - initialTop);
this.top.setValue(this.state.navBarHeight - initialTop);
}
if (this.props.isOnline) {
@ -132,6 +138,7 @@ export default class NetworkIndicator extends PureComponent {
stopPeriodicStatusUpdates();
this.networkListener.removeEventListener();
AppState.removeEventListener('change', this.handleAppStateChange);
EventEmitter.off(ViewTypes.CHANNEL_NAV_BAR_CHANGED, this.getNavBarHeight);
clearTimeout(this.connectionRetryTimeout);
this.connectionRetryTimeout = null;
@ -180,7 +187,7 @@ export default class NetworkIndicator extends PureComponent {
),
Animated.timing(
this.top, {
toValue: (this.getNavBarHeight() - INDICATOR_BAR_HEIGHT),
toValue: (this.state.navBarHeight - INDICATOR_BAR_HEIGHT),
duration: 300,
delay: 500,
useNativeDriver: false,
@ -196,26 +203,8 @@ export default class NetworkIndicator extends PureComponent {
}
};
getNavBarHeight = (isLandscape = this.props.isLandscape) => {
if (Platform.OS === 'android') {
if (isLandscape) {
return ANDROID_TOP_LANDSCAPE;
}
return ANDROID_TOP_PORTRAIT;
}
const iPhoneWithInsets = DeviceTypes.IS_IPHONE_WITH_INSETS;
if (iPhoneWithInsets && isLandscape) {
return IOS_TOP_LANDSCAPE;
} else if (iPhoneWithInsets) {
return IOS_INSETS_TOP_PORTRAIT;
} else if (isLandscape && !DeviceTypes.IS_TABLET) {
return IOS_TOP_LANDSCAPE;
}
return IOS_TOP_PORTRAIT;
getNavBarHeight = (navBarHeight) => {
this.setState({navBarHeight});
};
handleWebSocket = (open) => {
@ -329,7 +318,7 @@ export default class NetworkIndicator extends PureComponent {
Animated.timing(
this.top, {
toValue: this.getNavBarHeight(),
toValue: this.state.navBarHeight,
duration: 300,
useNativeDriver: false,
},
@ -391,14 +380,17 @@ export default class NetworkIndicator extends PureComponent {
pointerEvents='none'
style={[styles.container, {top: this.top, backgroundColor: background, opacity: this.state.opacity}]}
>
<Animated.View style={styles.wrapper}>
<AnimatedSafeAreaView
edges={['left', 'right']}
style={styles.wrapper}
>
<FormattedText
defaultMessage={defaultMessage}
id={i18nId}
style={styles.message}
/>
{action}
</Animated.View>
</AnimatedSafeAreaView>
</Animated.View>
);
}

View file

@ -14,7 +14,6 @@ import {isDateLine, isStartOfNewMessages} from '@mm-redux/utils/post_list';
import {isPostFlagged, isSystemMessage} from '@mm-redux/utils/post_utils';
import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel';
import {isLandscape} from 'app/selectors/device';
import Post from './post';
@ -86,7 +85,6 @@ function makeMapStateToProps() {
theme: getTheme(state),
isFlagged: isPostFlagged(post.id, myPreferences),
isCommentMention,
isLandscape: isLandscape(state),
previousPostExists: Boolean(previousPost),
beforePrevPostUserId: (beforePrevPost ? beforePrevPost.user_id : null),
};

View file

@ -23,7 +23,6 @@ import PostHeader from '@components/post_header';
import PostProfilePicture from '@components/post_profile_picture';
import PostPreHeader from '@components/post_header/post_pre_header';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
import {NavigationTypes} from '@constants';
@ -70,7 +69,6 @@ export default class Post extends PureComponent {
isCommentMention: PropTypes.bool,
location: PropTypes.string,
isBot: PropTypes.bool,
isLandscape: PropTypes.bool.isRequired,
previousPostExists: PropTypes.bool,
beforePrevPostUserId: PropTypes.string,
};
@ -266,7 +264,6 @@ export default class Post extends PureComponent {
skipFlaggedHeader,
skipPinnedHeader,
location,
isLandscape,
previousPostExists,
beforePrevPostUserId,
} = this.props;
@ -327,7 +324,7 @@ export default class Post extends PureComponent {
return (
<View
testID={testID}
style={[style.postStyle, highlighted, padding(isLandscape)]}
style={[style.postStyle, highlighted]}
>
<TouchableWithFeedback
onPress={this.handlePress}

View file

@ -217,56 +217,40 @@ exports[`PostDraft Should render the DraftInput 1`] = `
}
}
>
<View
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"marginLeft": 0,
"marginRight": 0,
"backgroundColor": "transparent",
"color": "#3d3c40",
"fontSize": 11,
"marginBottom": 2,
"paddingLeft": 10,
"paddingTop": 3,
"position": "absolute",
}
}
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"backgroundColor": "transparent",
"color": "#3d3c40",
"fontSize": 11,
"marginBottom": 2,
"paddingLeft": 10,
"paddingTop": 3,
"position": "absolute",
}
}
/>
<View
style={
Object {
"backgroundColor": "#ffffff",
"height": 0,
}
}
/>
</View>
/>
</View>
<View
<RNCSafeAreaView
edges={
Array [
"left",
"right",
]
}
onLayout={[Function]}
style={
Array [
Object {
"alignItems": "flex-end",
"backgroundColor": "#ffffff",
"borderTopColor": "rgba(61,60,64,0.2)",
"borderTopWidth": 1,
"flexDirection": "row",
"justifyContent": "center",
"paddingBottom": 2,
},
null,
]
Object {
"alignItems": "flex-end",
"backgroundColor": "#ffffff",
"borderTopColor": "rgba(61,60,64,0.2)",
"borderTopWidth": 1,
"flexDirection": "row",
"justifyContent": "center",
"paddingBottom": 2,
}
}
testID="post_draft"
>
@ -566,13 +550,17 @@ exports[`PostDraft Should render the DraftInput 1`] = `
</View>
</View>
</RCTScrollView>
</View>
</RNCSafeAreaView>
</KeyboardTrackingView>
`;
exports[`PostDraft Should render the ReadOnly for canPost 1`] = `
<RCTSafeAreaView
emulateUnlessSupported={true}
<RNCSafeAreaView
edges={
Array [
"bottom",
]
}
style={
Object {
"backgroundColor": "rgba(61,60,64,0.04)",
@ -617,12 +605,16 @@ exports[`PostDraft Should render the ReadOnly for canPost 1`] = `
This channel is read-only.
</Text>
</View>
</RCTSafeAreaView>
</RNCSafeAreaView>
`;
exports[`PostDraft Should render the ReadOnly for channelIsReadOnly 1`] = `
<RCTSafeAreaView
emulateUnlessSupported={true}
<RNCSafeAreaView
edges={
Array [
"bottom",
]
}
style={
Object {
"backgroundColor": "rgba(61,60,64,0.04)",
@ -667,5 +659,5 @@ exports[`PostDraft Should render the ReadOnly for channelIsReadOnly 1`] = `
This channel is read-only.
</Text>
</View>
</RCTSafeAreaView>
</RNCSafeAreaView>
`;

View file

@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
import {Platform, ScrollView, View} from 'react-native';
import {intlShape} from 'react-intl';
import HWKeyboardEvent from 'react-native-hw-keyboard-event';
import {SafeAreaView} from 'react-native-safe-area-context';
import Autocomplete from '@components/autocomplete';
import PostInput from '@components/post_draft/post_input';
@ -13,7 +14,6 @@ import QuickActions from '@components/post_draft/quick_actions';
import SendAction from '@components/post_draft/send_action';
import Typing from '@components/post_draft/typing';
import Uploads from '@components/post_draft/uploads';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
import {CHANNEL_POST_TEXTBOX_CURSOR_CHANGE, CHANNEL_POST_TEXTBOX_VALUE_CHANGE, IS_REACTION_REGEX} from '@constants/post_draft';
import {NOTIFY_ALL_MEMBERS} from '@constants/view';
import EventEmitter from '@mm-redux/utils/event_emitter';
@ -400,8 +400,8 @@ export default class DraftInput extends PureComponent {
channelDisplayName,
channelId,
cursorPositionEvent,
isLandscape,
files,
isLandscape,
maxMessageLength,
screenId,
valueEvent,
@ -417,10 +417,11 @@ export default class DraftInput extends PureComponent {
theme={theme}
registerTypingAnimation={registerTypingAnimation}
/>
<View
testID={testID}
style={[style.inputWrapper, padding(isLandscape)]}
<SafeAreaView
edges={['left', 'right']}
onLayout={this.handleLayout}
style={style.inputWrapper}
testID={testID}
>
<ScrollView
style={style.inputContainer}
@ -467,7 +468,7 @@ export default class DraftInput extends PureComponent {
/>
</View>
</ScrollView>
</View>
</SafeAreaView>
{Platform.OS === 'android' &&
<Autocomplete
cursorPositionEvent={cursorPositionEvent}

View file

@ -17,7 +17,6 @@ describe('PostInput', () => {
handleCommentDraftChanged: jest.fn(),
handlePostDraftChanged: jest.fn(),
inputEventType: '',
isLandscape: false,
maxMessageLength: 4000,
onPasteFiles: jest.fn(),
onSend: jest.fn(),

View file

@ -1,7 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PostDraft ReadOnly Should match snapshot 1`] = `
<ForwardRef(SafeAreaView)
<RNCSafeAreaView
edges={
Array [
"bottom",
]
}
style={
Object {
"backgroundColor": "rgba(61,60,64,0.04)",
@ -46,5 +51,5 @@ exports[`PostDraft ReadOnly Should match snapshot 1`] = `
}
/>
</View>
</ForwardRef(SafeAreaView)>
</RNCSafeAreaView>
`;

View file

@ -2,7 +2,8 @@
// See LICENSE.txt for license information.
import React, {ReactNode} from 'react';
import {SafeAreaView, View} from 'react-native';
import {View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
@ -16,10 +17,13 @@ interface ReadOnlyProps {
const ReadOnlyChannnel = ({theme}: ReadOnlyProps): ReactNode => {
const style = getStyle(theme);
return (
<SafeAreaView style={style.background}>
<SafeAreaView
edges={['bottom']}
style={style.background}
>
<View
testID='post_draft.read_only'
style={style.container}
testID='post_draft.read_only'
>
<CompassIcon
name='glasses'

View file

@ -11,7 +11,6 @@ import {
import EventEmitter from '@mm-redux/utils/event_emitter';
import FormattedText from '@components/formatted_text';
import SafeAreaView from '@components/safe_area_view';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {TYPING_VISIBLE, TYPING_HEIGHT} from '@constants/post_draft';
@ -95,19 +94,13 @@ export default class Typing extends PureComponent {
return (
<Animated.View style={{bottom: this.typingBottom}}>
<SafeAreaView
excludeHeader={true}
excludeFooter={true}
useLandscapeMargin={true}
<Text
style={style.typing}
ellipsizeMode='tail'
numberOfLines={1}
>
<Text
style={style.typing}
ellipsizeMode='tail'
numberOfLines={1}
>
{this.renderTyping()}
</Text>
</SafeAreaView>
{this.renderTyping()}
</Text>
</Animated.View>
);
}

View file

@ -18,7 +18,7 @@ exports[`MoreMessagesButton should match snapshot 1`] = `
Object {
"transform": Array [
Object {
"translateY": -138,
"translateY": -438,
},
],
},

View file

@ -15,7 +15,7 @@ import ViewTypes, {INDICATOR_BAR_HEIGHT} from '@constants/view';
import {makeStyleSheetFromTheme, hexToHue} from '@utils/theme';
import {t} from '@utils/i18n';
const HIDDEN_TOP = -100;
const HIDDEN_TOP = -400;
const SHOWN_TOP = 0;
export const INDICATOR_BAR_FACTOR = Math.abs(INDICATOR_BAR_HEIGHT / (HIDDEN_TOP - SHOWN_TOP));
export const MIN_INPUT = 0;

View file

@ -3,19 +3,14 @@
exports[`Reactions Should match snapshot with default emojis 1`] = `
<View
style={
Array [
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"height": 72,
"justifyContent": "space-between",
},
Object {
"paddingLeft": 12,
"paddingRight": 12,
},
]
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"height": 72,
"justifyContent": "space-between",
"paddingHorizontal": 12,
}
}
>
<ReactionButton
@ -265,19 +260,14 @@ exports[`Reactions Should match snapshot with default emojis 1`] = `
exports[`Reactions Should match snapshot with default emojis 2`] = `
<View
style={
Array [
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"height": 72,
"justifyContent": "space-between",
},
Object {
"paddingLeft": 12,
"paddingRight": 12,
},
]
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"height": 72,
"justifyContent": "space-between",
"paddingHorizontal": 12,
}
}
>
<ReactionButton

View file

@ -3,16 +3,12 @@
import {connect} from 'react-redux';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getDimensions, isLandscape} from 'app/selectors/device';
import ReactionPicker from './reaction_picker';
function mapStateToProps(state) {
const {deviceWidth} = getDimensions(state);
return {
theme: getTheme(state),
recentEmojis: state.views.recentEmojis,
deviceWidth,
isLandscape: isLandscape(state),
};
}

View file

@ -1,15 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {
View,
TouchableWithoutFeedback,
} from 'react-native';
import {View, TouchableWithoutFeedback, useWindowDimensions} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
import {
REACTION_PICKER_HEIGHT,
DEFAULT_EMOJIS,
@ -23,88 +19,73 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import ReactionButton from './reaction_button';
export default class ReactionPicker extends PureComponent {
static propTypes = {
addReaction: PropTypes.func.isRequired,
openReactionScreen: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
recentEmojis: PropTypes.array,
deviceWidth: PropTypes.number,
isLandscape: PropTypes.bool.isRequired,
const ReactionPicker = (props) => {
const {theme} = props;
const style = getStyleSheet(theme);
const {width} = useWindowDimensions();
const isSmallDevice = width < SMALL_ICON_BREAKPOINT;
const handlePress = (emoji) => {
props.addReaction(emoji);
};
let containerSize = LARGE_CONTAINER_SIZE;
let iconSize = LARGE_ICON_SIZE;
if (isSmallDevice) {
containerSize = SMALL_CONTAINER_SIZE;
iconSize = SMALL_ICON_SIZE;
}
handlePress = (emoji) => {
this.props.addReaction(emoji);
}
render() {
const {
theme,
deviceWidth,
isLandscape,
} = this.props;
const style = getStyleSheet(theme);
const isSmallDevice = deviceWidth < SMALL_ICON_BREAKPOINT;
let containerSize = LARGE_CONTAINER_SIZE;
let iconSize = LARGE_ICON_SIZE;
if (isSmallDevice) {
containerSize = SMALL_CONTAINER_SIZE;
iconSize = SMALL_ICON_SIZE;
}
// Mixing recent emojis with default list and removing duplicates
const emojis = Array.from(new Set(this.props.recentEmojis.concat(DEFAULT_EMOJIS))).splice(0, 6);
const list = emojis.map((emoji) => {
return (
<ReactionButton
key={emoji}
theme={theme}
addReaction={this.handlePress}
emoji={emoji}
iconSize={iconSize}
containerSize={containerSize}
/>
);
});
let paddingRes = padding(isLandscape, 12);
if (!paddingRes) {
paddingRes = {
paddingLeft: 12,
paddingRight: 12,
};
}
const emojis = Array.from(new Set(props.recentEmojis.concat(DEFAULT_EMOJIS))).splice(0, 6);
const list = emojis.map((emoji) => {
return (
<View style={[style.reactionListContainer, paddingRes]}>
{list}
<TouchableWithoutFeedback
onPress={this.props.openReactionScreen}
testID='open.add_reaction.button'
>
<View
style={[
style.reactionContainer,
{
width: containerSize,
height: containerSize,
},
]}
>
<CompassIcon
name='emoticon-plus-outline'
size={31.2}
style={style.icon}
/>
</View>
</TouchableWithoutFeedback>
</View>
<ReactionButton
key={emoji}
theme={theme}
addReaction={handlePress}
emoji={emoji}
iconSize={iconSize}
containerSize={containerSize}
/>
);
}
}
});
return (
<View style={style.reactionListContainer}>
{list}
<TouchableWithoutFeedback
onPress={props.openReactionScreen}
testID='open.add_reaction.button'
>
<View
style={[
style.reactionContainer,
{
width: containerSize,
height: containerSize,
},
]}
>
<CompassIcon
name='emoticon-plus-outline'
size={31.2}
style={style.icon}
/>
</View>
</TouchableWithoutFeedback>
</View>
);
};
ReactionPicker.propTypes = {
addReaction: PropTypes.func.isRequired,
openReactionScreen: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
recentEmojis: PropTypes.array,
};
export default ReactionPicker;
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
@ -117,6 +98,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
alignItems: 'center',
height: REACTION_PICKER_HEIGHT,
justifyContent: 'space-between',
paddingHorizontal: 12,
},
reactionContainer: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),

View file

@ -11,8 +11,6 @@ import Preferences from '@mm-redux/constants/preferences';
describe('Reactions', () => {
const baseProps = {
addReaction: jest.fn(),
deviceWidth: undefined,
isLandscape: false,
openReactionScreen: jest.fn(),
recentEmojis: [],
theme: Preferences.THEMES.default,

View file

@ -1,32 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SafeAreaIos should match snapshot 1`] = `
<View
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"marginLeft": 0,
"marginRight": 0,
}
}
>
<ForwardRef(AnimatedComponentWrapper)
style={
Object {
"backgroundColor": "#1153ab",
"paddingTop": 44,
"zIndex": 10,
}
}
/>
<View
style={
Object {
"backgroundColor": "#ffffff",
"height": 0,
}
}
/>
</View>
`;

View file

@ -5,7 +5,7 @@ import {connect} from 'react-redux';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import SafeAreaView from './safe_area_view';
import SafeAreaView from './safe_area';
function mapStateToProps(state) {
return {

View file

@ -1,20 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DeviceTypes, ViewTypes} from 'app/constants';
export const paddingHorizontal = (isLandscape, modifier = 0) => {
return DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? {paddingHorizontal: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null;
};
export const paddingLeft = (isLandscape, modifier = 0) => {
return DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? {paddingLeft: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null;
};
export const paddingRight = (isLandscape, modifier = 0) => {
return DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? {paddingRight: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null;
};
export const marginHorizontal = (isLandscape, modifier = 0) => {
return DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? {marginHorizontal: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null;
};

View file

@ -0,0 +1,84 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import type {Theme} from '@mm-redux/types/preferences';
type SafeAreaProps = {
backgroundColor?: string;
children: Array<React.ReactChild> | React.ReactChild;
excludeHeader?: boolean;
excludeFooter?: boolean;
excludeLeft?: boolean;
excludeRight?: boolean;
headerComponent?: React.ReactElement;
footerColor?: string;
footerComponent?: React.ReactElement;
navBarBackgroundColor?: string;
theme: Theme;
}
const SafeArea = (props: SafeAreaProps) => {
const {
backgroundColor,
excludeFooter,
excludeHeader,
excludeLeft,
excludeRight,
footerColor,
footerComponent,
headerComponent,
navBarBackgroundColor,
theme,
} = props;
const insets = useSafeAreaInsets();
const renderTop = useCallback(() => {
if (excludeHeader) {
return null;
}
let topColor = theme.sidebarHeaderBg;
if (navBarBackgroundColor) {
topColor = navBarBackgroundColor;
}
return (
<View style={{backgroundColor: topColor, zIndex: 10, paddingTop: insets.top, paddingLeft: insets.left, paddingRight: insets.right}}>
{headerComponent}
</View>
);
}, [insets, props.theme]);
let bgColor = theme.centerChannelBg;
if (backgroundColor) {
bgColor = backgroundColor;
}
let bottomColor = theme.centerChannelBg;
if (footerColor) {
bottomColor = footerColor;
}
let bottomInset = insets.bottom;
if (excludeFooter) {
bottomInset = 0;
}
return (
<View style={{flex: 1, backgroundColor: bgColor}}>
{renderTop()}
<View style={{flex: 1, marginLeft: excludeLeft ? 0 : insets.left, marginRight: excludeRight ? 0 : insets.right}}>
{props.children}
</View>
<View style={{marginBottom: bottomInset, backgroundColor: bottomColor}}>
{footerComponent}
</View>
</View>
);
};
export default SafeArea;

View file

@ -1,8 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const SafeAreaAndroid = (props) => {
return props.children;
};
export default SafeAreaAndroid;

View file

@ -1,243 +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 {Animated, Dimensions, Keyboard, NativeModules, View} from 'react-native';
import SafeArea from 'react-native-safe-area';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {DeviceTypes, ViewTypes} from 'app/constants';
import mattermostManaged from 'app/mattermost_managed';
import EphemeralStore from 'app/store/ephemeral_store';
const {StatusBarManager} = NativeModules;
const {PORTRAIT, LANDSCAPE} = ViewTypes;
export default class SafeAreaIos extends PureComponent {
static propTypes = {
backgroundColor: PropTypes.string,
children: PropTypes.node.isRequired,
excludeHeader: PropTypes.bool,
excludeFooter: PropTypes.bool,
footerColor: PropTypes.string,
footerComponent: PropTypes.node,
keyboardOffset: PropTypes.number.isRequired,
navBarBackgroundColor: PropTypes.string,
headerComponent: PropTypes.node,
theme: PropTypes.object.isRequired,
useLandscapeMargin: PropTypes.bool.isRequired,
};
static defaultProps = {
keyboardOffset: 0,
useLandscapeMargin: false,
};
constructor(props) {
super(props);
const insetTop = DeviceTypes.IS_IPHONE_WITH_INSETS ? 44 : 20;
let insetBottom = 0;
if ((DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) && props.excludeFooter) {
insetBottom = 20;
}
this.state = {
keyboard: false,
safeAreaInsets: {
top: insetTop,
left: 0,
bottom: insetBottom,
right: 0,
},
};
this.topBarHeight = new Animated.Value(insetTop);
}
componentDidMount() {
this.mounted = true;
Dimensions.addEventListener('change', this.getSafeAreaInsets);
EventEmitter.on('update_safe_area_view', this.getSafeAreaInsets);
if (EphemeralStore.safeAreaInsets[PORTRAIT] === null || EphemeralStore.safeAreaInsets[LANDSCAPE] === null) {
SafeArea.addEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
}
this.keyboardDidShowListener = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
this.getSafeAreaInsets();
}
componentWillUnmount() {
Dimensions.removeEventListener('change', this.getSafeAreaInsets);
EventEmitter.off('update_safe_area_view', this.getSafeAreaInsets);
SafeArea.removeEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
this.mounted = false;
}
getSafeAreaInsets = async (dimensions) => {
this.getStatusBarHeight();
if (DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) {
const window = dimensions?.window || Dimensions.get('window');
const orientation = window.width > window.height ? LANDSCAPE : PORTRAIT;
const {safeAreaInsets} = await SafeArea.getSafeAreaInsetsForRootView();
this.setSafeAreaInsets(safeAreaInsets, orientation);
}
}
setSafeAreaInsets = (safeAreaInsets, orientation) => {
if (EphemeralStore.safeAreaInsets[orientation] === null) {
EphemeralStore.safeAreaInsets[orientation] = safeAreaInsets;
}
if (this.mounted) {
this.setState({
safeAreaInsets: EphemeralStore.safeAreaInsets[orientation],
});
}
}
getStatusBarHeight = () => {
try {
StatusBarManager.getHeight(
(statusBarFrameData) => {
if (this.mounted) {
if (statusBarFrameData.height === 0) {
this.hideTopBar();
} else {
this.showTopBar();
}
}
},
);
} catch (e) {
// not needed
}
};
onSafeAreaInsetsForRootViewChange = ({safeAreaInsets}) => {
if (EphemeralStore.safeAreaInsets[PORTRAIT] !== null && EphemeralStore.safeAreaInsets[LANDSCAPE] !== null) {
SafeArea.removeEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
return;
}
if (DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) {
this.getStatusBarHeight();
const {width, height} = Dimensions.get('window');
const orientation = width > height ? LANDSCAPE : PORTRAIT;
this.setSafeAreaInsets(safeAreaInsets, orientation);
}
}
keyboardWillHide = () => {
this.setState({keyboard: false});
};
keyboardWillShow = () => {
this.setState({keyboard: true});
};
hideTopBar = () => {
Animated.timing(this.topBarHeight, {
toValue: 10,
duration: 350,
useNativeDriver: false,
}).start();
}
showTopBar = () => {
Animated.timing(this.topBarHeight, {
toValue: this.state.safeAreaInsets.top,
duration: 350,
useNativeDriver: false,
}).start();
}
renderTopBar = () => {
const {headerComponent, excludeHeader, navBarBackgroundColor, theme} = this.props;
if (excludeHeader) {
return null;
}
let topColor = theme.sidebarHeaderBg;
if (navBarBackgroundColor) {
topColor = navBarBackgroundColor;
}
if (headerComponent) {
return (
<Animated.View
style={{
backgroundColor: topColor,
height: this.topBarHeight,
zIndex: 10,
}}
>
{headerComponent}
</Animated.View>
);
}
return (
<Animated.View
style={{
backgroundColor: topColor,
paddingTop: this.topBarHeight,
zIndex: 10,
}}
/>
);
};
render() {
const {backgroundColor, children, excludeFooter, footerColor, footerComponent, keyboardOffset, theme, useLandscapeMargin} = this.props;
const {keyboard, safeAreaInsets} = this.state;
let bgColor = theme.centerChannelBg;
if (backgroundColor) {
bgColor = backgroundColor;
}
let bottomColor = theme.centerChannelBg;
if (footerColor) {
bottomColor = footerColor;
}
let offset = 0;
if (keyboardOffset && mattermostManaged.hasSafeAreaInsets) {
offset = keyboardOffset;
}
let bottomInset = safeAreaInsets.bottom;
if (excludeFooter) {
bottomInset = 0;
}
return (
<View
style={{
flex: 1,
backgroundColor: bgColor,
marginLeft: useLandscapeMargin ? safeAreaInsets.left : 0,
marginRight: useLandscapeMargin ? safeAreaInsets.right : 0,
}}
>
{this.renderTopBar()}
{children}
<View style={{height: keyboard ? offset : bottomInset, backgroundColor: bottomColor}}>
{footerComponent}
</View>
</View>
);
}
}

View file

@ -1,305 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import SafeArea from 'react-native-safe-area';
import {shallow} from 'enzyme';
import Preferences from '@mm-redux/constants/preferences';
import {DeviceTypes, ViewTypes} from 'app/constants';
import mattermostManaged from 'app/mattermost_managed';
import EphemeralStore from 'app/store/ephemeral_store';
import SafeAreaIos from './safe_area_view.ios';
const {PORTRAIT, LANDSCAPE} = ViewTypes;
describe('SafeAreaIos', () => {
const baseProps = {
children: [],
keyboardOffset: 100,
useLandscapeMargin: false,
theme: Preferences.THEMES.default,
};
const TEST_INSETS_1 = {
safeAreaInsets: {
top: 123,
left: 123,
bottom: 123,
right: 123,
},
};
const TEST_INSETS_2 = {
safeAreaInsets: {
top: 456,
left: 456,
bottom: 456,
right: 456,
},
};
const PORTRAIT_INSETS = {
safeAreaInsets: {
top: 111,
left: 111,
bottom: 111,
right: 111,
},
};
const LANDSCAPE_INSETS = {
safeAreaInsets: {
top: 222,
left: 222,
bottom: 222,
right: 222,
},
};
const IGNORED_INSETS = {
safeAreaInsets: {
top: 333,
left: 333,
bottom: 333,
right: 333,
},
};
SafeArea.getSafeAreaInsetsForRootView = jest.fn().mockImplementation(() => {
return Promise.resolve(TEST_INSETS_1);
});
beforeEach(() => {
EphemeralStore.safeAreaInsets = {
[PORTRAIT]: null,
[LANDSCAPE]: null,
};
});
test('should match snapshot', () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should get safe area insets on mount if DeviceTypes.IS_IPHONE_WITH_INSETS is true', async () => {
DeviceTypes.IS_IPHONE_WITH_INSETS = true;
mattermostManaged.hasSafeAreaInsets = false;
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
expect(SafeArea.getSafeAreaInsetsForRootView).toHaveBeenCalled();
await SafeArea.getSafeAreaInsetsForRootView();
expect(wrapper.state().safeAreaInsets).toEqual(TEST_INSETS_1.safeAreaInsets);
});
test('should get safe area insets on mount if mattermostManaged.hasSafeAreaInsets is true', async () => {
DeviceTypes.IS_IPHONE_WITH_INSETS = false;
mattermostManaged.hasSafeAreaInsets = true;
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
expect(SafeArea.getSafeAreaInsetsForRootView).toHaveBeenCalled();
await SafeArea.getSafeAreaInsetsForRootView();
expect(wrapper.state().safeAreaInsets).toEqual(TEST_INSETS_1.safeAreaInsets);
});
test('should not get safe area insets on mount if neither DeviceTypes.IS_IPHONE_WITH_INSET nor mattermostManaged.hasSafeAreaInsets is true', async () => {
DeviceTypes.IS_IPHONE_WITH_INSETS = false;
mattermostManaged.hasSafeAreaInsets = false;
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
expect(SafeArea.getSafeAreaInsetsForRootView).not.toHaveBeenCalled();
await SafeArea.getSafeAreaInsetsForRootView();
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_1.safeAreaInsets);
});
test('should set safe area insets on change if mounted and DeviceTypes.IS_IPHONE_WITH_INSETS is true', async () => {
DeviceTypes.IS_IPHONE_WITH_INSETS = true;
mattermostManaged.hasSafeAreaInsets = false;
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
const instance = wrapper.instance();
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_2);
expect(wrapper.state().safeAreaInsets).toEqual(TEST_INSETS_2.safeAreaInsets);
});
test('should set safe area insets on change if mounted and mattermostManaged.hasSafeAreaInsets is true', async () => {
DeviceTypes.IS_IPHONE_WITH_INSETS = false;
mattermostManaged.hasSafeAreaInsets = true;
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
const instance = wrapper.instance();
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_2);
expect(wrapper.state().safeAreaInsets).toEqual(TEST_INSETS_2.safeAreaInsets);
});
test('should not set safe area insets on change if mounted and neither DeviceTypes.IS_IPHONE_WITH_INSETS nor mattermostManaged.hasSafeAreaInsets is true', async () => {
DeviceTypes.IS_IPHONE_WITH_INSETS = false;
mattermostManaged.hasSafeAreaInsets = false;
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
const instance = wrapper.instance();
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_2);
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
});
test('should set safe area insets on change not mounted', async () => {
DeviceTypes.IS_IPHONE_WITH_INSETS = true;
mattermostManaged.hasSafeAreaInsets = true;
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
const instance = wrapper.instance();
instance.mounted = false;
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_2);
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
});
test('should set portrait safe area insets', async () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
expect(wrapper.state().safeAreaInsets).not.toEqual(PORTRAIT_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
const orientation = PORTRAIT;
const instance = wrapper.instance();
instance.setSafeAreaInsets(PORTRAIT_INSETS.safeAreaInsets, orientation);
expect(wrapper.state().safeAreaInsets).toEqual(PORTRAIT_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(PORTRAIT_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
});
test('should set portrait safe area insets from EphemeralStore', async () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
EphemeralStore.safeAreaInsets[PORTRAIT] = PORTRAIT_INSETS.safeAreaInsets;
expect(wrapper.state().safeAreaInsets).not.toEqual(PORTRAIT_INSETS.safeAreaInsets);
const orientation = PORTRAIT;
const instance = wrapper.instance();
instance.setSafeAreaInsets(IGNORED_INSETS.safeAreaInsets, orientation);
expect(wrapper.state().safeAreaInsets).toEqual(PORTRAIT_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(PORTRAIT_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
});
test('should set landscape safe area insets', () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
expect(wrapper.state().safeAreaInsets).not.toEqual(LANDSCAPE_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
const orientation = LANDSCAPE;
const instance = wrapper.instance();
instance.setSafeAreaInsets(LANDSCAPE_INSETS.safeAreaInsets, orientation);
expect(wrapper.state().safeAreaInsets).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
});
test('should set landscape safe area insets from EphemeralStore', async () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
EphemeralStore.safeAreaInsets[LANDSCAPE] = LANDSCAPE_INSETS.safeAreaInsets;
expect(wrapper.state().safeAreaInsets).not.toEqual(LANDSCAPE_INSETS.safeAreaInsets);
const orientation = LANDSCAPE;
const instance = wrapper.instance();
instance.setSafeAreaInsets(IGNORED_INSETS.safeAreaInsets, orientation);
expect(wrapper.state().safeAreaInsets).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
});
test('should add safeAreaInsetsForRootViewDidChange listener when EphemeralStore values are not set', async () => {
const addEventListener = jest.spyOn(SafeArea, 'addEventListener');
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
let wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
let instance = wrapper.instance();
expect(addEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
addEventListener.mockClear();
EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
instance = wrapper.instance();
expect(addEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
addEventListener.mockClear();
EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
EphemeralStore.safeAreaInsets[LANDSCAPE] = TEST_INSETS_1.safeAreaInsets;
wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
instance = wrapper.instance();
expect(addEventListener).not.toHaveBeenCalled();
});
test('should remove safeAreaInsetsForRootViewDidChange listener when EphemeralStore values are set', async () => {
const removeEventListener = jest.spyOn(SafeArea, 'removeEventListener');
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
const instance = wrapper.instance();
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_1);
expect(removeEventListener).not.toHaveBeenCalled();
EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_1);
expect(removeEventListener).not.toHaveBeenCalled();
EphemeralStore.safeAreaInsets[LANDSCAPE] = TEST_INSETS_1.safeAreaInsets;
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_1);
expect(removeEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
});
});

View file

@ -28,10 +28,6 @@ const DRAGGING = 'Dragging';
const SETTLING = 'Settling';
const emptyObject = {};
export const DRAWER_INITIAL_OFFSET = 40;
export const TABLET_WIDTH = 250;
export default class DrawerLayout extends Component {
static propTypes = {
children: PropTypes.any,

View file

@ -0,0 +1,57 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import DrawerLayout from './drawer_layout';
export const DRAWER_INITIAL_OFFSET = 40;
export const TABLET_WIDTH = 250;
interface DrawerLayoutAdapterProps {
children: any;
drawerBackgroundColor?: string;
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open';
drawerPosition?: 'left' | 'right';
drawerWidth: number;
keyboardDismissMode?: 'none' | 'on-drag';
forwardRef: any;
isTablet: boolean;
onDrawerClose?: () => void;
onDrawerOpen?: () => void;
onDrawerSlide?: (event: {nativeEvent: {offset: number}}) => void;
onDrawerStateChanged?: () => 'Idle' | 'Dragging' | 'Settling';
renderNavigationView: (drawerWidth: number) => any;
statusBarBackgroundColor?: string;
testID?: string;
}
const DrawerLayoutAdapter = (props: DrawerLayoutAdapterProps) => {
const insets = useSafeAreaInsets();
const horizontal = insets.left + insets.right;
return (
<DrawerLayout
drawerBackgroundColor={props.drawerBackgroundColor}
drawerLockMode={props.drawerLockMode}
drawerPosition={props.drawerPosition}
drawerWidth={props.drawerWidth - horizontal}
isTablet={props.isTablet}
keyboardDismissMode={props.keyboardDismissMode}
onDrawerClose={props.onDrawerClose}
onDrawerOpen={props.onDrawerOpen}
onDrawerSlide={props.onDrawerSlide}
onDrawerStateChanged={props.onDrawerStateChanged}
ref={props.forwardRef}
renderNavigationView={props.renderNavigationView}
statusBarBackgroundColor={props.statusBarBackgroundColor}
testID={props.testID}
useNativeAnimations={true}
>
{props.children}
</DrawerLayout>
);
};
export default DrawerLayoutAdapter;

View file

@ -1,9 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MainSidebar should match, full snapshot 1`] = `
<DrawerLayout
drawerPosition="left"
<DrawerLayoutAdapter
drawerWidth={710}
forwardRef={
Object {
"current": null,
}
}
isTablet={false}
onDrawerClose={[Function]}
onDrawerOpen={[Function]}

View file

@ -14,7 +14,6 @@ exports[`ChannelItem should match snapshot 1`] = `
"height": 44,
},
undefined,
null,
]
}
>
@ -117,7 +116,6 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
"height": 44,
},
undefined,
null,
]
}
>
@ -230,7 +228,6 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
"height": 44,
},
undefined,
null,
]
}
>
@ -343,7 +340,6 @@ exports[`ChannelItem should match snapshot for deactivated user and is currentCh
"height": 44,
},
undefined,
null,
]
}
>
@ -456,7 +452,6 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes
"height": 44,
},
undefined,
null,
]
}
>
@ -559,7 +554,6 @@ exports[`ChannelItem should match snapshot for deactivated user and not searchRe
"height": 44,
},
undefined,
null,
]
}
>
@ -662,7 +656,6 @@ exports[`ChannelItem should match snapshot for isManualUnread 1`] = `
"height": 44,
},
undefined,
null,
]
}
>
@ -769,7 +762,6 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
"height": 44,
},
undefined,
null,
]
}
>
@ -874,7 +866,6 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
Object {
"opacity": 0.5,
},
null,
]
}
>

View file

@ -11,7 +11,6 @@ import {
import {intlShape} from 'react-intl';
import {General} from '@mm-redux/constants';
import {paddingLeft as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import Badge from 'app/components/badge';
import ChannelIcon from 'app/components/channel_icon';
import {preventDoubleTap} from 'app/utils/tap';
@ -37,7 +36,6 @@ export default class ChannelItem extends PureComponent {
unreadMsgs: PropTypes.number.isRequired,
isSearchResult: PropTypes.bool,
isBot: PropTypes.bool.isRequired,
isLandscape: PropTypes.bool.isRequired,
};
static defaultProps = {
@ -78,7 +76,6 @@ export default class ChannelItem extends PureComponent {
isSearchResult,
channel,
isBot,
isLandscape,
} = this.props;
// Only ever show an archived channel if it's the currently viewed channel.
@ -173,7 +170,7 @@ export default class ChannelItem extends PureComponent {
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={this.onPress}
>
<View style={[style.container, mutedStyle, padding(isLandscape)]}>
<View style={[style.container, mutedStyle]}>
{extraBorder}
<View style={[style.item, extraItemStyle]}>
{icon}

View file

@ -39,7 +39,6 @@ describe('ChannelItem', () => {
unreadMsgs: 1,
isSearchResult: false,
isBot: false,
isLandscape: false,
};
test('should match snapshot', () => {

View file

@ -15,9 +15,8 @@ import {getTheme, getTeammateNameDisplaySetting} from '@mm-redux/selectors/entit
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
import {getUserIdFromChannelName, isChannelMuted} from '@mm-redux/utils/channel_utils';
import {displayUsername} from '@mm-redux/utils/user_utils';
import {isLandscape} from 'app/selectors/device';
import {getDraftForChannel} from 'app/selectors/views';
import {isGuest as isGuestUser} from 'app/utils/users';
import {getDraftForChannel} from '@selectors/views';
import {isGuest as isGuestUser} from '@utils/users';
import ChannelItem from './channel_item';
@ -82,7 +81,6 @@ function makeMapStateToProps() {
isBot,
isChannelMuted: isChannelMuted(member),
isGuest,
isLandscape: isLandscape(state),
isManualUnread: isManuallyUnread(state, ownProps.channelId),
mentions: member ? member.mention_count : 0,
shouldHideChannel,

View file

@ -8,10 +8,10 @@ import {
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import {SafeAreaView} from 'react-native-safe-area-context';
import CompassIcon from '@components/compass_icon';
import SearchBar from '@components/search_bar';
import {paddingLeft as padding} from '@components/safe_area_view/iphone_x_spacing';
import {ViewTypes} from '@constants';
import {
changeOpacity,
@ -32,7 +32,6 @@ export default class ChannelsList extends PureComponent {
onSearchStart: PropTypes.func.isRequired,
onSelectChannel: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
onShowTeams: PropTypes.func,
};
@ -93,11 +92,7 @@ export default class ChannelsList extends PureComponent {
render() {
const {intl} = this.context;
const {
onShowTeams,
theme,
isLandscape,
} = this.props;
const {onShowTeams, theme} = this.props;
const {searching, term} = this.state;
const styles = getStyleSheet(theme);
@ -130,7 +125,7 @@ export default class ChannelsList extends PureComponent {
const title = (
<View
style={[styles.searchContainer, padding(isLandscape)]}
style={styles.searchContainer}
>
<SearchBar
ref={this.setSearchBarRef}
@ -158,7 +153,8 @@ export default class ChannelsList extends PureComponent {
);
return (
<View
<SafeAreaView
edges={['left']}
style={styles.container}
testID='channels.list'
>
@ -166,7 +162,7 @@ export default class ChannelsList extends PureComponent {
{title}
</View>
{list}
</View>
</SafeAreaView>
);
}
}

View file

@ -13,13 +13,12 @@ import {
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import ChannelItem from '@components/sidebars/main/channels_list/channel_item';
import {ListTypes} from '@constants';
import {General} from '@mm-redux/constants';
import {sortChannelsByDisplayName} from '@mm-redux/utils/channel_utils';
import {displayUsername} from '@mm-redux/utils/user_utils';
import {t} from 'app/utils/i18n';
import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item';
import {ListTypes} from 'app/constants';
import {paddingLeft} from 'app/components/safe_area_view/iphone_x_spacing';
import {t} from '@utils/i18n';
const VIEWABILITY_CONFIG = ListTypes.VISIBILITY_CONFIG_DEFAULTS;
@ -50,7 +49,6 @@ class FilteredList extends Component {
styles: PropTypes.object.isRequired,
term: PropTypes.string,
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
};
static defaultProps = {
@ -354,23 +352,21 @@ class FilteredList extends Component {
};
renderSectionHeader = ({section}) => {
const {intl, isLandscape, styles} = this.props;
const {intl, styles} = this.props;
const {
defaultMessage,
id,
} = section;
return (
<React.Fragment>
<View style={[styles.titleContainer, paddingLeft(isLandscape)]}>
<Text style={styles.title}>
{intl.formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
<View style={styles.separatorContainer}>
<View style={styles.separator}/>
</View>
<View style={styles.titleContainer}>
<Text style={styles.title}>
{intl.formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
<View style={styles.separatorContainer}>
<View style={styles.separator}/>
</View>
</React.Fragment>
</View>
);
};

View file

@ -21,7 +21,6 @@ import {getConfig} from '@mm-redux/selectors/entities/general';
import {getCurrentTeam} from '@mm-redux/selectors/entities/teams';
import {getCurrentUserId, getProfilesInCurrentTeam, getUsers, getUserIdsInChannels, getUserStatuses} from '@mm-redux/selectors/entities/users';
import {getDirectShowPreferences, getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
import {isLandscape} from '@selectors/device';
import FilteredList from './filtered_list';
@ -127,7 +126,6 @@ function mapStateToProps(state) {
pastDirectMessages: pastDirectMessages(state),
restrictDms,
theme: getTheme(state),
isLandscape: isLandscape(state),
};
}

View file

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

View file

@ -3,6 +3,7 @@
import {connect} from 'react-redux';
import {DeviceTypes, ViewTypes} from '@constants';
import {General} from '@mm-redux/constants';
import {
getSortedFavoriteChannelIds,
@ -19,9 +20,6 @@ import {getConfig, getLicense, hasNewPermissions} from '@mm-redux/selectors/enti
import {haveITeamPermission} from '@mm-redux/selectors/entities/roles';
import Permissions from '@mm-redux/constants/permissions';
import {isLandscape} from 'app/selectors/device';
import {DeviceTypes, ViewTypes} from 'app/constants';
import List from './list';
const filterZeroUnreads = memoizeResult((sections) => {
@ -71,7 +69,6 @@ function mapStateToProps(state) {
theme: getTheme(state),
unreadChannelIds,
orderedChannelIds,
isLandscape: isLandscape(state),
};
}

View file

@ -21,7 +21,6 @@ import {debounce} from '@mm-redux/actions/helpers';
import CompassIcon from '@components/compass_icon';
import ChannelItem from '@components/sidebars/main/channels_list/channel_item';
import {paddingLeft} from '@components/safe_area_view/iphone_x_spacing';
import {DeviceTypes, ListTypes, NavigationTypes} from '@constants';
import {SidebarSectionTypes} from '@constants/view';
@ -48,7 +47,6 @@ export default class List extends PureComponent {
styles: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
orderedChannelIds: PropTypes.array.isRequired,
isLandscape: PropTypes.bool.isRequired,
};
static contextTypes = {
@ -326,24 +324,22 @@ export default class List extends PureComponent {
};
renderSectionHeader = ({section}) => {
const {styles, isLandscape} = this.props;
const {styles} = this.props;
const {intl} = this.context;
const {action, defaultMessage, id} = section;
const anchor = (id === 'sidebar.types.recent' || id === 'mobile.channel_list.channels');
return (
<React.Fragment>
<View style={[styles.titleContainer, paddingLeft(isLandscape)]}>
<Text style={styles.title}>
{intl.formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
<View style={styles.separatorContainer}>
<View style={styles.separator}/>
</View>
{action && this.renderSectionAction(styles, action, anchor, id)}
<View style={styles.titleContainer}>
<Text style={styles.title}>
{intl.formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
<View style={styles.separatorContainer}>
<View style={styles.separator}/>
</View>
</React.Fragment>
{action && this.renderSectionAction(styles, action, anchor, id)}
</View>
);
};

View file

@ -6,10 +6,9 @@ import {Dimensions, Keyboard} from 'react-native';
import {intlShape} from 'react-intl';
import AsyncStorage from '@react-native-community/async-storage';
import DrawerLayout, {DRAWER_INITIAL_OFFSET, TABLET_WIDTH} from '@components/sidebars/drawer_layout/index.tsx';
import {DeviceTypes, NavigationTypes} from '@constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import DrawerLayout, {DRAWER_INITIAL_OFFSET, TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
import {DeviceTypes, NavigationTypes} from 'app/constants';
import mattermostManaged from 'app/mattermost_managed';
import MainSidebarBase from './main_sidebar_base';
@ -125,7 +124,7 @@ export default class MainSidebarIOS extends MainSidebarBase {
return (
<DrawerLayout
testID={this.props.testID}
ref={this.drawerRef}
forwardRef={this.drawerRef}
renderNavigationView={this.renderNavigationView}
onDrawerClose={this.handleDrawerClose}
onDrawerOpen={this.handleDrawerOpen}
@ -137,4 +136,4 @@ export default class MainSidebarIOS extends MainSidebarBase {
</DrawerLayout>
);
}
}
}

View file

@ -4,10 +4,9 @@
import React from 'react';
import {shallow} from 'enzyme';
import {DeviceTypes} from '@constants';
import Preferences from '@mm-redux/constants/preferences';
import {DeviceTypes} from 'app/constants';
import MainSidebar from './main_sidebar.ios';
jest.mock('react-intl');
@ -31,27 +30,27 @@ describe('MainSidebar', () => {
theme: Preferences.THEMES.default,
};
test('should match, full snapshot', () => {
const wrapper = shallow(
<MainSidebar {...baseProps}/>,
const loadShallow = (props) => {
return shallow(
<MainSidebar {...(props || baseProps)}/>,
);
};
test('should match, full snapshot', () => {
const wrapper = loadShallow();
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should not set the permanentSidebar state if not Tablet', () => {
const wrapper = shallow(
<MainSidebar {...baseProps}/>,
);
const wrapper = loadShallow();
wrapper.instance().handlePermanentSidebar();
expect(wrapper.state('permanentSidebar')).toBeUndefined();
});
test('should set the permanentSidebar state if Tablet', async () => {
const wrapper = shallow(
<MainSidebar {...baseProps}/>,
);
const wrapper = loadShallow();
DeviceTypes.IS_TABLET = true;
@ -71,9 +70,7 @@ describe('MainSidebar', () => {
theme,
};
const wrapper = shallow(
<MainSidebar {...props}/>,
);
const wrapper = loadShallow(props);
const instance = wrapper.instance();
instance.render = jest.fn();
@ -82,13 +79,4 @@ describe('MainSidebar', () => {
wrapper.setProps({theme: newTheme});
expect(instance.render).toHaveBeenCalledTimes(1);
});
test('should render main sidebar below PostList for iOS', () => {
const wrapper = shallow(
<MainSidebar {...baseProps}/>,
);
const drawer = wrapper.dive().childAt(1);
const drawerStyle = drawer.props().style.reduce((acc, obj) => ({...acc, ...obj}));
expect(drawerStyle).toHaveProperty('zIndex', 0);
});
});

View file

@ -10,20 +10,18 @@ import {
View,
} from 'react-native';
import SafeAreaView from '@components/safe_area_view';
import {NavigationTypes, WebsocketEvents} from '@constants';
import {General} from '@mm-redux/constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import SafeAreaView from 'app/components/safe_area_view';
import {NavigationTypes, WebsocketEvents} from 'app/constants';
import tracker from 'app/utils/time_tracker';
import {t} from 'app/utils/i18n';
import {t} from '@utils/i18n';
import tracker from '@utils/time_tracker';
import telemetry from '@telemetry/';
import ChannelsList from './channels_list';
import DrawerSwiper from './drawer_swiper';
import TeamsList from './teams_list';
import telemetry from 'app/telemetry';
export default class MainSidebarBase extends Component {
static propTypes = {
actions: PropTypes.shape({
@ -246,6 +244,8 @@ export default class MainSidebarBase extends Component {
return (
<SafeAreaView
excludeFooter={true}
excludeLeft={true}
excludeRight={true}
navBarBackgroundColor={theme.sidebarBg}
backgroundColor={theme.sidebarHeaderBg}
footerColor={theme.sidebarBg}

View file

@ -13,19 +13,19 @@ import {
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import {SafeAreaView} from 'react-native-safe-area-context';
import {showModal} from '@actions/navigation';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {DeviceTypes, ListTypes, ViewTypes} from '@constants';
import {getCurrentServerUrl} from '@init/credentials';
import telemetry from '@telemetry';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {removeProtocol} from '@utils/url';
import tracker from '@utils/time_tracker';
import {getCurrentServerUrl} from 'app/init/credentials';
import telemetry from 'app/telemetry';
import TeamsListItem from './teams_list_item';
const {ANDROID_TOP_PORTRAIT} = ViewTypes;
@ -155,7 +155,10 @@ export default class TeamsList extends PureComponent {
}
return (
<View style={styles.container}>
<SafeAreaView
edges={['left']}
style={styles.container}
>
<View style={styles.headerContainer}>
<FormattedText
id='mobile.drawer.teamsTitle'
@ -172,7 +175,7 @@ export default class TeamsList extends PureComponent {
keyExtractor={this.keyExtractor}
viewabilityConfig={VIEWABILITY_CONFIG}
/>
</View>
</SafeAreaView>
);
}
}

View file

@ -6,8 +6,6 @@ import {connect} from 'react-redux';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getCurrentTeamId, getTeam, makeGetBadgeCountForTeamId} from '@mm-redux/selectors/entities/teams';
import {isLandscape} from 'app/selectors/device';
import TeamsListItem from './teams_list_item.js';
function makeMapStateToProps() {
@ -22,7 +20,6 @@ function makeMapStateToProps() {
mentionCount: getMentionCount(state, ownProps.teamId),
name: team.name,
theme: getTheme(state),
isLandscape: isLandscape(state),
};
};
}

View file

@ -12,7 +12,6 @@ import {
import Badge from '@components/badge';
import CompassIcon from '@components/compass_icon';
import TeamIcon from '@components/team_icon';
import {paddingLeft as padding} from '@components/safe_area_view/iphone_x_spacing';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@ -26,7 +25,6 @@ export default class TeamsListItem extends React.PureComponent {
selectTeam: PropTypes.func.isRequired,
teamId: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
};
selectTeam = preventDoubleTap(() => {
@ -42,7 +40,6 @@ export default class TeamsListItem extends React.PureComponent {
name,
teamId,
theme,
isLandscape,
} = this.props;
const styles = getStyleSheet(theme);
@ -79,7 +76,7 @@ export default class TeamsListItem extends React.PureComponent {
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={this.selectTeam}
>
<View style={[styles.teamContainer, padding(isLandscape)]}>
<View style={styles.teamContainer}>
<View>
<TeamIcon
teamId={teamId}

View file

@ -1,10 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SettingsSidebar should match snapshot 1`] = `
<DrawerLayout
<DrawerLayoutAdapter
drawerPosition="right"
drawerWidth={710}
isTablet={false}
forwardRef={
Object {
"current": null,
}
}
onDrawerClose={[Function]}
onDrawerOpen={[Function]}
renderNavigationView={[Function]}

View file

@ -27,9 +27,9 @@ export default class SettingsDrawerAndroid extends SettingsSidebarBase {
this.goToEditProfileScreen(intl);
});
goToFlagged = preventDoubleTap(() => {
goToSaved = preventDoubleTap(() => {
const {intl} = this.providerRef.getChildContext();
this.goToFlaggedScreen(intl);
this.goToSavedPostsScreen(intl);
});
goToMentions = preventDoubleTap(() => {

View file

@ -7,11 +7,11 @@ import {Dimensions, Keyboard, View} from 'react-native';
import EventEmitter from '@mm-redux/utils/event_emitter';
import SafeAreaView from 'app/components/safe_area_view';
import DrawerLayout, {DRAWER_INITIAL_OFFSET, TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
import {DeviceTypes, NavigationTypes} from 'app/constants';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import SafeAreaView from '@components/safe_area_view';
import DrawerLayout, {DRAWER_INITIAL_OFFSET, TABLET_WIDTH} from '@components/sidebars/drawer_layout/index.tsx';
import {DeviceTypes, NavigationTypes} from '@constants';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import SettingsSidebarBase from './settings_sidebar_base';
@ -23,6 +23,8 @@ export default class SettingsDrawer extends SettingsSidebarBase {
constructor(props) {
super(props);
this.drawerRef = React.createRef();
this.state = {
deviceWidth: Dimensions.get('window').width,
openDrawerOffset: DRAWER_INITIAL_OFFSET,
@ -42,26 +44,22 @@ export default class SettingsDrawer extends SettingsSidebarBase {
Dimensions.removeEventListener('change', this.handleDimensions);
}
setDrawerRef = (ref) => {
this.drawerRef = ref;
}
confirmReset = (status) => {
const {intl} = this.context;
this.confirmResetBase(status, intl);
};
closeSettingsSidebar = () => {
if (this.drawerRef && this.drawerOpened) {
this.drawerRef.closeDrawer();
if (this.drawerRef.current && this.drawerOpened) {
this.drawerRef.current.closeDrawer();
}
};
open = () => {
EventEmitter.emit(NavigationTypes.BLUR_POST_DRAFT);
if (this.drawerRef && !this.drawerOpened) {
this.drawerRef.openDrawer();
if (this.drawerRef.current && !this.drawerOpened) {
this.drawerRef.current.openDrawer();
}
};
@ -93,9 +91,9 @@ export default class SettingsDrawer extends SettingsSidebarBase {
this.goToEditProfileScreen(intl);
});
goToFlagged = preventDoubleTap(() => {
goToSaved = preventDoubleTap(() => {
const {intl} = this.context;
this.goToFlaggedScreen(intl);
this.goToSavedPostsScreen(intl);
});
goToMentions = preventDoubleTap(() => {
@ -120,6 +118,8 @@ export default class SettingsDrawer extends SettingsSidebarBase {
return (
<SafeAreaView
backgroundColor={theme.centerChannelBg}
excludeLeft={true}
excludeRight={true}
navBarBackgroundColor={theme.centerChannelBg}
footerColor={theme.centerChannelBg}
footerComponent={<View style={style.container}/>}
@ -138,12 +138,12 @@ export default class SettingsDrawer extends SettingsSidebarBase {
return (
<DrawerLayout
ref={this.setDrawerRef}
drawerPosition='right'
drawerWidth={drawerWidth}
forwardRef={this.drawerRef}
renderNavigationView={this.renderNavigationView}
onDrawerClose={this.handleDrawerClose}
onDrawerOpen={this.handleDrawerOpen}
drawerPosition='right'
drawerWidth={drawerWidth}
useNativeAnimations={true}
>
{children}

View file

@ -92,9 +92,9 @@ export default class SettingsSidebarBase extends PureComponent {
);
};
goToFlaggedScreen = (intl) => {
goToSavedPostsScreen = (intl) => {
this.openModal(
'FlaggedPosts',
'SavedPosts',
intl.formatMessage({id: 'search_header.title3', defaultMessage: 'Saved Messages'}),
);
};
@ -228,7 +228,7 @@ export default class SettingsSidebarBase extends PureComponent {
defaultMessage='Saved Messages'
i18nId='search_header.title3'
iconName='bookmark-outline'
onPress={this.goToFlagged}
onPress={this.goToSaved}
separator={false}
theme={theme}
/>

View file

@ -236,6 +236,7 @@ export default class SlideUpPanel extends PureComponent {
const styles = getStyleSheet(theme);
const translateStyle = {
alignItems: 'center',
transform: [{translateY: this.translateY}],
};
const backdropStyle = {
@ -289,32 +290,34 @@ export default class SlideUpPanel extends PureComponent {
</Animated.View>
</PanGestureHandler>
{headerComponent}
<PanGestureHandler
ref={this.panRef}
simultaneousHandlers={[this.scrollRef, this.masterRef]}
waitFor={this.headerRef}
shouldCancelWhenOutside={false}
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onHandlerStateChange}
>
<Animated.View style={[styles.container, !headerComponent && styles.border]}>
<NativeViewGestureHandler
ref={this.scrollRef}
waitFor={this.masterRef}
simultaneousHandlers={this.panRef}
>
<Animated.ScrollView
ref={this.scrollViewRef}
bounces={false}
onScrollBeginDrag={this.onRegisterLastScroll}
scrollEventThrottle={1}
style={{marginBottom: (this.props.marginFromTop + BOTTOM_MARGIN)}}
<View style={styles.wrapper}>
<PanGestureHandler
ref={this.panRef}
simultaneousHandlers={[this.scrollRef, this.masterRef]}
waitFor={this.headerRef}
shouldCancelWhenOutside={false}
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onHandlerStateChange}
>
<Animated.View style={[styles.container, !headerComponent && styles.border]}>
<NativeViewGestureHandler
ref={this.scrollRef}
waitFor={this.masterRef}
simultaneousHandlers={this.panRef}
>
{children}
</Animated.ScrollView>
</NativeViewGestureHandler>
</Animated.View>
</PanGestureHandler>
<Animated.ScrollView
ref={this.scrollViewRef}
bounces={false}
onScrollBeginDrag={this.onRegisterLastScroll}
scrollEventThrottle={1}
style={{marginBottom: (this.props.marginFromTop + BOTTOM_MARGIN)}}
>
{children}
</Animated.ScrollView>
</NativeViewGestureHandler>
</Animated.View>
</PanGestureHandler>
</View>
</Animated.View>
</View>
</TapGestureHandler>
@ -331,6 +334,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flex: 1,
backgroundColor: theme.centerChannelBg,
},
wrapper: {
flex: 1,
maxWidth: 450,
width: '100%',
},
border: {
...Platform.select({
android: {

View file

@ -35,16 +35,13 @@ exports[`components/widgets/settings/RadioSetting should match snapshot when err
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"borderBottomColor": "rgba(61,60,64,0.1)",
"borderBottomWidth": 1,
"borderTopColor": "rgba(61,60,64,0.1)",
"borderTopWidth": 1,
},
null,
]
Object {
"backgroundColor": "#ffffff",
"borderBottomColor": "rgba(61,60,64,0.1)",
"borderBottomWidth": 1,
"borderTopColor": "rgba(61,60,64,0.1)",
"borderTopWidth": 1,
}
}
>
<ForwardRef
@ -52,14 +49,11 @@ exports[`components/widgets/settings/RadioSetting should match snapshot when err
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"paddingHorizontal": 15,
},
null,
]
Object {
"alignItems": "center",
"flexDirection": "row",
"paddingHorizontal": 15,
}
}
>
<View
@ -93,14 +87,11 @@ exports[`components/widgets/settings/RadioSetting should match snapshot when err
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"paddingHorizontal": 15,
},
null,
]
Object {
"alignItems": "center",
"flexDirection": "row",
"paddingHorizontal": 15,
}
}
>
<View
@ -134,14 +125,11 @@ exports[`components/widgets/settings/RadioSetting should match snapshot when err
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"paddingHorizontal": 15,
},
null,
]
Object {
"alignItems": "center",
"flexDirection": "row",
"paddingHorizontal": 15,
}
}
>
<View
@ -170,9 +158,7 @@ exports[`components/widgets/settings/RadioSetting should match snapshot when err
</View>
</ForwardRef>
</View>
<View
style={null}
>
<View>
<Text
style={
Object {

View file

@ -9,12 +9,11 @@ import {
Switch,
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import FormattedText from '@components/formatted_text';
import {
changeOpacity,
makeStyleSheetFromTheme,
} from 'app/utils/theme';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
} from '@utils/theme';
export default class BoolSetting extends PureComponent {
static propTypes = {
@ -33,11 +32,6 @@ export default class BoolSetting extends PureComponent {
optional: PropTypes.bool,
theme: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
isLandscape: PropTypes.bool.isRequired,
};
static defaultProps = {
isLandscape: false,
};
handleChange = (value) => {
@ -53,7 +47,6 @@ export default class BoolSetting extends PureComponent {
errorText,
optional,
theme,
isLandscape,
} = this.props;
const style = getStyleSheet(theme);
@ -104,27 +97,27 @@ export default class BoolSetting extends PureComponent {
}
return (
<React.Fragment>
<View style={padding(isLandscape)}>
<>
<View>
{labelContent}
</View>
<View style={style.separator}/>
<View style={style.inputContainer}>
<Text style={[style.placeholderText, padding(isLandscape)]}>
<Text style={style.placeholderText}>
{placeholder}
</Text>
<Switch
onValueChange={this.handleChange}
value={value}
style={[style.inputSwitch, padding(isLandscape)]}
style={style.inputSwitch}
/>
</View>
<View style={style.separator}/>
<View style={padding(isLandscape)}>
<View>
{helpTextContent}
{errorTextContent}
</View>
</React.Fragment>
</>
);
}
}

View file

@ -20,7 +20,6 @@ describe('components/widgets/settings/TextSetting', () => {
optional={false}
theme={theme}
onChange={onChange}
isLandscape={false}
/>,
);

View file

@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import {Text, TouchableOpacity, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
export default class RadioSetting extends PureComponent {
@ -17,15 +16,10 @@ export default class RadioSetting extends PureComponent {
default: PropTypes.string,
onChange: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
helpText: PropTypes.node,
errorText: PropTypes.node,
};
static defaultProps = {
isLandscape: false,
};
constructor(props) {
super(props);
@ -66,7 +60,6 @@ export default class RadioSetting extends PureComponent {
label,
helpText,
errorText,
isLandscape,
} = this.props;
const style = getStyleSheet(theme);
@ -90,7 +83,7 @@ export default class RadioSetting extends PureComponent {
let additionalTextContent;
if (errorText || helpText) {
additionalTextContent = (
<View style={padding(isLandscape)}>
<View >
{helpTextContent}
{errorTextContent}
</View>
@ -104,7 +97,7 @@ export default class RadioSetting extends PureComponent {
onPress={() => this.handleChange(value)}
key={value}
>
<View style={[style.container, padding(isLandscape)]}>
<View style={style.container}>
<View style={style.rowContainer}>
<Text>{text}</Text>
</View>
@ -121,7 +114,7 @@ export default class RadioSetting extends PureComponent {
<Text style={style.asterisk}>{' *'}</Text>
</View>
<View style={[style.items, padding(isLandscape)]}>
<View style={style.items}>
{options}
</View>
{additionalTextContent}

View file

@ -10,13 +10,12 @@ import {
Platform,
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import FormattedText from '@components/formatted_text';
import {
changeOpacity,
makeStyleSheetFromTheme,
getKeyboardAppearanceFromTheme,
} from 'app/utils/theme';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
} from '@utils/theme';
export default class TextSetting extends PureComponent {
static propTypes = {
@ -39,7 +38,6 @@ export default class TextSetting extends PureComponent {
onChange: PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
multiline: PropTypes.bool,
isLandscape: PropTypes.bool.isRequired,
keyboardType: PropTypes.oneOf([
'default',
'number-pad',
@ -57,7 +55,6 @@ export default class TextSetting extends PureComponent {
disabled: false,
multiline: false,
keyboardType: 'default',
isLandscape: false,
secureTextEntry: false,
};
@ -78,7 +75,6 @@ export default class TextSetting extends PureComponent {
errorText,
value,
multiline,
isLandscape,
secureTextEntry,
} = this.props;
const style = getStyleSheet(theme);
@ -151,12 +147,12 @@ export default class TextSetting extends PureComponent {
return (
<View>
<View style={[style.titleContainer, padding(isLandscape)]}>
<View style={style.titleContainer}>
{labelContent}
{asterisk}
{optionalContent}
</View>
<View style={[style.inputContainer, padding(isLandscape), noediting]}>
<View style={[style.inputContainer, noediting]}>
<View>
<TextInput
value={value}
@ -177,7 +173,7 @@ export default class TextSetting extends PureComponent {
/>
</View>
</View>
<View style={padding(isLandscape)}>
<View>
{disabledTextContent}
{helpTextContent}
{errorTextContent}

View file

@ -1,12 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import RNFetchBlobFS from 'rn-fetch-blob/fs';
import keyMirror from '@mm-redux/utils/key_mirror';
const DeviceModel = DeviceInfo.getModel();
const deviceTypes = keyMirror({
CONNECTION_CHANGED: null,
DEVICE_DIMENSIONS_CHANGED: null,
@ -19,7 +18,7 @@ export default {
...deviceTypes,
DOCUMENTS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Documents`,
IMAGES_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Images`,
IS_IPHONE_WITH_INSETS: DeviceModel.includes('iPhone X') || DeviceModel.includes('iPhone 11'),
IS_IPHONE_WITH_INSETS: Platform.OS === 'ios' && DeviceInfo.hasNotch(),
IS_TABLET: DeviceInfo.isTablet(),
VIDEOS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Videos`,
PERMANENT_SIDEBAR_SETTINGS: '@PERMANENT_SIDEBAR_SETTINGS',

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