[MM-18362] Sidebar improvements (#3223)

* sidebar lists displays past bottom safe area insets

* Sidebar animation speed

* Sidebar improvements

* Add status icons and avatars to mattermost font

* Bot icon the same size as other channel icons

* improvements to the channel badge

* Badge color and border

* More precision when showing more unread above

* Improve GM icon

* Fix badge on team sidebar

* Align channel sidebar badges

* alignments

* Fix tests

* Remove unnecessary isLandscape in main sidebar connector
This commit is contained in:
Elias Nahum 2019-09-16 13:17:48 -03:00 committed by GitHub
parent d28aaf21a8
commit 19e6b2a00e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 612 additions and 495 deletions

View file

@ -5,63 +5,75 @@ exports[`Badge should match snapshot 1`] = `
style={
Array [
Object {
"backgroundColor": "#444",
"borderRadius": 20,
"height": 20,
"padding": 12,
"paddingBottom": 3,
"paddingTop": 3,
"position": "absolute",
"right": 30,
"top": 2,
},
Object {
"backgroundColor": "#ffffff",
},
Object {
"opacity": 0,
"borderColor": "#000000",
},
]
}
>
<View
style={
Object {
"alignItems": "center",
"alignSelf": "center",
"flex": 1,
"justifyContent": "center",
}
Array [
Object {
"backgroundColor": "#444",
"height": 20,
"padding": 12,
"paddingBottom": 3,
"paddingTop": 3,
},
Object {
"backgroundColor": "#ffffff",
},
Object {
"opacity": 0,
},
]
}
>
<View
style={
Object {
"alignItems": "center",
"alignSelf": "center",
"flex": 1,
"flexDirection": "row",
"justifyContent": "center",
"textAlignVertical": "center",
}
}
>
<Text
onLayout={[Function]}
<View
style={
Array [
Object {
"color": "white",
"fontSize": 14,
},
Object {
"color": "#145dbf",
"fontSize": 10,
},
]
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"justifyContent": "center",
"textAlignVertical": "center",
}
}
>
1
</Text>
<Text
onLayout={[Function]}
style={
Array [
Object {
"color": "white",
"fontSize": 14,
},
Object {
"color": "#145dbf",
"fontSize": 10,
},
]
}
>
99+
</Text>
</View>
</View>
</View>
</View>

View file

@ -20,6 +20,7 @@ export default class Badge extends PureComponent {
};
static propTypes = {
containerStyle: ViewPropTypes.style,
count: PropTypes.number.isRequired,
extraPaddingHorizontal: PropTypes.number,
style: ViewPropTypes.style,
@ -66,9 +67,13 @@ export default class Badge extends PureComponent {
}
};
setBadgeRef = (ref) => {
this.badgeContainerRef = ref;
};
setNativeProps = (props) => {
if (this.mounted && this.refs.badgeContainer) {
this.refs.badgeContainer.setNativeProps(props);
if (this.mounted && this.badgeContainerRef) {
this.badgeContainerRef.setNativeProps(props);
}
};
@ -95,7 +100,7 @@ export default class Badge extends PureComponent {
};
renderText = () => {
const {count} = this.props;
const {containerStyle, count, style} = this.props;
let unreadCount = null;
let unreadIndicator = null;
if (count < 0) {
@ -110,25 +115,33 @@ export default class Badge extends PureComponent {
</View>
);
} else {
let mentionCount = count;
if (count > 99) {
mentionCount = '99+';
}
unreadCount = (
<View style={styles.verticalAlign}>
<Text
style={[styles.text, this.props.countStyle]}
onLayout={this.onLayout}
>
{count.toString()}
{mentionCount.toString()}
</Text>
</View>
);
}
return (
<View
ref='badgeContainer'
style={[styles.badge, this.props.style, {opacity: 0}]}
>
<View style={styles.wrapper}>
{unreadCount}
{unreadIndicator}
<View style={[styles.badgeContainer, containerStyle]}>
<View
ref={this.setBadgeRef}
style={[styles.badge, style, {opacity: 0}]}
>
<View style={styles.wrapper}>
{unreadCount}
{unreadIndicator}
</View>
</View>
</View>
);
@ -153,11 +166,13 @@ export default class Badge extends PureComponent {
const styles = StyleSheet.create({
badge: {
backgroundColor: '#444',
borderRadius: 20,
height: 20,
padding: 12,
paddingTop: 3,
paddingBottom: 3,
},
badgeContainer: {
borderRadius: 20,
position: 'absolute',
right: 30,
top: 2,
@ -173,10 +188,10 @@ const styles = StyleSheet.create({
color: 'white',
},
unreadIndicator: {
height: 4,
width: 4,
height: 5,
width: 5,
backgroundColor: '#444',
borderRadius: 4,
borderRadius: 5,
},
verticalAlign: {
flex: 1,

View file

@ -9,9 +9,10 @@ import Badge from './badge';
describe('Badge', () => {
const baseProps = {
count: 1,
count: 100,
countStyle: {color: '#145dbf', fontSize: 10},
style: {backgroundColor: '#ffffff'},
containerStyle: {borderColor: '#000000'},
onPress: jest.fn(),
};
@ -22,6 +23,6 @@ describe('Badge', () => {
expect(wrapper.instance().renderText()).toMatchSnapshot();
expect(wrapper.find(TouchableWithoutFeedback).exists()).toEqual(true);
expect(wrapper.find(Text).first().props().children).toContain('1');
expect(wrapper.find(Text).first().props().children).toContain('99+');
});
});

View file

@ -41,8 +41,6 @@ const createStyleSheet = makeStyleSheetFromTheme((theme) => {
alignSelf: 'center',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.15),
borderRadius: 2,
color: theme.centerChannelColor,
fontWeight: '600',
marginRight: 2,
marginBottom: 1,
...Platform.select({
@ -55,7 +53,9 @@ const createStyleSheet = makeStyleSheetFromTheme((theme) => {
paddingHorizontal: 4,
},
botText: {
color: theme.centerChannelColor,
fontSize: 10,
fontWeight: '600',
},
};
});

View file

@ -4,7 +4,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Image,
Text,
View,
} from 'react-native';
@ -86,45 +85,45 @@ export default class ChannelIcon extends React.PureComponent {
<Icon
name='archive'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
type='fontawesome'
type='mattermost'
/>
);
} else if (isBot) {
icon = (
<Icon
name='robot'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}, style.iconBot]}
type='fontawesome5'
name='bot'
style={[style.icon, unreadIcon, activeIcon, {fontSize: (size - 1), left: -1.5, top: -1}]}
type='mattermost'
/>
);
} else if (hasDraft) {
icon = (
<Icon
name='pencil'
name='draft'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
type='fontawesome'
type='mattermost'
/>
);
} else if (type === General.OPEN_CHANNEL) {
icon = (
<Icon
name='globe'
name='public'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
type='fontawesome'
type='mattermost'
/>
);
} else if (type === General.PRIVATE_CHANNEL) {
icon = (
<Icon
name='lock'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
type='fontawesome'
name='private'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, left: 0.5}]}
type='mattermost'
/>
);
} else if (type === General.GM_CHANNEL) {
icon = (
<View style={[style.groupBox, unreadGroupBox, activeGroupBox, {width: size, height: size}]}>
<Text style={[style.group, unreadGroup, activeGroup, {fontSize: (size - 6)}]}>
<View style={[style.groupBox, unreadGroupBox, activeGroupBox, {width: size + 1, height: size + 1}]}>
<Text style={[style.group, unreadGroup, activeGroup, {fontSize: (size - 4)}]}>
{membersCount}
</Text>
</View>
@ -133,33 +132,37 @@ export default class ChannelIcon extends React.PureComponent {
switch (status) {
case General.AWAY:
icon = (
<Image
source={require('assets/images/status/away_avatar.png')}
style={{width: size, height: size, tintColor: theme.awayIndicator}}
<Icon
name='away-avatar'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, color: theme.awayIndicator}]}
type='mattermost'
/>
);
break;
case General.DND:
icon = (
<Image
source={require('assets/images/status/dnd_avatar.png')}
style={{width: size, height: size, tintColor: theme.dndIndicator}}
<Icon
name='dnd-avatar'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, color: theme.dndIndicator}]}
type='mattermost'
/>
);
break;
case General.ONLINE:
icon = (
<Image
source={require('assets/images/status/online_avatar.png')}
style={{width: size, height: size, tintColor: theme.onlineIndicator}}
<Icon
name='online-avatar'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, color: theme.onlineIndicator}]}
type='mattermost'
/>
);
break;
default:
icon = (
<Image
source={require('assets/images/status/offline_avatar.png')}
style={{width: size, height: size, tintColor: offlineColor}}
<Icon
name='offline-avatar'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, color: offlineColor}]}
type='mattermost'
/>
);
break;
@ -167,7 +170,7 @@ export default class ChannelIcon extends React.PureComponent {
}
return (
<View style={[style.container, {width: size, height: size}]}>
<View style={[style.container, {height: size}]}>
{icon}
</View>
);
@ -192,27 +195,26 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
iconInfo: {
color: theme.centerChannelColor,
},
iconBot: {
marginLeft: -5,
},
groupBox: {
alignSelf: 'flex-start',
alignItems: 'center',
backgroundColor: changeOpacity(theme.sidebarText, 0.3),
borderColor: changeOpacity(theme.sidebarText, 0.3),
borderWidth: 1,
borderColor: changeOpacity(theme.sidebarText, 0.4),
borderRadius: 2,
justifyContent: 'center',
},
groupBoxActive: {
borderColor: theme.sidebarTextActiveColor,
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.3),
},
groupBoxUnread: {
borderColor: theme.sidebarUnreadText,
backgroundColor: changeOpacity(theme.sidebarUnreadText, 0.3),
},
groupBoxInfo: {
borderColor: theme.centerChannelColor,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.3),
},
group: {
color: changeOpacity(theme.sidebarText, 0.4),
color: changeOpacity(theme.sidebarText, 0.6),
fontSize: 10,
fontWeight: '600',
},

View file

@ -10,8 +10,10 @@ export const FADE_DURATION = 100;
export default class Fade extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
duration: PropTypes.number,
style: PropTypes.object,
visible: PropTypes.bool.isRequired,
disableScale: PropTypes.bool,
};
constructor(props) {
@ -27,7 +29,7 @@ export default class Fade extends PureComponent {
this.state.fadeAnim,
{
toValue: prevProps.visible ? 0 : 1,
duration: FADE_DURATION,
duration: this.props.duration || FADE_DURATION,
useNativeDriver: true,
}
).start();
@ -35,6 +37,7 @@ export default class Fade extends PureComponent {
}
render() {
const {disableScale} = this.props;
const {fadeAnim} = this.state;
return (
@ -42,7 +45,7 @@ export default class Fade extends PureComponent {
style={{
...this.props.style,
opacity: fadeAnim,
transform: [{scale: fadeAnim}],
transform: disableScale ? [] : [{scale: fadeAnim}],
}}
>
{this.props.children}

View file

@ -8,6 +8,7 @@ import React, { Component } from 'react';
import {
Animated,
Dimensions,
Easing,
Keyboard,
PanResponder,
StyleSheet,
@ -183,7 +184,7 @@ export default class DrawerLayout extends Component {
/* Overlay styles */
const overlayOpacity = this.openValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 0.7],
outputRange: [0, 0.5],
extrapolate: 'clamp',
});
const animatedOverlayStyles = { opacity: overlayOpacity };
@ -274,10 +275,10 @@ export default class DrawerLayout extends Component {
openDrawer = (options: DrawerMovementOptionType = {}) => {
if (!this.props.isTablet) {
this._emitStateChanged(SETTLING);
Animated.spring(this.openValue, {
Animated.timing(this.openValue, {
toValue: 1,
bounciness: 0,
restSpeedThreshold: 0.1,
duration: 150,
easing: Easing.out(Easing.cubic),
useNativeDriver: this.props.useNativeAnimations,
...options,
}).start(() => {
@ -295,10 +296,10 @@ export default class DrawerLayout extends Component {
closeDrawer = (options: DrawerMovementOptionType = {}) => {
this._emitStateChanged(SETTLING);
Animated.spring(this.openValue, {
Animated.timing(this.openValue, {
toValue: 0,
bounciness: 0,
restSpeedThreshold: 1,
duration: 150,
easing: Easing.out(Easing.cubic),
useNativeDriver: this.props.useNativeAnimations,
...options,
}).start(() => {

View file

@ -82,14 +82,16 @@ exports[`ChannelItem should match snapshot 1`] = `
Array [
Object {
"alignSelf": "center",
"color": "rgba(255,255,255,0.4)",
"color": "rgba(255,255,255,0.88)",
"flex": 1,
"fontSize": 14,
"fontWeight": "600",
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"paddingRight": 10,
},
Object {
"color": "#ffffff",
"fontWeight": "500",
},
]
}
@ -195,10 +197,11 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
Array [
Object {
"alignSelf": "center",
"color": "rgba(255,255,255,0.4)",
"color": "rgba(255,255,255,0.88)",
"flex": 1,
"fontSize": 14,
"fontWeight": "600",
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"paddingRight": 10,
},
Object {
@ -308,10 +311,11 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
Array [
Object {
"alignSelf": "center",
"color": "rgba(255,255,255,0.4)",
"color": "rgba(255,255,255,0.88)",
"flex": 1,
"fontSize": 14,
"fontWeight": "600",
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"paddingRight": 10,
},
Object {
@ -421,10 +425,11 @@ exports[`ChannelItem should match snapshot for deactivated user and is currentCh
Array [
Object {
"alignSelf": "center",
"color": "rgba(255,255,255,0.4)",
"color": "rgba(255,255,255,0.88)",
"flex": 1,
"fontSize": 14,
"fontWeight": "600",
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"paddingRight": 10,
},
Object {
@ -523,14 +528,16 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes
Array [
Object {
"alignSelf": "center",
"color": "rgba(255,255,255,0.4)",
"color": "rgba(255,255,255,0.88)",
"flex": 1,
"fontSize": 14,
"fontWeight": "600",
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"paddingRight": 10,
},
Object {
"color": "#ffffff",
"fontWeight": "500",
},
]
}
@ -631,14 +638,16 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
Array [
Object {
"alignSelf": "center",
"color": "rgba(255,255,255,0.4)",
"color": "rgba(255,255,255,0.88)",
"flex": 1,
"fontSize": 14,
"fontWeight": "600",
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"paddingRight": 10,
},
Object {
"color": "#ffffff",
"fontWeight": "500",
},
]
}
@ -735,14 +744,16 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
Array [
Object {
"alignSelf": "center",
"color": "rgba(255,255,255,0.4)",
"color": "rgba(255,255,255,0.88)",
"flex": 1,
"fontSize": 14,
"fontWeight": "600",
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"paddingRight": 10,
},
Object {
"color": "#ffffff",
"fontWeight": "500",
},
]
}
@ -750,26 +761,33 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
display_name
</Text>
<Badge
containerStyle={
Object {
"borderColor": "#1153ab",
"borderRadius": 14,
"borderWidth": 0,
"marginRight": 16,
"right": 0,
"top": 11,
}
}
count={1}
countStyle={
Object {
"color": "#145dbf",
"fontSize": 10,
"fontSize": 12,
}
}
extraPaddingHorizontal={10}
minHeight={20}
minWidth={20}
minWidth={21}
onPress={[Function]}
style={
Object {
"backgroundColor": undefined,
"borderColor": "#1153ab",
"borderRadius": 10,
"borderWidth": 1,
"height": 21,
"padding": 3,
"position": "relative",
"right": 16,
}
}
/>

View file

@ -160,10 +160,12 @@ export default class ChannelItem extends PureComponent {
if (mentions) {
badge = (
<Badge
containerStyle={style.badgeContainer}
style={style.badge}
countStyle={style.mention}
count={mentions}
onPress={this.onPress}
minWidth={21}
/>
);
}
@ -238,31 +240,38 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
paddingLeft: 11,
},
text: {
color: changeOpacity(theme.sidebarText, 0.4),
fontSize: 14,
fontWeight: '600',
color: changeOpacity(theme.sidebarText, 0.88),
fontSize: 16,
lineHeight: 24,
paddingRight: 10,
flex: 1,
alignSelf: 'center',
fontFamily: 'Open Sans',
},
textActive: {
color: theme.sidebarTextActiveColor,
},
textUnread: {
color: theme.sidebarUnreadText,
fontWeight: '500',
},
badge: {
backgroundColor: theme.mentionBg,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
padding: 3,
position: 'relative',
right: 16,
height: 21,
},
badgeContainer: {
borderColor: theme.sidebarHeaderBg,
borderRadius: 14,
borderWidth: 0,
right: 0,
top: 11,
marginRight: 16,
},
mention: {
color: theme.mentionColor,
fontSize: 10,
fontSize: 12,
},
muted: {
opacity: 0.5,

View file

@ -180,13 +180,34 @@ export default class ChannelsList extends PureComponent {
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
above: {
backgroundColor: theme.mentionBg,
top: 40,
},
action: {
color: changeOpacity(theme.sidebarText, 0.4),
fontSize: 26,
fontWeight: '100',
},
actionContainer: {
alignItems: 'center',
justifyContent: 'center',
marginRight: 16,
},
container: {
backgroundColor: theme.sidebarBg,
flex: 1,
},
header: {
color: theme.sidebarHeaderTextColor,
flex: 1,
fontSize: 17,
fontWeight: 'normal',
paddingLeft: 16,
},
headerContainer: {
alignItems: 'center',
paddingLeft: 10,
paddingLeft: 13,
backgroundColor: theme.sidebarBg,
flexDirection: 'row',
borderBottomWidth: 1,
@ -196,36 +217,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
height: ANDROID_TOP_PORTRAIT,
},
ios: {
height: 44,
height: 54,
},
}),
},
header: {
color: theme.sidebarHeaderTextColor,
flex: 1,
fontSize: 17,
fontWeight: 'normal',
paddingLeft: 16,
},
switchContainer: {
position: 'relative',
top: -1,
},
titleContainer: { // These aren't used by this component, but they are passed down to the list component
alignItems: 'center',
flex: 1,
flexDirection: 'row',
height: 48,
marginLeft: 16,
},
title: {
flex: 1,
color: theme.sidebarText,
opacity: 1,
fontSize: 15,
fontWeight: '400',
letterSpacing: 0.8,
lineHeight: 18,
hitSlop: {
bottom: 10,
left: 10,
right: 10,
top: 10,
},
searchContainer: {
flex: 1,
@ -239,26 +239,36 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
}),
},
divider: {
backgroundColor: changeOpacity(theme.sidebarText, 0.1),
separator: {
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.1),
height: 1,
width: '100%',
},
actionContainer: {
alignItems: 'center',
height: 48,
separatorContainer: {
flex: 1,
justifyContent: 'center',
width: 50,
marginHorizontal: 16,
},
action: {
switchContainer: {
position: 'relative',
top: -1,
},
title: {
color: theme.sidebarText,
fontSize: 20,
fontWeight: '500',
opacity: 0.4,
fontSize: 12,
fontWeight: '600',
letterSpacing: 0.2,
lineHeight: 18,
fontFamily: 'Open Sans',
},
above: {
backgroundColor: theme.mentionBg,
top: 9,
titleContainer: { // These aren't used by this component, but they are passed down to the list component
alignItems: 'center',
backgroundColor: theme.sidebarBg,
flex: 1,
flexDirection: 'row',
height: 40,
paddingLeft: 16,
},
};
});

View file

@ -5,17 +5,13 @@ import deepEqual from 'deep-equal';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
FlatList,
Keyboard,
Platform,
SectionList,
Text,
TouchableHighlight,
View,
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {changeOpacity} from 'app/utils/theme';
import {General} from 'mattermost-redux/constants';
import {sortChannelsByDisplayName} from 'mattermost-redux/utils/channel_utils';
@ -119,20 +115,6 @@ class FilteredList extends Component {
this.props.onSelectChannel(channel, currentChannel.id);
};
createChannelElement = (channel) => {
return (
<ChannelItem
channelId={channel.id}
channel={channel}
isSearchResult={true}
isUnread={channel.isUnread}
mentions={0}
onSelectChannel={this.onSelectChannel}
previewChannel={this.props.previewChannel}
/>
);
};
filterChannels = (channels, term) => {
if (!term) {
return channels;
@ -330,11 +312,10 @@ class FilteredList extends Component {
}
buildSectionsForSearch = (props, term) => {
const items = [];
const {searchOrder, styles} = props;
const sections = [];
const {searchOrder} = props;
const sectionBuilders = this.getSectionBuilders();
let previousDataLength = 0;
for (const section of searchOrder) {
if (sectionBuilders.hasOwnProperty(section)) {
const sectionBuilder = sectionBuilders[section];
@ -342,14 +323,12 @@ class FilteredList extends Component {
const data = builder(props, term);
if (data.length) {
const title = this.renderTitle(styles, id, defaultMessage, null, previousDataLength > 0, true);
items.push(title, ...data);
previousDataLength = data.length;
sections.push({id, defaultMessage, data});
}
}
}
return items;
return sections;
};
buildData = (props, term) => {
@ -360,74 +339,59 @@ class FilteredList extends Component {
return this.buildSectionsForSearch(props, term);
};
renderSectionAction = (styles, action) => {
const {theme} = this.props;
return (
<TouchableHighlight
style={styles.actionContainer}
onPress={action}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
>
<MaterialIcon
name='add'
style={styles.action}
/>
</TouchableHighlight>
);
};
keyExtractor = (item) => item.id || item;
renderDivider = (styles, marginLeft) => {
renderItem = ({item}) => {
return (
<View
style={[styles.divider, {marginLeft}]}
<ChannelItem
channelId={item.id}
channel={item}
isSearchResult={true}
isUnread={item.isUnread}
mentions={0}
onSelectChannel={this.onSelectChannel}
previewChannel={this.props.previewChannel}
/>
);
};
renderItem = ({item}) => {
if (!item.isTitle) {
return this.createChannelElement(item);
}
return item.title;
};
renderTitle = (styles, id, defaultMessage, action, topDivider, bottomDivider) => {
const {formatMessage} = this.props.intl;
return {
renderSectionHeader = ({section}) => {
const {intl, isLandscape, styles} = this.props;
const {
defaultMessage,
id,
isTitle: true,
title: (
<View>
{topDivider && this.renderDivider(styles, 0)}
<View style={styles.titleContainer}>
<Text style={[styles.title, padding(this.props.isLandscape)]}>
{formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
{action && this.renderSectionAction(styles, action)}
} = section;
return (
<React.Fragment>
<View style={[styles.titleContainer, padding(isLandscape)]}>
<Text style={styles.title}>
{intl.formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
<View style={styles.separatorContainer}>
<View style={styles.separator}/>
</View>
{bottomDivider && this.renderDivider(styles, 16)}
</View>
),
};
</React.Fragment>
);
};
render() {
const {styles} = this.props;
const {dataSource} = this.state;
return (
<View
style={styles.container}
>
<FlatList
data={dataSource}
<SectionList
sections={dataSource}
renderItem={this.renderItem}
keyExtractor={(item) => item.id}
onViewableItemsChanged={this.updateUnreadIndicators}
renderSectionHeader={this.renderSectionHeader}
keyExtractor={this.keyExtractor}
{...this.keyboardDismissProp}
keyboardShouldPersistTaps={'always'}
maxToRenderPerBatch={10}
stickySectionHeadersEnabled={true}
viewabilityConfig={VIEWABILITY_CONFIG}
/>
</View>

View file

@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import {
Dimensions,
findNodeHandle,
InteractionManager,
Keyboard,
Platform,
SectionList,
@ -16,18 +15,18 @@ import {
} from 'react-native';
import {intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FontAwesomePro from 'react-native-vector-icons/Ionicons';
import {General} from 'mattermost-redux/constants';
import {debounce} from 'mattermost-redux/actions/helpers';
import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item';
import {DeviceTypes, ListTypes, ViewTypes} from 'app/constants';
import {DeviceTypes, ListTypes} from 'app/constants';
import {SidebarSectionTypes} from 'app/constants/view';
import BottomSheet from 'app/utils/bottom_sheet';
import {t} from 'app/utils/i18n';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
import {paddingLeft as padding} from 'app/components/safe_area_view/iphone_x_spacing';
@ -81,6 +80,12 @@ export default class List extends PureComponent {
});
}
componentDidMount() {
if (!UnreadIndicator) {
UnreadIndicator = require('app/components/sidebars/main/channels_list/unread_indicator').default;
}
}
componentWillReceiveProps(nextProps) {
const {
canCreatePrivateChannels,
@ -162,12 +167,10 @@ export default class List extends PureComponent {
orderedChannelIds,
} = props;
return orderedChannelIds.map((s, i) => {
return orderedChannelIds.map((s) => {
return {
...this.getSectionConfigByType(props, s.type),
data: s.items,
topSeparator: i !== 0,
bottomSeparator: s.items.length > 0,
};
});
};
@ -292,15 +295,15 @@ export default class List extends PureComponent {
};
renderSectionAction = (styles, action, anchor) => {
const {theme} = this.props;
return (
<TouchableHighlight
style={styles.actionContainer}
onPress={action}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
underlayColor={'transparent'}
hitSlop={styles.hitSlop}
>
<MaterialIcon
name='add'
<FontAwesomePro
name='ios-add-circle-outline'
ref={anchor ? this.combinedActionsRef : null}
style={styles.action}
/>
@ -308,13 +311,6 @@ export default class List extends PureComponent {
);
};
renderSectionSeparator = () => {
const {styles} = this.props;
return (
<View style={[styles.divider]}/>
);
};
renderItem = ({item}) => {
const {favoriteChannelIds, unreadChannelIds, previewChannel} = this.props;
@ -334,25 +330,24 @@ export default class List extends PureComponent {
const {intl} = this.context;
const {
action,
bottomSeparator,
defaultMessage,
id,
topSeparator,
} = section;
const anchor = (id === 'sidebar.types.recent' || id === 'mobile.channel_list.channels');
return (
<View>
{topSeparator && this.renderSectionSeparator()}
<React.Fragment>
<View style={[styles.titleContainer, padding(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)}
</View>
{bottomSeparator && this.renderSectionSeparator()}
</View>
</React.Fragment>
);
};
@ -367,24 +362,19 @@ export default class List extends PureComponent {
};
emitUnreadIndicatorChange = debounce((showIndicator) => {
if (showIndicator && !UnreadIndicator) {
UnreadIndicator = require('app/components/sidebars/main/channels_list/unread_indicator').default;
}
this.setState({showIndicator});
}, 100);
}, 10);
updateUnreadIndicators = ({viewableItems}) => {
InteractionManager.runAfterInteractions(() => {
const {unreadChannelIds} = this.props;
const firstUnread = unreadChannelIds.length && unreadChannelIds[0];
if (firstUnread && viewableItems.length) {
const isVisible = viewableItems.find((v) => v.item === firstUnread);
const {unreadChannelIds} = this.props;
const firstUnread = unreadChannelIds.length && unreadChannelIds[0];
if (firstUnread && viewableItems.length) {
const isVisible = viewableItems.find((v) => v.item === firstUnread);
return this.emitUnreadIndicatorChange(!isVisible);
}
return this.emitUnreadIndicatorChange(!isVisible);
}
return this.emitUnreadIndicatorChange(false);
});
return this.emitUnreadIndicatorChange(false);
};
scrollBeginDrag = () => {
@ -408,15 +398,11 @@ export default class List extends PureComponent {
};
render() {
const {styles, theme, isLandscape} = this.props;
const {sections, width, showIndicator} = this.state;
const {styles, theme} = this.props;
const {sections, showIndicator} = this.state;
const paddingBottom = this.listContentPadding();
const unreadBarStyles = DeviceTypes.IS_IPHONE_X && isLandscape ? {width: width - 32, marginLeft: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + 16} : {width};
const unreadTextStyles = DeviceTypes.IS_IPHONE_X && isLandscape ? {marginLeft: -44} : null;
return (
<View
style={styles.container}
@ -428,19 +414,18 @@ export default class List extends PureComponent {
contentContainerStyle={{paddingBottom}}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
keyboardShouldPersistTaps={'always'}
keyExtractor={this.keyExtractor}
onViewableItemsChanged={this.updateUnreadIndicators}
maxToRenderPerBatch={10}
stickySectionHeadersEnabled={false}
stickySectionHeadersEnabled={true}
viewabilityConfig={VIEWABILITY_CONFIG}
keyboardShouldPersistTaps={'always'}
{...this.keyboardDismissProp}
/>
{showIndicator &&
{UnreadIndicator &&
<UnreadIndicator
show={showIndicator}
style={[styles.above, unreadBarStyles]}
textStyle={unreadTextStyles}
style={styles.above}
onPress={this.scrollToTop}
theme={theme}
/>

View file

@ -4,15 +4,15 @@
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeam, getMyTeamsCount, getChannelDrawerBadgeCount} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTeamId, getMyTeamsCount, getChannelDrawerBadgeCount} from 'mattermost-redux/selectors/entities/teams';
import SwitchTeamsButton from './switch_teams_button';
function mapStateToProps(state) {
const team = getCurrentTeam(state) || {};
const currentTeamId = getCurrentTeamId(state);
return {
currentTeamId: team.id,
currentTeamId,
mentionCount: getChannelDrawerBadgeCount(state),
teamsCount: getMyTeamsCount(state),
theme: getTheme(state),

View file

@ -48,8 +48,10 @@ export default class SwitchTeamsButton extends React.PureComponent {
const badge = (
<Badge
containerStyle={styles.badgeContainer}
style={styles.badge}
countStyle={styles.mention}
minWidth={20}
count={mentionCount}
/>
);
@ -66,11 +68,11 @@ export default class SwitchTeamsButton extends React.PureComponent {
size={12}
style={styles.switcherArrow}
/>
<View style={styles.switcherDivider}/>
<TeamIcon
teamId={currentTeamId}
styleContainer={styles.teamIconContainer}
styleText={styles.teamIconText}
styleImage={styles.teamIcon}
/>
</View>
</TouchableHighlight>
@ -82,49 +84,51 @@ export default class SwitchTeamsButton extends React.PureComponent {
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
switcherContainer: {
backgroundColor: theme.sidebarHeaderTextColor,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
height: 32,
borderRadius: 2,
marginLeft: 6,
marginRight: 6,
paddingHorizontal: 3,
},
switcherArrow: {
color: theme.sidebarHeaderBg,
marginRight: 3,
},
switcherDivider: {
backgroundColor: theme.sidebarHeaderBg,
height: 15,
marginHorizontal: 6,
width: 1,
},
teamIconContainer: {
width: 26,
height: 26,
marginLeft: 3,
},
teamIconText: {
fontSize: 14,
},
badge: {
backgroundColor: theme.mentionBg,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
flexDirection: 'row',
height: 20,
padding: 3,
},
badgeContainer: {
borderColor: theme.sidebarBg,
borderRadius: 14,
borderWidth: 2,
position: 'absolute',
left: -5,
top: -5,
right: 0,
top: -9,
},
mention: {
color: theme.mentionColor,
fontSize: 10,
},
switcherArrow: {
color: theme.sidebarHeaderBg,
marginRight: 8,
top: 1,
},
switcherContainer: {
alignItems: 'center',
backgroundColor: theme.sidebarHeaderTextColor,
borderRadius: 2,
flexDirection: 'row',
justifyContent: 'center',
height: 32,
marginRight: 12,
width: 56,
},
teamIcon: {
width: 24,
height: 24,
},
teamIconContainer: {
alignItems: 'center',
justifyContent: 'center',
height: 24,
width: 24,
},
teamIconText: {
fontFamily: 'Open Sans',
fontSize: 14,
},
};
});

View file

@ -10,6 +10,7 @@ import {
} from 'react-native';
import IonIcon from 'react-native-vector-icons/Ionicons';
import Fade from 'app/components/fade';
import FormattedText from 'app/components/formatted_text';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
@ -28,45 +29,47 @@ export default class UnreadIndicator extends PureComponent {
render() {
const {onPress, show, theme} = this.props;
if (!show) {
return null;
}
const style = getStyleSheet(theme);
return (
<TouchableWithoutFeedback onPress={onPress}>
<View
style={[style.container, this.props.style]}
>
<FormattedText
style={[style.indicatorText, this.props.textStyle]}
id='sidebar.unreads'
defaultMessage='More unreads'
/>
<IonIcon
size={14}
name='md-arrow-round-up'
color={theme.mentionColor}
style={style.arrow}
/>
</View>
</TouchableWithoutFeedback>
<Fade
visible={show}
style={style.container}
duration={150}
disableScale={true}
>
<TouchableWithoutFeedback onPress={onPress}>
<View style={[style.wrapper, this.props.style]}>
<FormattedText
style={[style.indicatorText, this.props.textStyle]}
id='sidebar.unreads'
defaultMessage='More unreads'
/>
<IonIcon
size={14}
name='md-arrow-round-up'
color={theme.mentionColor}
style={style.arrow}
/>
</View>
</TouchableWithoutFeedback>
</Fade>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
arrow: {
position: 'relative',
bottom: -1,
},
container: {
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
position: 'absolute',
borderRadius: 15,
marginHorizontal: 25,
height: 25,
right: 0,
left: 0,
},
indicatorText: {
backgroundColor: 'transparent',
@ -77,9 +80,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
textAlign: 'center',
textAlignVertical: 'center',
},
arrow: {
position: 'relative',
bottom: -1,
wrapper: {
borderRadius: 15,
height: 25,
flexDirection: 'row',
paddingLeft: 10,
paddingRight: 12,
justifyContent: 'center',
alignItems: 'center',
},
};
});

View file

@ -11,11 +11,12 @@ import Swiper from 'app/components/swiper';
export default class DrawerSwiper extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
drawerOpened: PropTypes.bool,
drawerWidth: PropTypes.number.isRequired,
hasSafeAreaInsets: PropTypes.bool,
onPageSelected: PropTypes.func,
showTeams: PropTypes.bool.isRequired,
theme: PropTypes.object.isRequired,
drawerOpened: PropTypes.bool,
};
static defaultProps = {
@ -78,8 +79,9 @@ export default class DrawerSwiper extends Component {
onIndexChanged={this.swiperPageSelected}
width={drawerWidth}
style={{backgroundColor: theme.sidebarBg}}
paginationBackgroundColor={changeOpacity(theme.sidebarText, 0.1)}
activeDotColor={theme.sidebarText}
dotColor={changeOpacity(theme.sidebarText, 0.5)}
dotColor={changeOpacity(theme.sidebarText, 0.3)}
scrollEnabled={showTeams}
showsPagination={showTeams}
keyboardShouldPersistTaps={'always'}

View file

@ -11,7 +11,7 @@ import {getCurrentTeamId, getMyTeamsCount} from 'mattermost-redux/selectors/enti
import {setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {isLandscape, getDimensions} from 'app/selectors/device';
import {getDimensions} from 'app/selectors/device';
import telemetry from 'app/telemetry';
import MainSidebar from './main_sidebar.js';
@ -41,7 +41,6 @@ function mapStateToProps(state) {
...getDimensions(state),
currentTeamId: getCurrentTeamId(state),
currentUserId,
isLandscape: isLandscape(state),
teamsCount: getMyTeamsCount(state),
theme: getTheme(state),
};

View file

@ -30,7 +30,6 @@ import TeamsList from './teams_list';
import telemetry from 'app/telemetry';
const DRAWER_INITIAL_OFFSET = 40;
const DRAWER_LANDSCAPE_OFFSET = 150;
export default class ChannelSidebar extends Component {
static propTypes = {
@ -46,7 +45,6 @@ export default class ChannelSidebar extends Component {
currentTeamId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
deviceWidth: PropTypes.number.isRequired,
isLandscape: PropTypes.bool.isRequired,
teamsCount: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired,
previewChannel: PropTypes.func,
@ -59,17 +57,12 @@ export default class ChannelSidebar extends Component {
constructor(props) {
super(props);
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (props.isLandscape || DeviceTypes.IS_TABLET) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.swiperIndex = 1;
this.drawerRef = React.createRef();
this.channelListRef = React.createRef();
this.state = {
show: false,
openDrawerOffset,
openDrawerOffset: DRAWER_INITIAL_OFFSET,
drawerOpened: false,
searching: false,
isSplitView: false,
@ -79,7 +72,7 @@ export default class ChannelSidebar extends Component {
componentDidMount() {
this.mounted = true;
this.props.actions.getTeams();
this.handleDimensions();
this.handleDimensions({window: Dimensions.get('window')});
this.handlePermanentSidebar();
EventEmitter.on('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.on('renderDrawer', this.handleShowDrawerContent);
@ -89,23 +82,8 @@ export default class ChannelSidebar extends Component {
Dimensions.addEventListener('change', this.handleDimensions);
}
componentWillReceiveProps(nextProps) {
const {isLandscape} = this.props;
if (nextProps.isLandscape !== isLandscape) {
if (this.state.openDrawerOffset !== 0) {
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (nextProps.isLandscape || DeviceTypes.IS_TABLET) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.setState({openDrawerOffset});
}
}
}
shouldComponentUpdate(nextProps, nextState) {
const {currentTeamId, deviceWidth, isLandscape, teamsCount, theme} = this.props;
const {currentTeamId, deviceWidth, teamsCount, theme} = this.props;
const {openDrawerOffset, isSplitView, permanentSidebar, show, searching} = this.state;
if (nextState.openDrawerOffset !== openDrawerOffset || nextState.show !== show || nextState.searching !== searching) {
@ -113,7 +91,7 @@ export default class ChannelSidebar extends Component {
}
return nextProps.currentTeamId !== currentTeamId ||
nextProps.isLandscape !== isLandscape || nextProps.deviceWidth !== deviceWidth ||
nextProps.deviceWidth !== deviceWidth ||
nextProps.teamsCount !== teamsCount ||
nextProps.theme !== theme ||
nextState.isSplitView !== isSplitView ||
@ -139,12 +117,23 @@ export default class ChannelSidebar extends Component {
return false;
};
handleDimensions = () => {
if (DeviceTypes.IS_TABLET && this.mounted) {
mattermostManaged.isRunningInSplitView().then((result) => {
const isSplitView = Boolean(result.isSplitView);
this.setState({isSplitView});
});
handleDimensions = ({window}) => {
if (this.mounted) {
if (DeviceTypes.IS_TABLET) {
mattermostManaged.isRunningInSplitView().then((result) => {
const isSplitView = Boolean(result.isSplitView);
this.setState({isSplitView});
});
}
if (this.state.openDrawerOffset !== 0) {
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if ((window.width > window.height) || DeviceTypes.IS_TABLET) {
openDrawerOffset = window.width * 0.5;
}
this.setState({openDrawerOffset});
}
}
};
@ -343,6 +332,7 @@ export default class ChannelSidebar extends Component {
return null;
}
const hasSafeAreaInsets = DeviceTypes.IS_IPHONE_X || mattermostManaged.hasSafeAreaInsets;
const multipleTeams = teamsCount > 1;
const showTeams = !searching && multipleTeams;
if (this.drawerSwiper) {
@ -389,6 +379,7 @@ export default class ChannelSidebar extends Component {
return (
<SafeAreaView
excludeFooter={true}
navBarBackgroundColor={theme.sidebarBg}
backgroundColor={theme.sidebarHeaderBg}
footerColor={theme.sidebarBg}
@ -399,6 +390,7 @@ export default class ChannelSidebar extends Component {
showTeams={showTeams}
drawerOpened={this.state.drawerOpened}
drawerWidth={drawerWidth}
hasSafeAreaInsets={hasSafeAreaInsets}
>
{lists}
</DrawerSwiper>

View file

@ -46,6 +46,16 @@ export default class TeamsListItem extends React.PureComponent {
} = this.props;
const styles = getStyleSheet(theme);
const badge = (
<Badge
containerStyle={styles.badgeContainer}
countStyle={styles.mention}
count={mentionCount}
minWidth={20}
style={styles.badge}
/>
);
let current;
if (teamId === currentTeamId) {
current = (
@ -58,14 +68,6 @@ export default class TeamsListItem extends React.PureComponent {
);
}
const badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={mentionCount}
/>
);
return (
<View style={styles.teamWrapper}>
<TouchableHighlight
@ -73,11 +75,14 @@ export default class TeamsListItem extends React.PureComponent {
onPress={this.selectTeam}
>
<View style={[styles.teamContainer, padding(isLandscape)]}>
<TeamIcon
teamId={teamId}
styleContainer={styles.teamIconContainer}
styleText={styles.teamIconText}
/>
<View>
<TeamIcon
teamId={teamId}
styleContainer={styles.teamIconContainer}
styleText={styles.teamIconText}
/>
{badge}
</View>
<View style={styles.teamNameContainer}>
<Text
numberOfLines={1}
@ -97,7 +102,6 @@ export default class TeamsListItem extends React.PureComponent {
{current}
</View>
</TouchableHighlight>
{badge}
</View>
);
}
@ -145,14 +149,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
badge: {
backgroundColor: theme.mentionBg,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
flexDirection: 'row',
height: 20,
padding: 3,
},
badgeContainer: {
borderColor: theme.sidebarBg,
borderRadius: 14,
borderWidth: 2,
position: 'absolute',
left: 45,
top: -2,
right: -12,
top: -10,
},
mention: {
color: theme.mentionColor,

View file

@ -20,6 +20,7 @@ export default class Swiper extends PureComponent {
activeDotColor: PropTypes.string,
children: PropTypes.node.isRequired,
dotColor: PropTypes.string,
paginationBackgroundColor: PropTypes.string,
initialPage: PropTypes.number,
keyboardShouldPersistTaps: PropTypes.string,
onIndexChanged: PropTypes.func,
@ -138,7 +139,7 @@ export default class Swiper extends PureComponent {
};
renderPagination = () => {
if (this.state.total <= 1 || !this.props.showsPagination) {
if (this.state.total <= 1 || !this.props.showsPagination || Platform.OS === 'android') {
return null;
}
@ -171,10 +172,10 @@ export default class Swiper extends PureComponent {
const bottom = this.paginationBottom(width, height);
const drawerWidth = (width > height) ? width - ViewTypes.IOS_HORIZONTAL_LANDSCAPE : width;
let style;
if (DeviceTypes.IS_IPHONE_X) {
if (DeviceTypes.IS_IPHONE_X && (width < height)) {
style = {
bottom,
drawerWidth,
width: drawerWidth,
};
} else {
style = {
@ -188,7 +189,9 @@ export default class Swiper extends PureComponent {
pointerEvents='none'
style={[styles.pagination, style]}
>
{dots}
<View style={[styles.paginationWrapper, {backgroundColor: this.props.paginationBackgroundColor}]}>
{dots}
</View>
</View>
);
};
@ -217,7 +220,7 @@ export default class Swiper extends PureComponent {
paginationBottom = (width, height) => {
if (DeviceTypes.IS_TABLET) {
if (Platform.OS === 'ios' && mattermostManaged.hasSafeAreaInsets) {
return 14;
return 34;
}
return 24;
@ -225,7 +228,7 @@ export default class Swiper extends PureComponent {
const landscape = width > height;
if (DeviceTypes.IS_IPHONE_X) {
return landscape ? 14 : 4;
return landscape ? 14 : 34;
}
return 24;
@ -278,9 +281,17 @@ const styles = StyleSheet.create({
position: 'absolute',
left: 0,
right: 0,
justifyContent: 'center',
alignItems: 'center',
width: '100%',
},
paginationWrapper: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
height: 24,
borderRadius: 15,
width: 44,
},
dotStyle: {
width: 8,

View file

@ -9,8 +9,12 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import TeamIcon from './team_icon';
function mapStateToProps(state, ownProps) {
const team = getTeam(state, ownProps.teamId);
const lastIconUpdate = team.last_team_icon_update;
return {
team: getTeam(state, ownProps.teamId),
displayName: team.display_name,
lastIconUpdate,
theme: getTheme(state),
};
}

View file

@ -17,20 +17,21 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class TeamIcon extends React.PureComponent {
static propTypes = {
displayName: PropTypes.string,
lastIconUpdate: PropTypes.number,
teamId: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
styleContainer: PropTypes.any,
styleText: PropTypes.any,
styleImage: PropTypes.any,
team: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
const {team} = props;
if (team.last_team_icon_update) {
ImageCacheManager.cache('', Client4.getTeamIconUrl(team.id, team.last_team_icon_update), this.setImageURL);
const {lastIconUpdate, teamId} = props;
if (lastIconUpdate) {
ImageCacheManager.cache('', Client4.getTeamIconUrl(teamId, lastIconUpdate), this.setImageURL);
}
this.state = {
@ -40,12 +41,18 @@ export default class TeamIcon extends React.PureComponent {
}
componentWillReceiveProps(nextProps) {
this.setState({imageError: false});
const newState = {imageError: false};
if (this.props.team.last_team_icon_update !== nextProps.team.last_team_icon_update) {
const {team} = nextProps;
ImageCacheManager.cache('', Client4.getTeamIconUrl(team.id, team.last_team_icon_update), this.setImageURL);
if (this.props.teamId !== nextProps.teamId) {
newState.teamIcon = null;
}
if (nextProps.lastIconUpdate && this.props.lastIconUpdate !== nextProps.lastIconUpdate) {
const {lastIconUpdate, teamId} = nextProps;
ImageCacheManager.cache('', Client4.getTeamIconUrl(teamId, lastIconUpdate), this.setImageURL);
}
this.setState(newState);
}
setImageURL = (teamIcon) => {
@ -54,7 +61,7 @@ export default class TeamIcon extends React.PureComponent {
render() {
const {
team,
displayName,
theme,
styleContainer,
styleText,
@ -75,7 +82,7 @@ export default class TeamIcon extends React.PureComponent {
} else {
teamIconContent = (
<Text style={[styles.text, styleText]}>
{team.display_name.substr(0, 2).toUpperCase()}
{displayName.substr(0, 2).toUpperCase()}
</Text>
);
}

View file

@ -1,69 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UserStatus should match snapshot, away status 1`] = `
<Image
source={
Object {
"testUri": "../../../dist/assets/images/status/away.png",
}
}
<VectorIcon
name="away"
size={14}
style={
Object {
"height": 32,
"tintColor": "#ffbc42",
"width": 32,
"color": "#ffbc42",
"fontSize": 32,
}
}
type="mattermost"
/>
`;
exports[`UserStatus should match snapshot, dnd status 1`] = `
<Image
source={
Object {
"testUri": "../../../dist/assets/images/status/dnd.png",
}
}
<VectorIcon
name="dnd"
size={14}
style={
Object {
"height": 32,
"tintColor": "#f74343",
"width": 32,
"color": "#f74343",
"fontSize": 32,
}
}
type="mattermost"
/>
`;
exports[`UserStatus should match snapshot, online status 1`] = `
<Image
source={
Object {
"testUri": "../../../dist/assets/images/status/online.png",
}
}
<VectorIcon
name="online"
size={14}
style={
Object {
"height": 32,
"tintColor": "#06d6a0",
"width": 32,
"color": "#06d6a0",
"fontSize": 32,
}
}
type="mattermost"
/>
`;
exports[`UserStatus should match snapshot, should default to offline status 1`] = `
<Image
source={
Object {
"testUri": "../../../dist/assets/images/status/offline.png",
}
}
<VectorIcon
name="offline"
size={14}
style={
Object {
"height": 32,
"tintColor": "rgba(61,60,64,0.3)",
"width": 32,
"color": "rgba(61,60,64,0.3)",
"fontSize": 32,
}
}
type="mattermost"
/>
`;

View file

@ -2,41 +2,38 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Image} from 'react-native';
import PropTypes from 'prop-types';
import {General} from 'mattermost-redux/constants';
import Icon from 'app/components/vector_icon';
import {changeOpacity} from 'app/utils/theme';
import away from 'assets/images/status/away.png';
import dnd from 'assets/images/status/dnd.png';
import offline from 'assets/images/status/offline.png';
import online from 'assets/images/status/online.png';
const statusToIcon = {
away,
dnd,
offline,
ooo: offline,
online,
away: General.AWAY,
dnd: General.DND,
offline: General.OFFLINE,
ooo: General.OFFLINE,
online: General.ONLINE,
};
export default class UserStatus extends PureComponent {
static propTypes = {
isAvatar: PropTypes.bool,
size: PropTypes.number,
status: PropTypes.string,
theme: PropTypes.object.isRequired,
};
static defaultProps = {
size: 14,
size: 6,
status: General.OFFLINE,
};
render() {
const {size, status, theme} = this.props;
const Icon = statusToIcon[status];
const iconName = statusToIcon[status];
let iconColor;
switch (status) {
@ -55,9 +52,10 @@ export default class UserStatus extends PureComponent {
}
return (
<Image
source={Icon}
style={{height: size, width: size, tintColor: iconColor}}
<Icon
name={iconName}
style={{fontSize: size, color: iconColor}}
type='mattermost'
/>
);
}

View file

@ -4,12 +4,21 @@
import PropTypes from 'prop-types';
import React, {PureComponent} from 'react';
import {Text} from 'react-native';
import {createIconSetFromIcoMoon} from 'react-native-vector-icons';
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
import FontAwesome5Icon from 'react-native-vector-icons/FontAwesome5';
import FoundationIcon from 'react-native-vector-icons/Foundation';
import IonIcon from 'react-native-vector-icons/Ionicons';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import icoMoonConfig from 'assets/mattermost-fonts.json';
const Mattermost = createIconSetFromIcoMoon(
icoMoonConfig,
'Mattermost',
'Mattermost-Regular.otf'
);
export default class VectorIcon extends PureComponent {
static propTypes = {
name: PropTypes.string,
@ -66,6 +75,14 @@ export default class VectorIcon extends PureComponent {
size={size}
/>
);
case 'mattermost':
return (
<Mattermost
name={name}
style={style}
size={size}
/>
);
}
return null;

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
const VISIBILITY_CONFIG_DEFAULTS = {
viewAreaCoveragePercentThreshold: 3,
itemVisiblePercentThreshold: 100,
waitForInteraction: false,
};

View file

@ -77,10 +77,12 @@ class ChannelDrawerButton extends PureComponent {
if (badgeCount && visible) {
badge = (
<Badge
containerStyle={style.badgeContainer}
style={style.badge}
countStyle={style.mention}
count={badgeCount}
onPress={this.handlePress}
minWidth={19}
/>
);
}
@ -137,14 +139,16 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
},
badge: {
backgroundColor: theme.mentionBg,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
left: -13,
height: 19,
padding: 3,
},
badgeContainer: {
borderColor: theme.sidebarHeaderBg,
borderRadius: 14,
borderWidth: 2,
position: 'absolute',
right: 0,
top: -4,
right: -14,
top: -7,
},
mention: {
color: theme.mentionColor,

View file

@ -6,7 +6,6 @@ import React, {PureComponent} from 'react';
import {
Keyboard,
Platform,
StyleSheet,
View,
} from 'react-native';
@ -18,6 +17,7 @@ import PostList from 'app/components/post_list';
import RetryBarIndicator from 'app/components/retry_bar_indicator';
import {ViewTypes} from 'app/constants';
import tracker from 'app/utils/time_tracker';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import telemetry from 'app/telemetry';
let ChannelIntro = null;
@ -216,8 +216,11 @@ export default class ChannelPostList extends PureComponent {
);
}
const style = getStyleSheet(theme);
return (
<View style={style.container}>
<View style={style.separator}/>
{component}
<AnnouncementBanner/>
<RetryBarIndicator/>
@ -226,8 +229,12 @@ export default class ChannelPostList extends PureComponent {
}
}
const style = StyleSheet.create({
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
container: {
flex: 1,
},
});
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
height: 1,
},
}));

View file

@ -5,6 +5,14 @@ exports[`thread should match snapshot, has root post 1`] = `
<Connect(SafeAreaIos)
excludeHeader={true}
>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"height": 1,
}
}
/>
<Connect(StatusBar) />
<React.Fragment>
<Connect(PostList)
@ -67,6 +75,14 @@ exports[`thread should match snapshot, no root post, loading 1`] = `
<Connect(SafeAreaIos)
excludeHeader={true}
>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"height": 1,
}
}
/>
<Connect(StatusBar) />
<Loading
color="grey"
@ -128,6 +144,14 @@ exports[`thread should match snapshot, render footer 3`] = `
<Connect(SafeAreaIos)
excludeHeader={true}
>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"height": 1,
}
}
/>
<Connect(StatusBar) />
<Loading
color="grey"

View file

@ -2,15 +2,16 @@
// See LICENSE.txt for license information.
import React from 'react';
import {View} from 'react-native';
import {THREAD} from 'app/constants/screen';
import Loading from 'app/components/loading';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import Loading from 'app/components/loading';
import PostList from 'app/components/post_list';
import PostTextbox from 'app/components/post_textbox';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import ThreadBase from './thread_base';
@ -22,6 +23,7 @@ export default class ThreadAndroid extends ThreadBase {
postIds,
rootId,
channelIsArchived,
theme,
} = this.props;
let content;
@ -54,10 +56,12 @@ export default class ThreadAndroid extends ThreadBase {
);
}
const style = getStyleSheet(theme);
return (
<SafeAreaView>
<StatusBar/>
<KeyboardLayout>
<View style={style.separator}/>
{content}
{postTextBox}
</KeyboardLayout>
@ -65,3 +69,10 @@ export default class ThreadAndroid extends ThreadBase {
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
height: 1,
},
}));

View file

@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import React from 'react';
import {View} from 'react-native';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
@ -16,6 +15,7 @@ import PostTextbox from 'app/components/post_textbox';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
import {THREAD} from 'app/constants/screen';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import ThreadBase from './thread_base';
@ -32,6 +32,7 @@ export default class ThreadIOS extends ThreadBase {
postIds,
rootId,
channelIsArchived,
theme,
} = this.props;
let content;
@ -87,9 +88,11 @@ export default class ThreadIOS extends ThreadBase {
);
}
const style = getStyleSheet(theme);
return (
<React.Fragment>
<SafeAreaView excludeHeader={true}>
<View style={style.separator}/>
<StatusBar/>
{content}
</SafeAreaView>
@ -98,3 +101,10 @@ export default class ThreadIOS extends ThreadBase {
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
height: 1,
},
}));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 B

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -5,7 +5,6 @@
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
@ -123,6 +122,7 @@
F006936FC2884C24A1321FC0 /* MaterialIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 356C9186FA374641A00EB2EA /* MaterialIcons.ttf */; };
F083DB472349411A8E6E7AAD /* OpenSans-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */; };
F23C99AA5FA10E457A76803A /* libPods-MattermostTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4246DC09024BB33A3E491E25 /* libPods-MattermostTests.a */; };
1A892F126F594A27B6AA095E /* Mattermost-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = E2D17ED040D0465DBF9112D3 /* Mattermost-Regular.otf */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -903,6 +903,7 @@
EE671DF7637347CD8C069819 /* libRNDeviceInfo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNDeviceInfo.a; sourceTree = "<group>"; };
F1F071EE85494E269A50AE88 /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = "<group>"; };
FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBoldItalic.ttf"; path = "../assets/fonts/OpenSans-ExtraBoldItalic.ttf"; sourceTree = "<group>"; };
E2D17ED040D0465DBF9112D3 /* Mattermost-Regular.otf */ = {isa = PBXFileReference; name = "Mattermost-Regular.otf"; path = "../assets/fonts/Mattermost-Regular.otf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -1086,6 +1087,7 @@
5C5FEE2D22F048709FB330F3 /* FontAwesome5_Brands.ttf */,
59A2AAD141C34AA7B4655F27 /* FontAwesome5_Regular.ttf */,
DC1D660B55BE462A9C3B8028 /* FontAwesome5_Solid.ttf */,
E2D17ED040D0465DBF9112D3 /* Mattermost-Regular.otf */,
);
name = Resources;
sourceTree = "<group>";
@ -2561,6 +2563,7 @@
E08B278B7BA14C41AD908BE2 /* FontAwesome5_Brands.ttf in Resources */,
DA32194224944F1DBD27C485 /* FontAwesome5_Regular.ttf in Resources */,
7BD159C40A68467FB5A17141 /* FontAwesome5_Solid.ttf in Resources */,
1A892F126F594A27B6AA095E /* Mattermost-Regular.otf in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View file

@ -115,6 +115,7 @@
<string>FontAwesome5_Brands.ttf</string>
<string>FontAwesome5_Regular.ttf</string>
<string>FontAwesome5_Solid.ttf</string>
<string>Mattermost-Regular.otf</string>
</array>
<key>UIBackgroundModes</key>
<array>