diff --git a/shared/actions/chat.js b/shared/actions/chat.js index add9027b1a42..3df585cbccbf 100644 --- a/shared/actions/chat.js +++ b/shared/actions/chat.js @@ -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 diff --git a/shared/chat/conversations-list/index.desktop.js b/shared/chat/conversations-list/index.desktop.js index 02c9c9cb97e6..45fde4e545f4 100644 --- a/shared/chat/conversations-list/index.desktop.js +++ b/shared/chat/conversations-list/index.desktop.js @@ -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) => ( - +const AddNewRow = ({onNewChat}: Props) => ( + onNewChat()}> + + New chat + +) + +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 + } + + 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 + 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 ( onNewChat()}> - - New chat - - {inbox.map(conversation => { - const participants = participantFilter(conversation.get('participants')) - - return ( onSelectConversation(conversation.get('conversationIDKey'))} - title={`${conversation.get('unreadCount')} unread`} - style={{ - ...containerStyle, - backgroundColor: selectedConversation === conversation.get('conversationIDKey') ? globalColors.darkBlue2 : globalColors.transparent, - }} - key={conversation.get('conversationIDKey')}> - - - - p.username).join(', ')} /> - {conversation.get('snippet')} + onClick={() => onSelectConversation(conversation.get('conversationIDKey'))} + title={`${conversation.get('unreadCount')} unread`} + style={{...rowContainerStyle, backgroundColor: isSelected ? globalColors.darkBlue2 : globalColors.transparent}}> + + + {isMuted && } + + + + + p.username).join(', ')} /> + {snippet && !isMuted && {snippet}} - {formatTimeForConversationList(conversation.get('time'), nowOverride)} - ) - })} + {formatTimeForConversationList(conversation.get('time'), nowOverride)} + + + ) +} + +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: ` + -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) => ( + + + + {props.inbox.map(conversation => )} + ) +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 diff --git a/shared/chat/dumb.js b/shared/chat/dumb.js index 643c8c91a76f..dee6bf246ab7 100644 --- a/shared/chat/dumb.js +++ b/shared/chat/dumb.js @@ -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 = { @@ -244,6 +253,14 @@ const conversationsList = { 'Normal': { ...commonConversationsProps, }, + 'Selected Normal': { + ...commonConversationsProps, + selectedConversation: 'convo1', + }, + 'SelectedMuted': { + ...commonConversationsProps, + selectedConversation: 'convo3', + }, 'Empty': { ...emptyConversationsProps, }, diff --git a/shared/common-adapters/avatar.desktop.js b/shared/common-adapters/avatar.desktop.js index f601df973cd2..f0abd5a08549 100644 --- a/shared/common-adapters/avatar.desktop.js +++ b/shared/common-adapters/avatar.desktop.js @@ -58,7 +58,7 @@ class Avatar extends PureComponent { ...avatarStyle, ...borderStyle, display: (!showNoAvatar && !showLoadingColor) ? 'block' : 'none', - backgroundColor: globalColors.white, + backgroundColor: this.props.backgroundColor || globalColors.white, opacity: this.props.hasOwnProperty('opacity') ? this.props.opacity : 1.0, backgroundClip: 'padding-box', }} diff --git a/shared/common-adapters/index.js b/shared/common-adapters/index.js index e14ce2e35b93..802a2a397571 100644 --- a/shared/common-adapters/index.js +++ b/shared/common-adapters/index.js @@ -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' diff --git a/shared/common-adapters/multi-avatar.js b/shared/common-adapters/multi-avatar.js new file mode 100644 index 000000000000..59c3d5ebd4b2 --- /dev/null +++ b/shared/common-adapters/multi-avatar.js @@ -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, + 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 + } + + return ( + + + + + ) +} + +const containerStyle = { + position: 'relative', +} + +const leftAvatar = { +} + +const rightAvatar = { + marginLeft: '34%', + marginTop: '-63%', +} + +export default MultiAvatar