Skip to content
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

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion shared/actions/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function _inboxToConversations (inbox: GetInboxAndUnboxLocalRes, author: ?string
info: convo.info,
conversationIDKey: conversationIDToKey(convo.info.id),
participants,
muted: false, // TODO
muted: false, // TODO integrate this when it's available
time: convo.readerInfo.mtime,
snippet,
unreadCount: convo.readerInfo.maxMsgid - convo.readerInfo.readMsgid, // TODO likely get this from the notifications payload miles is working on
Expand Down
143 changes: 105 additions & 38 deletions shared/chat/conversations-list/index.desktop.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,129 @@
// @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) => {
Copy link
Contributor Author

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)

if (idx === lastParticipantIndex) {
if (lastParticipantIndex === 1) {
if (hasUnread) {
return globalColors.orange
}
return isSelected ? globalColors.darkBlue2 : globalColors.darkBlue4
}
return hasUnread ? globalColors.orange : undefined
}
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The 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>
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>
<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 backgroundMode='Terminal' type='BodySmall' style={{marginRight: 4}}>{formatTimeForConversationList(conversation.get('time'), nowOverride)}</Text>
</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: `
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
17 changes: 17 additions & 0 deletions shared/chat/dumb.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ const inbox = [
snippet: 'long ago',
unreadCount: 0,
}),
new InboxStateRecord({
info: null,
participants: List(participants.slice(0, 2)),
conversationIDKey: 'convo6',
muted: false,
time: now - 1000 * 60 * 60 * 3,
snippet: '3 hours ago',
unreadCount: 1,
}),
]

const commonConversationsProps = {
Expand Down Expand Up @@ -244,6 +253,14 @@ const conversationsList = {
'Normal': {
...commonConversationsProps,
},
'Selected Normal': {
...commonConversationsProps,
selectedConversation: 'convo1',
},
'SelectedMuted': {
...commonConversationsProps,
selectedConversation: 'convo3',
},
'Empty': {
...emptyConversationsProps,
},
Expand Down
2 changes: 1 addition & 1 deletion shared/common-adapters/avatar.desktop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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',
}}
Expand Down
1 change: 1 addition & 0 deletions shared/common-adapters/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export {default as Input} from './input'
export {default as LinkWithIcon} from './link-with-icon'
export {default as ListItem} from './list-item'
export {default as Markdown} from './markdown.js'
export {default as MultiAvatar} from './multi-avatar.js'
export {default as Meta} from './meta'
export {default as PlatformIcon} from './platform-icon'
export {default as PopupMenu} from './popup-menu'
Expand Down
52 changes: 52 additions & 0 deletions shared/common-adapters/multi-avatar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// @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'

export 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Affected by this eslint import issue: import-js/eslint-plugin-import#660