MM-22968 Restyle mobile autocomplete (#4531) (#4846)

* WIP: slash suggestion autocomplete

* WIP: patched styles a bit

* WIP: Adding styles

* Adding active state to autocomplete items

* Fixing style for channel mention item

* Fixing bugs + styling issues for Android

* Updating snapshot

* Fixing autocomplete to render on top of post draft

- Misc style fixes

* Renaming props, patching slash suggestion icon

* Fixing tests and lint errors

* Resolving post-merge issue with slash commands

* Fixing android positioning for autocomplete

* Fixing autocomplete not scrolling in edit_channel_info

* WIP: Fixing things according to UX Review

* UX Fixes to autocomplete

* Updating snapshots

* Updating snapshots, replacing slash-command icons

* Fixing android scrolling and positioning issues

* Fixing issues with date_suggestion not rendering

* Making use of the "ShowFullName" config in at_mention_item

* Removing top border on first autocomplete section

* Allowing autocomplete to be smaller than its maxWidth

* Fixing slash_suggestion padding

* removing "componentWillReceiveProps" from date_suggestion

* Changing edit_channel_info autocomplete offset

* Replacing toUpperCase() with textTransform: uppercase

* Fixing odd border issues + prop validation warning

* Restore section header background & add paddingBottom

* Patching up padding on channel mentions

- Reverting previous incorrect padding adjustments

* Removing inline 'completeSuggestion' function

* Removing brackets from style prop

(cherry picked from commit 59045e3bb0)

Co-authored-by: Andre Vasconcelos <andre.onogoro@gmail.com>
This commit is contained in:
Mattermost Build
2020-09-25 14:39:38 -04:00
committed by GitHub
parent 5778019074
commit d8ff0e52bb
26 changed files with 430 additions and 302 deletions

View File

@@ -9,7 +9,6 @@ import {RequestStatus} from '@mm-redux/constants';
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
import AtMentionItem from 'app/components/autocomplete/at_mention_item';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
import GroupMentionItem from 'app/components/autocomplete/at_mention_group/at_mention_group';
@@ -67,7 +66,7 @@ export default class AtMention extends PureComponent {
mentionComplete: false,
sections: [],
});
this.props.onResultCountChange(0);
return;
}
@@ -208,12 +207,14 @@ export default class AtMention extends PureComponent {
};
renderSectionHeader = ({section}) => {
const isFirstSection = section.id === this.state.sections[0].id;
return (
<AutocompleteSectionHeader
id={section.id}
defaultMessage={section.defaultMessage}
theme={this.props.theme}
isLandscape={this.props.isLandscape}
isFirstSection={isFirstSection}
/>
);
};
@@ -270,7 +271,6 @@ export default class AtMention extends PureComponent {
sections={sections}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
ItemSeparatorComponent={AutocompleteDivider}
initialNumToRender={10}
nestedScrollEnabled={nestedScrollEnabled}
/>
@@ -282,6 +282,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
listView: {
backgroundColor: theme.centerChannelBg,
borderRadius: 4,
},
};
});

View File

@@ -12,7 +12,7 @@ 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} from 'app/utils/theme';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import FormattedText from 'app/components/formatted_text';
export default class AtMentionItem extends PureComponent {
@@ -28,6 +28,7 @@ export default class AtMentionItem extends PureComponent {
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
isCurrentUser: PropTypes.bool.isRequired,
showFullName: PropTypes.string,
};
static defaultProps = {
@@ -40,11 +41,24 @@ export default class AtMentionItem extends PureComponent {
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;
}
render() {
const {
firstName,
lastName,
nickname,
userId,
username,
theme,
@@ -55,46 +69,52 @@ export default class AtMentionItem extends PureComponent {
} = this.props;
const style = getStyleFromTheme(theme);
const hasFullName = firstName.length > 0 && lastName.length > 0;
const hasNickname = nickname.length > 0;
const name = this.renderNameBlock();
return (
<TouchableWithFeedback
key={userId}
onPress={this.completeMention}
style={[style.row, padding(isLandscape)]}
type={'opacity'}
style={padding(isLandscape)}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
type={'native'}
>
<View style={style.rowPicture}>
<ProfilePicture
userId={userId}
<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}
size={20}
status={null}
/>
<GuestTag
show={isGuest}
theme={theme}
/>
<Text
style={style.rowFullname}
numberOfLines={1}
>
{name}
{isCurrentUser &&
<FormattedText
id='suggestion.mention.you'
defaultMessage='(you)'
/>}
</Text>
<Text
numberOfLines={1}
style={style.rowUsername}
>
{` @${username}`}
</Text>
</View>
<Text style={style.rowUsername}>{`@${username}`}</Text>
<BotTag
show={isBot}
theme={theme}
/>
<GuestTag
show={isGuest}
theme={theme}
/>
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
<Text
style={style.rowFullname}
numberOfLines={1}
>
{hasFullName && `${firstName} ${lastName}`}
{hasNickname && ` (${nickname}) `}
{isCurrentUser &&
<FormattedText
id='suggestion.mention.you'
defaultMessage='(you)'
/>}
</Text>
</TouchableWithFeedback>
);
}
@@ -103,24 +123,28 @@ export default class AtMentionItem extends PureComponent {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
height: 40,
paddingVertical: 8,
paddingTop: 4,
paddingHorizontal: 16,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
},
rowPicture: {
marginHorizontal: 8,
width: 20,
marginRight: 10,
marginLeft: 2,
width: 24,
alignItems: 'center',
justifyContent: 'center',
},
rowUsername: {
fontSize: 13,
rowFullname: {
fontSize: 15,
color: theme.centerChannelColor,
},
rowFullname: {
rowUsername: {
color: theme.centerChannelColor,
opacity: 0.6,
fontSize: 15,
opacity: 0.56,
flex: 1,
},
};

View File

@@ -6,6 +6,7 @@ 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 AtMentionItem from './at_mention_item';
@@ -14,12 +15,13 @@ import {isGuest} from 'app/utils/users';
function mapStateToProps(state, ownProps) {
const user = getUser(state, ownProps.userId);
const config = getConfig(state);
return {
firstName: user.first_name,
lastName: user.last_name,
nickname: user.nickname,
username: user.username,
showFullName: config.ShowFullName,
isBot: Boolean(user.is_bot),
isGuest: isGuest(user),
theme: getTheme(state),

View File

@@ -36,8 +36,9 @@ export default class Autocomplete extends PureComponent {
valueEvent: PropTypes.string,
cursorPositionEvent: PropTypes.string,
nestedScrollEnabled: PropTypes.bool,
expandDown: PropTypes.bool,
onVisible: PropTypes.func,
offsetY: PropTypes.number,
onKeyboardOffsetChanged: PropTypes.func,
style: ViewPropTypes.style,
};
@@ -47,6 +48,8 @@ export default class Autocomplete extends PureComponent {
enableDateSuggestion: false,
nestedScrollEnabled: false,
onVisible: emptyFunction,
onKeyboardOffsetChanged: emptyFunction,
offsetY: 80,
};
static getDerivedStateFromProps(props, state) {
@@ -149,10 +152,12 @@ export default class Autocomplete extends PureComponent {
keyboardDidShow = (e) => {
const {height} = e.endCoordinates;
this.setState({keyboardOffset: height});
this.props.onKeyboardOffsetChanged(height);
};
keyboardDidHide = () => {
this.setState({keyboardOffset: 0});
this.props.onKeyboardOffsetChanged(0);
};
maxListHeight() {
@@ -166,42 +171,37 @@ export default class Autocomplete extends PureComponent {
offset = 90;
}
maxHeight = this.props.deviceHeight - offset - this.state.keyboardOffset;
maxHeight = (this.props.deviceHeight / 2) - offset;
}
return maxHeight;
}
render() {
const {theme, isSearch, expandDown} = this.props;
const {atMentionCount, channelMentionCount, emojiCount, commandCount, dateCount, cursorPosition, value} = this.state;
const {theme, isSearch, offsetY} = this.props;
const style = getStyleFromTheme(theme);
const maxListHeight = this.maxListHeight();
const wrapperStyles = [];
const containerStyles = [];
const containerStyles = [style.borders];
if (Platform.OS === 'ios') {
wrapperStyles.push(style.shadow);
}
if (isSearch) {
wrapperStyles.push(style.base, style.searchContainer);
containerStyles.push(style.content);
wrapperStyles.push(style.base, style.searchContainer, {height: maxListHeight});
} else {
const containerStyle = expandDown ? style.containerExpandDown : style.container;
const containerStyle = {bottom: offsetY};
containerStyles.push(style.base, containerStyle);
}
// We always need to render something, but we only draw the borders when we have results to show
const {atMentionCount, channelMentionCount, emojiCount, commandCount, dateCount, cursorPosition, value} = this.state;
if (atMentionCount + channelMentionCount + emojiCount + commandCount + dateCount > 0) {
if (this.props.isSearch) {
wrapperStyles.push(style.bordersSearch);
} else {
containerStyles.push(style.borders);
}
// Hide when there are no active autocompletes
if (atMentionCount + channelMentionCount + emojiCount + commandCount + dateCount === 0) {
wrapperStyles.push(style.hidden);
containerStyles.push(style.hidden);
}
if (this.props.style) {
containerStyles.push(this.props.style);
}
const maxListHeight = this.maxListHeight();
return (
<View style={wrapperStyles}>
<View
@@ -261,39 +261,37 @@ export default class Autocomplete extends PureComponent {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
base: {
left: 0,
overflow: 'hidden',
left: 8,
position: 'absolute',
right: 0,
right: 8,
},
borders: {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderBottomWidth: 0,
overflow: 'hidden',
borderRadius: 4,
},
bordersSearch: {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
},
container: {
bottom: 0,
},
containerExpandDown: {
top: 0,
},
content: {
flex: 1,
hidden: {
display: 'none',
},
searchContainer: {
flex: 1,
...Platform.select({
android: {
top: 46,
top: 42,
},
ios: {
top: 44,
top: 55,
},
}),
},
shadow: {
shadowColor: '#000',
shadowOpacity: 0.12,
shadowRadius: 8,
shadowOffset: {
width: 0,
height: 8,
},
},
};
});

View File

@@ -1,32 +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 {View} from 'react-native';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class AutocompleteDivider extends PureComponent {
static propTypes = {
theme: PropTypes.object.isRequired,
};
render() {
const {theme} = this.props;
const style = getStyleFromTheme(theme);
return (
<View style={style.divider}/>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
divider: {
height: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
},
};
});

View File

@@ -1,16 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import AutocompleteDivider from './autocomplete_divider';
function mapStateToProps(state) {
return {
theme: getTheme(state),
};
}
export default connect(mapStateToProps)(AutocompleteDivider);

View File

@@ -16,6 +16,7 @@ export default class AutocompleteSectionHeader extends PureComponent {
loading: PropTypes.bool,
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
isFirstSection: PropTypes.bool,
};
static defaultProps = {
@@ -23,12 +24,17 @@ export default class AutocompleteSectionHeader extends PureComponent {
};
render() {
const {defaultMessage, id, loading, theme, isLandscape} = this.props;
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={[style.section, padding(isLandscape)]}>
<View style={sectionStyles}>
<FormattedText
id={id}
defaultMessage={defaultMessage}
@@ -48,18 +54,24 @@ export default class AutocompleteSectionHeader extends PureComponent {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
section: {
justifyContent: 'center',
paddingHorizontal: 8,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTop: {
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
},
section: {
justifyContent: 'center',
position: 'relative',
top: -1,
flexDirection: 'row',
},
sectionText: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.7),
paddingVertical: 7,
fontWeight: '600',
textTransform: 'uppercase',
color: changeOpacity(theme.centerChannelColor, 0.56),
paddingTop: 16,
paddingBottom: 8,
paddingHorizontal: 16,
flex: 1,
},
sectionWrapper: {

View File

@@ -184,6 +184,7 @@ export default class ChannelMention extends PureComponent {
};
renderSectionHeader = ({section}) => {
const isFirstSection = section.id === this.state.sections[0].id;
return (
<AutocompleteSectionHeader
id={section.id}
@@ -191,6 +192,7 @@ export default class ChannelMention extends PureComponent {
loading={!section.hideLoadingIndicator && this.props.requestStatus === RequestStatus.STARTED}
theme={this.props.theme}
isLandscape={this.props.isLandscape}
isFirstSection={isFirstSection}
/>
);
};
@@ -235,6 +237,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
listView: {
backgroundColor: theme.centerChannelBg,
borderRadius: 4,
},
};
});

View File

@@ -5,14 +5,15 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
View,
} from 'react-native';
import VectorIcon from 'app/components/vector_icon.js';
import {General} from '@mm-redux/constants';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import {BotTag, GuestTag} from 'app/components/tag';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class ChannelMentionItem extends PureComponent {
static propTypes = {
@@ -49,8 +50,12 @@ export default class ChannelMentionItem extends PureComponent {
} = this.props;
const style = getStyleFromTheme(theme);
let iconName = 'public';
let component;
if (type === General.PRIVATE_CHANNEL) {
iconName = 'private';
}
if (type === General.DM_CHANNEL || type === General.GM_CHANNEL) {
if (!displayName) {
return null;
@@ -79,11 +84,19 @@ export default class ChannelMentionItem extends PureComponent {
<TouchableWithFeedback
key={channelId}
onPress={this.completeMention}
style={[style.row, padding(isLandscape)]}
type={'opacity'}
style={padding(isLandscape)}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
type={'native'}
>
<Text style={style.rowDisplayName}>{displayName}</Text>
<Text style={style.rowName}>{` (~${name})`}</Text>
<View style={style.row}>
<VectorIcon
name={iconName}
type={'mattermost'}
style={style.icon}
/>
<Text style={style.rowDisplayName}>{displayName}</Text>
<Text style={style.rowName}>{` ~${name}`}</Text>
</View>
</TouchableWithFeedback>
);
}
@@ -91,7 +104,6 @@ export default class ChannelMentionItem extends PureComponent {
return (
<React.Fragment>
{component}
<AutocompleteDivider/>
</React.Fragment>
);
}
@@ -99,19 +111,26 @@ export default class ChannelMentionItem extends PureComponent {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
icon: {
fontSize: 18,
marginRight: 11,
color: theme.centerChannelColor,
opacity: 0.56,
},
row: {
padding: 8,
paddingHorizontal: 16,
height: 40,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
},
rowDisplayName: {
fontSize: 13,
fontSize: 15,
color: theme.centerChannelColor,
},
rowName: {
fontSize: 15,
color: theme.centerChannelColor,
opacity: 0.6,
opacity: 0.56,
},
};
});

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Dimensions, Platform, StyleSheet} from 'react-native';
import {Dimensions, Platform, View} from 'react-native';
import PropTypes from 'prop-types';
import {CalendarList, LocaleConfig} from 'react-native-calendars';
import {intlShape} from 'react-intl';
@@ -10,7 +10,7 @@ import {intlShape} from 'react-intl';
import {memoizeResult} from '@mm-redux/utils/helpers';
import {DATE_MENTION_SEARCH_REGEX, ALL_SEARCH_FLAGS_REGEX} from 'app/constants/autocomplete';
import {changeOpacity} from 'app/utils/theme';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
export default class DateSuggestion extends PureComponent {
static propTypes = {
@@ -37,6 +37,7 @@ export default class DateSuggestion extends PureComponent {
this.state = {
mentionComplete: false,
active: false,
sections: [],
};
}
@@ -45,18 +46,37 @@ export default class DateSuggestion extends PureComponent {
this.setCalendarLocale();
}
onLayout = (e) => {
this.setState({calendarWidth: e.nativeEvent.layout.width});
};
componentDidUpdate(prevProps) {
const {locale, matchTerm} = this.props;
const {locale, matchTerm, enableDateSuggestion} = this.props;
const {mentionComplete} = this.state;
if ((matchTerm !== prevProps.matchTerm && matchTerm === null) || this.state.mentionComplete) {
this.resetComponent();
}
if (matchTerm === null || mentionComplete || !enableDateSuggestion) {
this.setCalendarActive(false);
return;
}
if (matchTerm !== null) {
this.props.onResultCountChange(1);
this.setCalendarActive(true);
}
if (locale !== prevProps.locale) {
this.setCalendarLocale();
}
}
setCalendarActive = (active) => {
this.setState({active});
}
completeMention = (day) => {
const mention = day.dateString;
const {cursorPosition, onChangeText, value} = this.props;
@@ -118,10 +138,11 @@ export default class DateSuggestion extends PureComponent {
};
render() {
const {mentionComplete} = this.state;
const {matchTerm, enableDateSuggestion, theme} = this.props;
const {active, calendarWidth} = this.state;
const {theme} = this.props;
const styles = getStyleFromTheme(theme);
if (matchTerm === null || mentionComplete || !enableDateSuggestion) {
if (!active) {
// If we are not in an active state or the mention has been completed return null so nothing is rendered
// other components are not blocked.
return null;
@@ -131,22 +152,29 @@ export default class DateSuggestion extends PureComponent {
const calendarStyle = calendarTheme(theme);
return (
<CalendarList
<View
onLayout={this.onLayout}
style={styles.calList}
current={currentDate}
maxDate={currentDate}
pastScrollRange={24}
futureScrollRange={0}
scrollingEnabled={true}
pagingEnabled={true}
hideArrows={false}
horizontal={true}
showScrollIndicator={true}
onDayPress={this.completeMention}
showWeekNumbers={false}
theme={calendarStyle}
keyboardShouldPersistTaps='always'
/>
>
{Boolean(calendarWidth) &&
<CalendarList
current={currentDate}
maxDate={currentDate}
pastScrollRange={24}
futureScrollRange={0}
scrollingEnabled={true}
calendarWidth={calendarWidth}
pagingEnabled={true}
hideArrows={false}
horizontal={true}
showScrollIndicator={true}
onDayPress={this.completeMention}
showWeekNumbers={false}
theme={calendarStyle}
keyboardShouldPersistTaps='always'
/>
}
</View>
);
}
}
@@ -197,9 +225,13 @@ const calendarTheme = memoizeResult((theme) => ({
},
}));
const styles = StyleSheet.create({
calList: {
height: 1700,
paddingTop: 5,
},
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
calList: {
paddingTop: 5,
width: '100%',
borderRadius: 4,
backgroundColor: theme.centerChannelBg,
},
};
});

View File

@@ -4,7 +4,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 1`] = `n
exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
<FlatList
ItemSeparatorComponent={[Function]}
data={
Array [
"+1",
@@ -3044,6 +3043,8 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
Array [
Object {
"backgroundColor": "#ffffff",
"borderRadius": 4,
"paddingTop": 16,
},
Object {
"maxHeight": undefined,

View File

@@ -11,12 +11,11 @@ import {
} from 'react-native';
import Fuse from 'fuse.js';
import AutocompleteDivider from '@components/autocomplete/autocomplete_divider';
import Emoji from '@components/emoji';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {BuiltInEmojis} from '@utils/emojis';
import {getEmojiByName, compareEmojis} from '@utils/emoji_utils';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
const EMOJI_REGEX = /(^|\s|^\+|^-)(:([^:\s]*))$/i;
const EMOJI_REGEX_WITHOUT_PREFIX = /\B(:([^:\s]*))$/i;
@@ -150,21 +149,23 @@ export default class EmojiSuggestion extends PureComponent {
renderItem = ({item}) => {
const style = getStyleFromTheme(this.props.theme);
const completeSuggestion = () => this.completeSuggestion(item);
return (
<TouchableWithFeedback
onPress={() => this.completeSuggestion(item)}
style={style.row}
type={'opacity'}
onPress={completeSuggestion}
underlayColor={changeOpacity(this.props.theme.buttonBg, 0.08)}
type={'native'}
>
<View style={style.emoji}>
<Emoji
emojiName={item}
textStyle={style.emojiText}
size={20}
/>
<View style={style.row}>
<View style={style.emoji}>
<Emoji
emojiName={item}
textStyle={style.emojiText}
size={24}
/>
</View>
<Text style={style.emojiName}>{`:${item}:`}</Text>
</View>
<Text style={style.emojiName}>{`:${item}:`}</Text>
</TouchableWithFeedback>
);
};
@@ -199,12 +200,14 @@ export default class EmojiSuggestion extends PureComponent {
return values;
}, []);
const data = results.sort(sorter);
this.props.onResultCountChange(data.length);
this.setState({
active: data.length > 0,
dataSource: data,
});
}, 100);
} else {
this.props.onResultCountChange(emojis.length);
this.setState({
active: emojis.length > 0,
dataSource: emojis.sort(sorter),
@@ -229,7 +232,6 @@ export default class EmojiSuggestion extends PureComponent {
data={this.state.dataSource}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
ItemSeparatorComponent={AutocompleteDivider}
pageSize={10}
initialListSize={10}
nestedScrollEnabled={nestedScrollEnabled}
@@ -244,7 +246,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
marginRight: 5,
},
emojiName: {
fontSize: 13,
fontSize: 15,
color: theme.centerChannelColor,
},
emojiText: {
@@ -252,14 +254,17 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontWeight: 'bold',
},
listView: {
paddingTop: 16,
backgroundColor: theme.centerChannelBg,
borderRadius: 4,
},
row: {
height: 40,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
backgroundColor: theme.centerChannelBg,
overflow: 'hidden',
paddingBottom: 8,
paddingHorizontal: 16,
height: 40,
},
};
});

View File

@@ -8,7 +8,6 @@ import {
Platform,
} from 'react-native';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import SlashSuggestionItem from './slash_suggestion_item';
@@ -211,7 +210,6 @@ export default class SlashSuggestion extends PureComponent {
data={this.state.dataSource}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
ItemSeparatorComponent={AutocompleteDivider}
pageSize={10}
initialListSize={10}
nestedScrollEnabled={nestedScrollEnabled}
@@ -225,6 +223,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg,
paddingTop: 8,
borderRadius: 4,
},
};
});

View File

@@ -3,11 +3,12 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Text} from 'react-native';
import {Image, Text, View} from 'react-native';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
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 = {
@@ -39,11 +40,30 @@ export default class SlashSuggestionItem extends PureComponent {
return (
<TouchableWithFeedback
onPress={this.completeSuggestion}
style={[style.row, padding(isLandscape)]}
type={'opacity'}
style={padding(isLandscape)}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
type={'native'}
>
<Text style={style.suggestionName}>{`${suggestion} ${hint}`}</Text>
<Text style={style.suggestionDescription}>{description}</Text>
<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}>{`${suggestion.substring(1)} ${hint}`}</Text>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={style.suggestionDescription}
>
{description}
</Text>
</View>
</View>
</TouchableWithFeedback>
);
}
@@ -51,32 +71,38 @@ export default class SlashSuggestionItem extends PureComponent {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
paddingVertical: 8,
icon: {
fontSize: 24,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
width: 35,
height: 35,
marginRight: 12,
borderRadius: 4,
justifyContent: 'center',
paddingHorizontal: 8,
backgroundColor: theme.centerChannelBg,
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2),
alignItems: 'center',
marginTop: 8,
},
rowDisplayName: {
fontSize: 13,
color: theme.centerChannelColor,
iconColor: {
tintColor: theme.centerChannelColor,
},
rowName: {
color: theme.centerChannelColor,
opacity: 0.6,
container: {
flexDirection: 'row',
alignItems: 'center',
paddingBottom: 8,
paddingHorizontal: 16,
overflow: 'hidden',
},
suggestionContainer: {
flex: 1,
},
suggestionDescription: {
fontSize: 11,
color: changeOpacity(theme.centerChannelColor, 0.6),
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.56),
},
suggestionName: {
fontSize: 13,
fontSize: 15,
color: theme.centerChannelColor,
marginBottom: 5,
marginBottom: 4,
},
};
});

View File

@@ -42,25 +42,27 @@ export default class SpecialMentionItem extends PureComponent {
return (
<TouchableWithFeedback
onPress={this.completeMention}
style={style.row}
type={'opacity'}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
type={'native'}
>
<View style={style.rowPicture}>
<Icon
name='users'
style={style.rowIcon}
/>
<View style={style.row}>
<View style={style.rowPicture}>
<Icon
name='users'
style={style.rowIcon}
/>
</View>
<Text style={style.textWrapper}>
<Text style={style.rowUsername}>{`@${completeHandle}`}</Text>
<Text style={style.rowUsername}>{' - '}</Text>
<FormattedText
id={id}
defaultMessage={defaultMessage}
values={values}
style={style.rowFullname}
/>
</Text>
</View>
<Text style={style.textWrapper}>
<Text style={style.rowUsername}>{`@${completeHandle}`}</Text>
<Text style={style.rowUsername}>{' - '}</Text>
<FormattedText
id={id}
defaultMessage={defaultMessage}
values={values}
style={style.rowFullname}
/>
</Text>
</TouchableWithFeedback>
);
}
@@ -68,10 +70,11 @@ export default class SpecialMentionItem extends PureComponent {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
height: 40,
paddingVertical: 8,
paddingHorizontal: 9,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
},
rowPicture: {
marginHorizontal: 8,
@@ -81,10 +84,10 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
},
rowIcon: {
color: changeOpacity(theme.centerChannelColor, 0.7),
fontSize: 14,
fontSize: 18,
},
rowUsername: {
fontSize: 13,
fontSize: 15,
color: theme.centerChannelColor,
},
rowFullname: {

View File

@@ -309,18 +309,28 @@ exports[`EditChannelInfo should match snapshot 1`] = `
</View>
</TouchableWithoutFeedback>
</KeyboardAwareScrollView>
<KeyboardTrackingView
<View
style={
Object {
"justifyContent": "flex-end",
}
Array [
Object {
"flex": 1,
"justifyContent": "flex-end",
"position": "absolute",
"width": "100%",
},
Object {
"bottom": 0,
},
]
}
>
<Connect(Autocomplete)
cursorPosition={6}
maxHeight={200}
nestedScrollEnabled={true}
offsetY={8}
onChangeText={[Function]}
onKeyboardOffsetChanged={[Function]}
style={
Object {
"position": undefined,
@@ -328,6 +338,6 @@ exports[`EditChannelInfo should match snapshot 1`] = `
}
value="header"
/>
</KeyboardTrackingView>
</View>
</React.Fragment>
`;

View File

@@ -9,11 +9,10 @@ import {
View,
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scrollview';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import {General} from '@mm-redux/constants';
import Autocomplete from 'app/components/autocomplete';
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';
@@ -71,6 +70,7 @@ export default class EditChannelInfo extends PureComponent {
this.state = {
keyboardVisible: false,
keyboardPosition: 0,
};
}
@@ -174,6 +174,10 @@ export default class EditChannelInfo extends PureComponent {
this.setState({keyboardVisible: false});
}
onKeyboardOffsetChanged = (keyboardPosition) => {
this.setState({keyboardPosition});
}
onHeaderFocus = () => {
if (this.state.keyboardVisible) {
this.scrollHeaderToTop();
@@ -201,8 +205,13 @@ export default class EditChannelInfo extends PureComponent {
error,
saving,
} = this.props;
const {keyboardVisible} = this.state;
const {keyboardVisible, keyboardPosition} = this.state;
const bottomStyle = {
bottom: Platform.select({
ios: keyboardPosition,
android: 0,
}),
};
const style = getStyleSheet(theme);
const displayHeaderOnly = channelType === General.DM_CHANNEL ||
@@ -354,16 +363,18 @@ export default class EditChannelInfo extends PureComponent {
</View>
</TouchableWithoutFeedback>
</KeyboardAwareScrollView>
<KeyboardTrackingView style={style.autocompleteContainer}>
<View style={[style.autocompleteContainer, bottomStyle]}>
<Autocomplete
cursorPosition={header.length}
maxHeight={200}
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.onHeaderChangeText}
value={header}
nestedScrollEnabled={true}
onKeyboardOffsetChanged={this.onKeyboardOffsetChanged}
offsetY={8}
style={style.autocomplete}
/>
</KeyboardTrackingView>
</View>
</React.Fragment>
);
}
@@ -375,6 +386,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
position: undefined,
},
autocompleteContainer: {
position: 'absolute',
width: '100%',
flex: 1,
justifyContent: 'flex-end',
},
container: {

View File

@@ -415,16 +415,6 @@ export default class DraftInput extends PureComponent {
theme={theme}
registerTypingAnimation={registerTypingAnimation}
/>
{Platform.OS === 'android' &&
<Autocomplete
cursorPositionEvent={cursorPositionEvent}
maxHeight={Math.min(this.state.top - AUTOCOMPLETE_MARGIN, AUTOCOMPLETE_MAX_HEIGHT)}
onChangeText={this.handleInputQuickAction}
valueEvent={valueEvent}
rootId={rootId}
channelId={channelId}
/>
}
<View
style={[style.inputWrapper, padding(isLandscape)]}
onLayout={this.handleLayout}
@@ -475,6 +465,16 @@ export default class DraftInput extends PureComponent {
</View>
</ScrollView>
</View>
{Platform.OS === 'android' &&
<Autocomplete
cursorPositionEvent={cursorPositionEvent}
maxHeight={Math.min(this.state.top - AUTOCOMPLETE_MARGIN, AUTOCOMPLETE_MAX_HEIGHT)}
onChangeText={this.handleInputQuickAction}
valueEvent={valueEvent}
rootId={rootId}
channelId={channelId}
/>
}
</>
);
}
@@ -513,4 +513,4 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderTopColor: changeOpacity(theme.centerChannelColor, 0.20),
},
};
});
});

View File

@@ -64,15 +64,6 @@ export default class ChannelIOS extends ChannelBase {
updateNativeScrollView={this.updateNativeScrollView}
registerTypingAnimation={this.registerTypingAnimation}
/>
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
<Autocomplete
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
channelId={currentChannelId}
/>
</View>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener/>}
</>
);
@@ -102,6 +93,15 @@ export default class ChannelIOS extends ChannelBase {
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
/>
}
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
<Autocomplete
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
channelId={currentChannelId}
/>
</View>
</>
);

View File

@@ -87,6 +87,7 @@ exports[`EditPost should match snapshot 1`] = `
cursorPosition={0}
maxHeight={200}
nestedScrollEnabled={true}
offsetY={8}
onChangeText={[Function]}
onVisible={[Function]}
style={

View File

@@ -281,6 +281,7 @@ export default class EditPost extends PureComponent {
value={message}
nestedScrollEnabled={true}
onVisible={this.onAutocompleteVisible}
offsetY={8}
style={style.autocomplete}
/>
</KeyboardTrackingView>

View File

@@ -48,18 +48,6 @@ exports[`thread should match snapshot, has root post 1`] = `
scrollViewNativeID="threadPostList"
/>
</ForwardRef(AnimatedComponentWrapper)>
<View
nativeID="threadAccessoriesContainer"
>
<Connect(Autocomplete)
channelId="channel_id"
cursorPositionEvent="onThreadTextBoxCursorChange"
maxHeight={200}
onChangeText={[Function]}
rootId="root_id"
valueEvent="onThreadTextBoxValueChange"
/>
</View>
</React.Fragment>
</Connect(SafeAreaIos)>
<Connect(PostDraft)
@@ -72,6 +60,18 @@ exports[`thread should match snapshot, has root post 1`] = `
scrollViewNativeID="threadPostList"
valueEvent="onThreadTextBoxValueChange"
/>
<View
nativeID="threadAccessoriesContainer"
>
<Connect(Autocomplete)
channelId="channel_id"
cursorPositionEvent="onThreadTextBoxCursorChange"
maxHeight={200}
onChangeText={[Function]}
rootId="root_id"
valueEvent="onThreadTextBoxValueChange"
/>
</View>
</React.Fragment>
`;
@@ -96,6 +96,18 @@ exports[`thread should match snapshot, no root post, loading 1`] = `
style={Object {}}
/>
</Connect(SafeAreaIos)>
<View
nativeID="threadAccessoriesContainer"
>
<Connect(Autocomplete)
channelId="channel_id"
cursorPositionEvent="onThreadTextBoxCursorChange"
maxHeight={200}
onChangeText={[Function]}
rootId="root_id"
valueEvent="onThreadTextBoxValueChange"
/>
</View>
</React.Fragment>
`;
@@ -166,5 +178,17 @@ exports[`thread should match snapshot, render footer 3`] = `
style={Object {}}
/>
</Connect(SafeAreaIos)>
<View
nativeID="threadAccessoriesContainer"
>
<Connect(Autocomplete)
channelId="channel_id"
cursorPositionEvent="onThreadTextBoxCursorChange"
maxHeight={200}
onChangeText={[Function]}
rootId="root_id"
valueEvent="onThreadTextBoxValueChange"
/>
</View>
</React.Fragment>
`;

View File

@@ -56,16 +56,6 @@ export default class ThreadIOS extends ThreadBase {
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
/>
</Animated.View>
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
<Autocomplete
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={THREAD_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={THREAD_POST_TEXTBOX_VALUE_CHANGE}
rootId={rootId}
channelId={channelId}
/>
</View>
</>
);
@@ -101,6 +91,16 @@ export default class ThreadIOS extends ThreadBase {
{content}
</SafeAreaView>
{postDraft}
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
<Autocomplete
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={THREAD_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={THREAD_POST_TEXTBOX_VALUE_CHANGE}
rootId={rootId}
channelId={channelId}
/>
</View>
</React.Fragment>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B