[IMPROVEMENT] Threads layout tweaks (#2686)
* improvement: Thread Details * fix: re-render Thread Messages Item * fix: update snapshots * improve: thread details component * fix: cast replies length * improvement: format date of threads * improvement: thread details styles * fix: wrap text * tests: update snapshot * improvement: use same date format for all dates * Icon size 24 * Remove date * Remove prop drill * Badge position * Badge container tweak * Fix inline style * Move ThreadDetails to containers * Update stories * Fix lint * Remove wrong prop Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
parent
32b1b36e48
commit
4d13689503
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,103 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
import Touchable from 'react-native-platform-touchable';
|
||||||
|
|
||||||
|
import { CustomIcon } from '../lib/Icons';
|
||||||
|
import { themes } from '../constants/colors';
|
||||||
|
import sharedStyles from '../views/Styles';
|
||||||
|
import { withTheme } from '../theme';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
detailsContainer: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
detailContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: 8
|
||||||
|
},
|
||||||
|
detailText: {
|
||||||
|
fontSize: 10,
|
||||||
|
marginLeft: 2,
|
||||||
|
...sharedStyles.textSemibold
|
||||||
|
},
|
||||||
|
badgeContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
marginRight: 8
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ThreadDetails = ({
|
||||||
|
item,
|
||||||
|
user,
|
||||||
|
badgeColor,
|
||||||
|
toggleFollowThread,
|
||||||
|
style,
|
||||||
|
theme
|
||||||
|
}) => {
|
||||||
|
let { tcount } = item;
|
||||||
|
if (tcount >= 1000) {
|
||||||
|
tcount = '+999';
|
||||||
|
} else if (tcount >= 100) {
|
||||||
|
tcount = '+99';
|
||||||
|
}
|
||||||
|
|
||||||
|
let replies = item?.replies?.length ?? 0;
|
||||||
|
if (replies >= 1000) {
|
||||||
|
replies = '+999';
|
||||||
|
} else if (replies >= 100) {
|
||||||
|
replies = '+99';
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFollowing = item.replies?.find(u => u === user?.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, style]}>
|
||||||
|
<View style={styles.detailsContainer}>
|
||||||
|
<View style={styles.detailContainer}>
|
||||||
|
<CustomIcon name='threads' size={24} color={themes[theme].auxiliaryText} />
|
||||||
|
<Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{tcount}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.detailContainer}>
|
||||||
|
<CustomIcon name='user' size={24} color={themes[theme].auxiliaryText} />
|
||||||
|
<Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{replies}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.badgeContainer}>
|
||||||
|
{badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null }
|
||||||
|
<Touchable onPress={() => toggleFollowThread?.(isFollowing, item.id)}>
|
||||||
|
<CustomIcon
|
||||||
|
size={24}
|
||||||
|
name={isFollowing ? 'notification' : 'notification-disabled'}
|
||||||
|
color={themes[theme].auxiliaryTintColor}
|
||||||
|
/>
|
||||||
|
</Touchable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
ThreadDetails.propTypes = {
|
||||||
|
item: PropTypes.object,
|
||||||
|
user: PropTypes.object,
|
||||||
|
badgeColor: PropTypes.string,
|
||||||
|
toggleFollowThread: PropTypes.func,
|
||||||
|
style: PropTypes.object,
|
||||||
|
theme: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTheme(ThreadDetails);
|
|
@ -1,15 +1,12 @@
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { View, Text } from 'react-native';
|
import { View, Text } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Touchable from 'react-native-platform-touchable';
|
|
||||||
|
|
||||||
import { formatMessageCount } from './utils';
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
|
||||||
import { THREAD } from './constants';
|
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { formatDateThreads } from '../../utils/room';
|
|
||||||
import MessageContext from './Context';
|
import MessageContext from './Context';
|
||||||
|
import ThreadDetails from '../ThreadDetails';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
|
||||||
const Thread = React.memo(({
|
const Thread = React.memo(({
|
||||||
msg, tcount, tlm, isThreadRoom, theme, id
|
msg, tcount, tlm, isThreadRoom, theme, id
|
||||||
|
@ -21,28 +18,26 @@ const Thread = React.memo(({
|
||||||
const {
|
const {
|
||||||
threadBadgeColor, toggleFollowThread, user, replies
|
threadBadgeColor, toggleFollowThread, user, replies
|
||||||
} = useContext(MessageContext);
|
} = useContext(MessageContext);
|
||||||
const time = formatDateThreads(tlm);
|
|
||||||
const buttonText = formatMessageCount(tcount, THREAD);
|
|
||||||
const isFollowing = replies?.find(u => u === user.id);
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.buttonContainer}>
|
<View style={styles.buttonContainer}>
|
||||||
<View
|
<View
|
||||||
style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
|
style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
|
||||||
testID={`message-thread-button-${ msg }`}
|
testID={`message-thread-button-${ msg }`}
|
||||||
>
|
>
|
||||||
<CustomIcon name='threads' size={16} style={[styles.buttonIcon, { color: themes[theme].buttonText }]} />
|
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Reply')}</Text>
|
||||||
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
<ThreadDetails
|
||||||
{threadBadgeColor ? <View style={[styles.threadBadge, { backgroundColor: threadBadgeColor }]} /> : null}
|
item={{
|
||||||
<Touchable onPress={() => toggleFollowThread(isFollowing, id)}>
|
tcount,
|
||||||
<CustomIcon
|
replies,
|
||||||
name={isFollowing ? 'notification' : 'notification-disabled'}
|
tlm,
|
||||||
size={24}
|
id
|
||||||
color={themes[theme].auxiliaryText}
|
}}
|
||||||
style={styles.threadBell}
|
user={user}
|
||||||
|
badgeColor={threadBadgeColor}
|
||||||
|
toggleFollowThread={toggleFollowThread}
|
||||||
|
style={styles.threadDetails}
|
||||||
/>
|
/>
|
||||||
</Touchable>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => {
|
}, (prevProps, nextProps) => {
|
||||||
|
|
|
@ -176,5 +176,9 @@ export default StyleSheet.create({
|
||||||
},
|
},
|
||||||
encrypted: {
|
encrypted: {
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
threadDetails: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 12
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { View, Text, StyleSheet } from 'react-native';
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
import Touchable from 'react-native-platform-touchable';
|
||||||
|
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
import Avatar from '../../containers/Avatar';
|
import Avatar from '../../containers/Avatar';
|
||||||
import Touch from '../../utils/touch';
|
|
||||||
import sharedStyles from '../Styles';
|
import sharedStyles from '../Styles';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import Markdown from '../../containers/markdown';
|
import Markdown from '../../containers/markdown';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
|
||||||
import { formatDateThreads, makeThreadName } from '../../utils/room';
|
import { formatDateThreads, makeThreadName } from '../../utils/room';
|
||||||
|
import ThreadDetails from '../../containers/ThreadDetails';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
@ -38,34 +38,26 @@ const styles = StyleSheet.create({
|
||||||
avatar: {
|
avatar: {
|
||||||
marginRight: 8
|
marginRight: 8
|
||||||
},
|
},
|
||||||
detailsContainer: {
|
threadDetails: {
|
||||||
marginTop: 8,
|
marginTop: 8
|
||||||
flexDirection: 'row'
|
|
||||||
},
|
|
||||||
detailContainer: {
|
|
||||||
marginRight: 8,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
},
|
|
||||||
detailText: {
|
|
||||||
fontSize: 10,
|
|
||||||
marginLeft: 2,
|
|
||||||
...sharedStyles.textSemibold
|
|
||||||
},
|
|
||||||
badgeContainer: {
|
|
||||||
marginLeft: 8,
|
|
||||||
justifyContent: 'center'
|
|
||||||
},
|
},
|
||||||
badge: {
|
badge: {
|
||||||
width: 12,
|
width: 8,
|
||||||
height: 12,
|
height: 8,
|
||||||
borderRadius: 6
|
borderRadius: 4,
|
||||||
|
marginHorizontal: 8,
|
||||||
|
alignSelf: 'center'
|
||||||
|
},
|
||||||
|
messageContainer: {
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
markdown: {
|
||||||
|
flex: 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const Item = ({
|
const Item = ({
|
||||||
item, baseUrl, theme, useRealName, user, badgeColor, onPress
|
item, baseUrl, theme, useRealName, user, badgeColor, onPress, toggleFollowThread
|
||||||
}) => {
|
}) => {
|
||||||
const username = (useRealName && item?.u?.name) || item?.u?.username;
|
const username = (useRealName && item?.u?.name) || item?.u?.username;
|
||||||
let time;
|
let time;
|
||||||
|
@ -73,13 +65,8 @@ const Item = ({
|
||||||
time = formatDateThreads(item.ts);
|
time = formatDateThreads(item.ts);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tlm;
|
|
||||||
if (item?.tlm) {
|
|
||||||
tlm = formatDateThreads(item.tlm);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Touch theme={theme} onPress={() => onPress(item)} testID={`thread-messages-view-${ item.msg }`} style={{ backgroundColor: themes[theme].backgroundColor }}>
|
<Touchable onPress={() => onPress(item)} testID={`thread-messages-view-${ item.msg }`} style={{ backgroundColor: themes[theme].backgroundColor }}>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Avatar
|
<Avatar
|
||||||
style={styles.avatar}
|
style={styles.avatar}
|
||||||
|
@ -96,33 +83,19 @@ const Item = ({
|
||||||
<Text style={[styles.title, { color: themes[theme].titleText }]} numberOfLines={1}>{username}</Text>
|
<Text style={[styles.title, { color: themes[theme].titleText }]} numberOfLines={1}>{username}</Text>
|
||||||
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Markdown msg={makeThreadName(item)} baseUrl={baseUrl} username={username} theme={theme} numberOfLines={2} preview />
|
<View style={styles.messageContainer}>
|
||||||
<View style={styles.detailsContainer}>
|
<Markdown msg={makeThreadName(item)} baseUrl={baseUrl} username={username} theme={theme} numberOfLines={2} style={[styles.markdown]} preview />
|
||||||
<View style={styles.detailContainer}>
|
{badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null }
|
||||||
<CustomIcon name='threads' size={20} color={themes[theme].auxiliaryText} />
|
|
||||||
<Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]}>{item?.tcount}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
<ThreadDetails
|
||||||
<View style={styles.detailContainer}>
|
item={item}
|
||||||
<CustomIcon name='user' size={20} color={themes[theme].auxiliaryText} />
|
user={user}
|
||||||
<Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]}>{item?.replies?.length}</Text>
|
toggleFollowThread={toggleFollowThread}
|
||||||
</View>
|
style={styles.threadDetails}
|
||||||
|
/>
|
||||||
<View style={styles.detailContainer}>
|
|
||||||
<CustomIcon name='clock' size={20} color={themes[theme].auxiliaryText} />
|
|
||||||
<Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]}>{tlm}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</Touchable>
|
||||||
{badgeColor
|
|
||||||
? (
|
|
||||||
<View style={styles.badgeContainer}>
|
|
||||||
<View style={[styles.badge, { backgroundColor: badgeColor }]} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</View>
|
|
||||||
</Touch>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -133,7 +106,8 @@ Item.propTypes = {
|
||||||
useRealName: PropTypes.bool,
|
useRealName: PropTypes.bool,
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
badgeColor: PropTypes.string,
|
badgeColor: PropTypes.string,
|
||||||
onPress: PropTypes.func
|
onPress: PropTypes.func,
|
||||||
|
toggleFollowThread: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withTheme(Item);
|
export default withTheme(Item);
|
||||||
|
|
|
@ -33,6 +33,8 @@ import { isIOS } from '../../utils/deviceInfo';
|
||||||
import { getBadgeColor, makeThreadName } from '../../utils/room';
|
import { getBadgeColor, makeThreadName } from '../../utils/room';
|
||||||
import { getHeaderTitlePosition } from '../../containers/Header';
|
import { getHeaderTitlePosition } from '../../containers/Header';
|
||||||
import SearchHeader from './SearchHeader';
|
import SearchHeader from './SearchHeader';
|
||||||
|
import EventEmitter from '../../utils/events';
|
||||||
|
import { LISTENER } from '../../containers/Toast';
|
||||||
|
|
||||||
const API_FETCH_COUNT = 50;
|
const API_FETCH_COUNT = 50;
|
||||||
|
|
||||||
|
@ -410,6 +412,15 @@ class ThreadMessagesView extends React.Component {
|
||||||
this.setState({ currentFilter: filter, displayingThreads });
|
this.setState({ currentFilter: filter, displayingThreads });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleFollowThread = async(isFollowingThread, tmid) => {
|
||||||
|
try {
|
||||||
|
await RocketChat.toggleFollowMessage(tmid, !isFollowingThread);
|
||||||
|
EventEmitter.emit(LISTENER, { message: isFollowingThread ? I18n.t('Unfollowed_thread') : I18n.t('Following_thread') });
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderItem = ({ item }) => {
|
renderItem = ({ item }) => {
|
||||||
const {
|
const {
|
||||||
user, navigation, baseUrl, useRealName
|
user, navigation, baseUrl, useRealName
|
||||||
|
@ -426,6 +437,7 @@ class ThreadMessagesView extends React.Component {
|
||||||
badgeColor
|
badgeColor
|
||||||
}}
|
}}
|
||||||
onPress={this.onThreadPress}
|
onPress={this.onThreadPress}
|
||||||
|
toggleFollowThread={this.toggleFollowThread}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -469,11 +469,6 @@ export default ({ theme }) => {
|
||||||
tcount={1}
|
tcount={1}
|
||||||
tlm={date}
|
tlm={date}
|
||||||
/>
|
/>
|
||||||
<Message
|
|
||||||
msg='How are you?'
|
|
||||||
tcount={9999}
|
|
||||||
tlm={date}
|
|
||||||
/>
|
|
||||||
<Message
|
<Message
|
||||||
msg="I'm fine!"
|
msg="I'm fine!"
|
||||||
tmid='1'
|
tmid='1'
|
||||||
|
|
Loading…
Reference in New Issue