-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversations panel (inbox) #4980
Changes from 7 commits
55fe663
837563a
322905f
c80c24f
9c33e64
f4bf91f
6156fb4
c847866
2f015ee
199bb2a
8efe1d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,62 +1,130 @@ | ||
// @flow | ||
import React from 'react' | ||
import {Box, Text, Avatar, Icon, Usernames} from '../../common-adapters' | ||
import {Box, Text, MultiAvatar, Icon, Usernames} from '../../common-adapters' | ||
import {globalStyles, globalColors} from '../../styles' | ||
import {participantFilter} from '../../constants/chat' | ||
import {formatTimeForConversationList} from '../../util/timestamp' | ||
|
||
import type {Props} from './' | ||
import type {InboxState} from '../../constants/chat' | ||
|
||
const ConversationList = ({inbox, onSelectConversation, selectedConversation, onNewChat, nowOverride}: Props) => ( | ||
<Box style={{...globalStyles.flexBoxColumn, backgroundColor: globalColors.darkBlue4, width: 240}}> | ||
const AddNewRow = ({onNewChat}: Props) => ( | ||
<Box | ||
style={{...globalStyles.flexBoxRow, ...globalStyles.clickable, minHeight: 48, justifyContent: 'center', alignItems: 'center', flexShrink: 0}} | ||
onClick={() => onNewChat()}> | ||
<Icon type='iconfont-new' style={{color: globalColors.blue, marginRight: 9}} /> | ||
<Text type='BodyBigLink'>New chat</Text> | ||
</Box> | ||
) | ||
|
||
const rowBorderColor = (idx: number, lastParticipantIndex: number, hasUnread: boolean, isSelected: boolean) => { | ||
if (idx === lastParticipantIndex) { | ||
if (lastParticipantIndex === 1) { | ||
if (hasUnread) { | ||
return globalColors.orange | ||
} | ||
return isSelected ? globalColors.darkBlue2 : globalColors.darkBlue4 | ||
} | ||
return hasUnread ? globalColors.orange : undefined | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🐑 🐑 I think this might be an easier to understand format: if (idx === lastParticipantIndex && lastParticipantIndex === 1 && hasUnread) {
return globalColors.orange
}
if (idx === lastParticipantIndex && lastParticipantIndex === 1 && isSelected) {
return globalColors.darkBlue2
}
if (idx === lastParticipantIndex && lastParticipantIndex === 1) {
return globalColors.darkBlue4
}
if (idx === lastParticipantIndex && hasUnread) {
return globalColors.orange
}
return undefined There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i changed mine to be more readable i think, take a look in the next commit |
||
|
||
return undefined | ||
} | ||
|
||
const Row = ({onSelectConversation, selectedConversation, onNewChat, nowOverride, conversation}: Props & {conversation: InboxState}) => { | ||
const participants = participantFilter(conversation.get('participants')) | ||
const isSelected = selectedConversation === conversation.get('conversationIDKey') | ||
const isMuted = conversation.get('muted') | ||
// $FlowIssue | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. flow doesn't like me passing avatar props into the multi since its a union. dunno what a good fix is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we do something like: https://github.com/keybase/client/blob/master/shared/chat/conversation/index.desktop.js#L16 so types will still help us? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah I think I understand, it won't even type to Array properly |
||
const avatarProps = participants.slice(0, 2).map((p, idx) => ({ | ||
backgroundColor: globalColors.darkBlue4, | ||
username: p.username, | ||
borderColor: rowBorderColor(idx, Math.min(2, participants.count()) - 1, !!conversation.get('unreadCount'), isSelected), | ||
})).toArray() | ||
const snippet = conversation.get('snippet') | ||
const subColor = isSelected ? globalColors.white : globalColors.blue3_40 | ||
return ( | ||
<Box | ||
style={{...globalStyles.flexBoxRow, ...globalStyles.clickable, height: 48, justifyContent: 'center', alignItems: 'center'}} | ||
onClick={() => onNewChat()}> | ||
<Icon type='iconfont-new' style={{color: globalColors.blue, marginRight: 9}} /> | ||
<Text type='BodyBigLink'>New chat</Text> | ||
</Box> | ||
{inbox.map(conversation => { | ||
const participants = participantFilter(conversation.get('participants')) | ||
|
||
return (<Box | ||
onClick={() => onSelectConversation(conversation.get('conversationIDKey'))} | ||
title={`${conversation.get('unreadCount')} unread`} | ||
style={{ | ||
...containerStyle, | ||
backgroundColor: selectedConversation === conversation.get('conversationIDKey') ? globalColors.darkBlue2 : globalColors.transparent, | ||
}} | ||
key={conversation.get('conversationIDKey')}> | ||
<Avatar | ||
size={32} | ||
backgroundColor={globalColors.darkBlue4} | ||
username={participants.first().username} | ||
borderColor={conversation.get('unreadCount') ? globalColors.orange : undefined} | ||
/> | ||
<Box style={{...globalStyles.flexBoxColumn, flex: 1, marginLeft: 12, position: 'relative'}}> | ||
<Box style={{...globalStyles.flexBoxColumn, position: 'absolute', top: 0, bottom: 0, left: 0, right: 0}}> | ||
<Usernames inline={true} type='Body' backgroundMode='Terminal' users={participants.toArray()} title={participants.map(p => p.username).join(', ')} /> | ||
<Text backgroundMode='Terminal' type='BodySmall' style={noWrapStyle}>{conversation.get('snippet')}</Text> | ||
</Box> | ||
onClick={() => onSelectConversation(conversation.get('conversationIDKey'))} | ||
title={`${conversation.get('unreadCount')} unread`} | ||
style={{...rowContainerStyle, | ||
backgroundColor: isSelected ? globalColors.darkBlue2 : globalColors.transparent}}> | ||
<Box style={{...globalStyles.flexBoxRow, flex: 1, maxWidth: 48, alignItems: 'center', justifyContent: 'flex-start', paddingLeft: 4}}> | ||
<MultiAvatar singleSize={32} multiSize={24} avatarProps={avatarProps} /> | ||
{isMuted && <Icon type='iconfont-shh' style={shhStyle} />} | ||
</Box> | ||
<Text backgroundMode='Terminal' type='BodySmall' style={{marginRight: 4}}>{formatTimeForConversationList(conversation.get('time'), nowOverride)}</Text> | ||
</Box>) | ||
})} | ||
<Box style={{...globalStyles.flexBoxRow, flex: 1, borderBottom: `solid 1px ${globalColors.black_10}`, paddingRight: 8, paddingTop: 4, paddingBottom: 4}}> | ||
<Box style={{...globalStyles.flexBoxColumn, flex: 1, position: 'relative'}}> | ||
<Box style={{...globalStyles.flexBoxColumn, position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, alignItems: 'center', justifyContent: 'center'}}> | ||
<Usernames | ||
inline={true} | ||
type='BodySemibold' | ||
style={{color: isMuted ? globalColors.blue3_40 : globalColors.white}} | ||
containerStyle={{color: isMuted ? globalColors.blue3_40 : globalColors.white, paddingRight: 7}} | ||
users={participants.toArray()} | ||
title={participants.map(p => p.username).join(', ')} /> | ||
{snippet && !isMuted && <Text type='BodySmall' style={{...noWrapStyle, color: subColor}}>{snippet}</Text>} | ||
</Box> | ||
</Box> | ||
<Text type='BodySmall' style={{marginRight: 4, alignSelf: isMuted ? 'center' : 'flex-start', color: subColor}}>{formatTimeForConversationList(conversation.get('time'), nowOverride)}</Text> | ||
</Box> | ||
</Box> | ||
) | ||
} | ||
|
||
const shhStyle = { | ||
color: globalColors.darkBlue2, | ||
alignSelf: 'flex-end', | ||
marginLeft: -5, | ||
marginTop: 5, | ||
// TODO remove this when we get the updated icon w/ the stroke | ||
textShadow: ` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was just to get it kinda looking close. I asked @cecileboucheron to give us a perfect icon instead of using the iconfont |
||
-1px -1px 0 ${globalColors.darkBlue4}, | ||
1px -1px 0 ${globalColors.darkBlue4}, | ||
-1px 1px 0 ${globalColors.darkBlue4}, | ||
1px 1px 0 ${globalColors.darkBlue4}, | ||
-2px -2px 0 ${globalColors.darkBlue4}, | ||
2px -2px 0 ${globalColors.darkBlue4}, | ||
-2px 2px 0 ${globalColors.darkBlue4}, | ||
2px 2px 0 ${globalColors.darkBlue4}`, | ||
} | ||
|
||
const ConversationList = (props: Props) => ( | ||
<Box style={containerStyle}> | ||
<AddNewRow {...props} /> | ||
<Box style={scrollableStyle}> | ||
{props.inbox.map(conversation => <Row {...props} key={conversation.get('conversationIDKey')} conversation={conversation} />)} | ||
</Box> | ||
</Box> | ||
) | ||
|
||
const containerStyle = { | ||
...globalStyles.flexBoxColumn, | ||
backgroundColor: globalColors.darkBlue4, | ||
maxWidth: 240, | ||
flex: 1, | ||
} | ||
|
||
const scrollableStyle = { | ||
...globalStyles.flexBoxColumn, | ||
flex: 1, | ||
overflowY: 'auto', | ||
} | ||
|
||
const noWrapStyle = { | ||
whiteSpace: 'nowrap', | ||
display: 'block', | ||
width: '100%', | ||
textOverflow: 'ellipsis', | ||
overflow: 'hidden', | ||
textOverflow: 'ellipsis', | ||
whiteSpace: 'nowrap', | ||
width: '100%', | ||
} | ||
|
||
const containerStyle = { | ||
const rowContainerStyle = { | ||
...globalStyles.flexBoxRow, | ||
...globalStyles.clickable, | ||
padding: 4, | ||
borderBottom: `solid 1px ${globalColors.black_10}`, | ||
flexShrink: 0, | ||
minHeight: 40, | ||
maxHeight: 40, | ||
} | ||
|
||
export default ConversationList |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,7 +58,7 @@ class Avatar extends PureComponent<void, Props, State> { | |
...avatarStyle, | ||
...borderStyle, | ||
display: (!showNoAvatar && !showLoadingColor) ? 'block' : 'none', | ||
backgroundColor: globalColors.white, | ||
backgroundColor: this.props.backgroundColor || globalColors.white, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if this is white always you can get bad aliasing on the edges on dark backgrounds due to the alpha blend on the border radius |
||
opacity: this.props.hasOwnProperty('opacity') ? this.props.opacity : 1.0, | ||
backgroundClip: 'padding-box', | ||
}} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// @flow | ||
// Simple control to show multiple avatars. Just used in chat but could be expanded. Keeping this simple for now | ||
import Avatar from './avatar' | ||
import Box from './box' | ||
import React from 'react' | ||
|
||
import type {Props as AvatarProps, AvatarSize} from './avatar' | ||
|
||
type Props = { | ||
avatarProps: Array<AvatarProps>, | ||
singleSize: AvatarSize, | ||
multiSize: AvatarSize, | ||
style?: ?Object, | ||
} | ||
|
||
const MultiAvatar = ({avatarProps, singleSize, multiSize, style}: Props) => { | ||
if (avatarProps.length < 0) { | ||
return null | ||
} | ||
if (avatarProps.length > 2) { | ||
console.warn('MultiAvatar only handles up to 2 avatars') | ||
return null | ||
} | ||
|
||
const leftProps: AvatarProps = avatarProps[0] | ||
const rightProps: AvatarProps = avatarProps[1] | ||
|
||
if (avatarProps.length === 1) { | ||
return <Avatar {...leftProps} size={singleSize} /> | ||
} | ||
|
||
return ( | ||
<Box style={{...containerStyle, ...style}}> | ||
<Avatar {...leftProps} style={leftAvatar} size={multiSize} /> | ||
<Avatar {...rightProps} style={rightAvatar} size={multiSize} /> | ||
</Box> | ||
) | ||
} | ||
|
||
const containerStyle = { | ||
position: 'relative', | ||
} | ||
|
||
const leftAvatar = { | ||
} | ||
|
||
const rightAvatar = { | ||
marginLeft: '34%', | ||
marginTop: '-63%', | ||
} | ||
|
||
export default MultiAvatar | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Affected by this eslint import issue: import-js/eslint-plugin-import#660 |
||
export type { | ||
Props, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the multi avatar case we actually have a blue border around the second avatar. If there is an unread then we orange circle the last avatar (can only be one also)