Add group avatar as on mobile: with multiple group members avatar

pull/1336/head
Audric Ackermann 5 years ago
parent bc8999e0b6
commit 35ea6af27f
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -15,6 +15,8 @@ interface ConversationAttributes {
timestamp: number; // timestamp of what? timestamp: number; // timestamp of what?
groupAdmins?: Array<string>; groupAdmins?: Array<string>;
isKickedFromGroup?: boolean; isKickedFromGroup?: boolean;
avatarPath?: string;
isMe?: boolean;
} }
export interface ConversationModel export interface ConversationModel

@ -9,6 +9,7 @@
img { img {
object-fit: cover; object-fit: cover;
border-radius: 50%; border-radius: 50%;
border: 1px solid $borderAvatarColor;
} }
} }
@ -167,8 +168,6 @@
@include color-svg('../images/note-28.svg', $color-white); @include color-svg('../images/note-28.svg', $color-white);
} }
// Module: Avatar
.module-avatar__label { .module-avatar__label {
color: $color-gray-05; color: $color-gray-05;
} }
@ -189,8 +188,10 @@
background-color: $color-gray-75; background-color: $color-gray-75;
} }
.module-avatar--no-image { .module-avatar__icon-closed {
@include themify($themes) { .module-avatar:last-child {
background-color: themed('steelColorShade'); position: absolute;
right: 0px;
bottom: 0px;
} }
} }

@ -6,6 +6,11 @@ $destructive: #ff453a;
$accentLightTheme: #00e97b; $accentLightTheme: #00e97b;
$accentDarkTheme: #00f782; $accentDarkTheme: #00f782;
$borderLightTheme: #f1f1f1; // search for references on ts TODO: make this exposed on ts
$borderDarkTheme: rgba($white, 0.06);
$borderAvatarColor: #000a; // search for references on ts TODO: make this exposed on ts
$themes: ( $themes: (
light: ( light: (
accent: $accentLightTheme, accent: $accentLightTheme,
@ -40,7 +45,7 @@ $themes: (
conversationItemHasUnread: #fcfcfc, conversationItemHasUnread: #fcfcfc,
conversationItemSelected: #f0f0f0, conversationItemSelected: #f0f0f0,
clickableHovered: #dfdfdf, clickableHovered: #dfdfdf,
sessionBorder: 1px solid #f1f1f1, sessionBorder: 1px solid $borderLightTheme,
sessionUnreadBorder: 4px solid $accentLightTheme, sessionUnreadBorder: 4px solid $accentLightTheme,
leftpaneOverlayBackground: $white, leftpaneOverlayBackground: $white,
// scrollbars // scrollbars
@ -52,8 +57,6 @@ $themes: (
// context menu // context menu
contextMenuBackground: #f5f5f5, contextMenuBackground: #f5f5f5,
filterSessionText: brightness(0) saturate(100%), filterSessionText: brightness(0) saturate(100%),
steelColor: #6b6b78,
steelColorShade: #5a5a63,
lastSeenIndicatorColor: #62656a, lastSeenIndicatorColor: #62656a,
lastSeenIndicatorTextColor: #070c14, lastSeenIndicatorTextColor: #070c14,
quoteBottomBarBackground: #f0f0f0, quoteBottomBarBackground: #f0f0f0,
@ -91,7 +94,7 @@ $themes: (
conversationItemHasUnread: #2c2c2c, conversationItemHasUnread: #2c2c2c,
conversationItemSelected: #404040, conversationItemSelected: #404040,
clickableHovered: #414347, clickableHovered: #414347,
sessionBorder: 1px solid rgba($white, 0.06), sessionBorder: 1px solid $borderDarkTheme,
sessionUnreadBorder: 4px solid $accentDarkTheme, sessionUnreadBorder: 4px solid $accentDarkTheme,
leftpaneOverlayBackground: linear-gradient(180deg, #171717 0%, #121212 100%), leftpaneOverlayBackground: linear-gradient(180deg, #171717 0%, #121212 100%),
// scrollbars // scrollbars
@ -103,8 +106,6 @@ $themes: (
// context menu // context menu
contextMenuBackground: #212121, contextMenuBackground: #212121,
filterSessionText: none, filterSessionText: none,
steelColor: #6b6b78,
steelColorShade: #5a5a63,
lastSeenIndicatorColor: #353535, lastSeenIndicatorColor: #353535,
lastSeenIndicatorTextColor: #a8a9aa, lastSeenIndicatorTextColor: #a8a9aa,
quoteBottomBarBackground: #404040, quoteBottomBarBackground: #404040,

@ -3,19 +3,20 @@ import classNames from 'classnames';
import { getInitials } from '../util/getInitials'; import { getInitials } from '../util/getInitials';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { AvatarPlaceHolder } from './AvatarPlaceHolder'; import { AvatarPlaceHolder, ClosedGroupAvatar } from './AvatarPlaceHolder';
import { ConversationAttributes } from '../../js/models/conversations';
interface Props { interface Props {
avatarPath?: string; avatarPath?: string;
color?: string; color?: string;
conversationType: 'group' | 'direct'; conversationType: 'group' | 'direct';
isPublic?: boolean;
noteToSelf?: boolean; noteToSelf?: boolean;
name?: string; name?: string;
phoneNumber?: string; phoneNumber?: string;
profileName?: string; profileName?: string;
size: number; size: number;
borderColor?: string; closedMemberConversations?: Array<ConversationAttributes>;
borderWidth?: number;
i18n?: LocalizerType; i18n?: LocalizerType;
onAvatarClick?: () => void; onAvatarClick?: () => void;
} }
@ -40,8 +41,9 @@ export class Avatar extends React.PureComponent<Props, State> {
} }
public handleImageError() { public handleImageError() {
// tslint:disable-next-line no-console window.log.warn(
console.log('Avatar: Image failed to load; failing over to placeholder'); 'Avatar: Image failed to load; failing over to placeholder'
);
this.setState({ this.setState({
imageBroken: true, imageBroken: true,
}); });
@ -62,6 +64,7 @@ export class Avatar extends React.PureComponent<Props, State> {
diameter={size} diameter={size}
name={userName} name={userName}
colors={this.getAvatarColors()} colors={this.getAvatarColors()}
borderColor={this.getAvatarBorderColor()}
/> />
); );
} }
@ -88,7 +91,15 @@ export class Avatar extends React.PureComponent<Props, State> {
} }
public renderNoImage() { public renderNoImage() {
const { conversationType, name, noteToSelf, size } = this.props; const {
conversationType,
closedMemberConversations,
isPublic,
name,
noteToSelf,
size,
i18n,
} = this.props;
const initials = getInitials(name); const initials = getInitials(name);
const isGroup = conversationType === 'group'; const isGroup = conversationType === 'group';
@ -118,6 +129,17 @@ export class Avatar extends React.PureComponent<Props, State> {
); );
} }
if (isGroup && !isPublic && closedMemberConversations) {
const forcedI18n = i18n || window.i18n;
return (
<ClosedGroupAvatar
size={size}
conversations={closedMemberConversations}
i18n={forcedI18n}
/>
);
}
return ( return (
<div <div
className={classNames( className={classNames(
@ -188,4 +210,8 @@ export class Avatar extends React.PureComponent<Props, State> {
// defined in session-android as `profile_picture_placeholder_colors` // defined in session-android as `profile_picture_placeholder_colors`
return ['#5ff8b0', '#26cdb9', '#f3c615', '#fcac5a']; return ['#5ff8b0', '#26cdb9', '#f3c615', '#fcac5a'];
} }
private getAvatarBorderColor(): string {
return '#000A'; // borderAvatarColor in themes.scss
}
} }

@ -5,6 +5,7 @@ interface Props {
diameter: number; diameter: number;
phoneNumber: string; phoneNumber: string;
colors: Array<string>; colors: Array<string>;
borderColor: string;
name?: string; name?: string;
} }
@ -41,7 +42,7 @@ export class AvatarPlaceHolder extends React.PureComponent<Props, State> {
return <></>; return <></>;
} }
const { colors, diameter, phoneNumber, name } = this.props; const { borderColor, colors, diameter, phoneNumber, name } = this.props;
const r = diameter / 2; const r = diameter / 2;
const initial = const initial =
getInitials(name)?.toLocaleUpperCase() || getInitials(name)?.toLocaleUpperCase() ||
@ -66,8 +67,8 @@ export class AvatarPlaceHolder extends React.PureComponent<Props, State> {
r={r} r={r}
fill={bgColor} fill={bgColor}
shape-rendering="geometricPrecision" shape-rendering="geometricPrecision"
// stroke="black" stroke={borderColor}
// stroke-width="1" stroke-width="1"
/> />
<text <text
font-size={fontSize} font-size={fontSize}

@ -0,0 +1,67 @@
import React from 'react';
import { Avatar } from '../Avatar';
import { LocalizerType } from '../../types/Util';
import { ConversationAttributes } from '../../../js/models/conversations';
interface Props {
size: number;
conversations: Array<ConversationAttributes>;
i18n: LocalizerType;
}
export class ClosedGroupAvatar extends React.PureComponent<Props> {
public render() {
const { conversations, size, i18n } = this.props;
if (conversations.length === 1) {
const conv = conversations[0];
return (
<Avatar
avatarPath={conv.avatarPath}
noteToSelf={conv.isMe}
conversationType="direct"
i18n={i18n}
name={name}
phoneNumber={conv.id}
profileName={conv.name}
size={size}
isPublic={false}
/>
);
} else if (conversations.length > 1) {
// in a closed group avatar, each visible avatar member size is 2/3 of the group avatar in size
const avatarsDiameter = 28; //FIXME audric (size * 2) / 3;
const conv1 = conversations[0];
const conv2 = conversations[1];
// use the 2 first members as group avatars
return (
<div className="module-avatar__icon-closed">
<Avatar
avatarPath={conv1.avatarPath}
noteToSelf={conv1.isMe}
conversationType="direct"
i18n={i18n}
name={name}
phoneNumber={conv1.id}
profileName={conv1.name}
size={avatarsDiameter}
isPublic={false}
/>
<Avatar
avatarPath={conv2.avatarPath}
noteToSelf={conv2.isMe}
conversationType="direct"
i18n={i18n}
name={name}
phoneNumber={conv2.id}
profileName={conv2.name}
size={avatarsDiameter}
isPublic={false}
/>
</div>
);
} else {
return <></>;
}
}
}

@ -1 +1,2 @@
export { AvatarPlaceHolder } from './AvatarPlaceHolder'; export { AvatarPlaceHolder } from './AvatarPlaceHolder';
export { ClosedGroupAvatar } from './ClosedGroupAvatar';

@ -10,7 +10,7 @@ import { Timestamp } from './conversation/Timestamp';
import { ContactName } from './conversation/ContactName'; import { ContactName } from './conversation/ContactName';
import { TypingAnimation } from './conversation/TypingAnimation'; import { TypingAnimation } from './conversation/TypingAnimation';
import { Colors, LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { import {
getBlockMenuItem, getBlockMenuItem,
getClearNicknameMenuItem, getClearNicknameMenuItem,
@ -20,6 +20,9 @@ import {
getInviteContactMenuItem, getInviteContactMenuItem,
getLeaveGroupMenuItem, getLeaveGroupMenuItem,
} from '../session/utils/Menu'; } from '../session/utils/Menu';
import { ConversationAttributes } from '../../js/models/conversations';
import { GroupUtils } from '../session/utils';
import { PubKey } from '../session/types';
export type PropsData = { export type PropsData = {
id: string; id: string;
@ -71,7 +74,32 @@ type PropsHousekeeping = {
type Props = PropsData & PropsHousekeeping; type Props = PropsData & PropsHousekeeping;
export class ConversationListItem extends React.PureComponent<Props> { type State = {
closedMemberConversations?: Array<ConversationAttributes>;
};
export class ConversationListItem extends React.PureComponent<Props, State> {
public constructor(props: Props) {
super(props);
this.state = { closedMemberConversations: undefined };
}
public componentDidMount() {
void this.fetchClosedConversationDetails();
}
public async fetchClosedConversationDetails() {
if (!this.props.isPublic && this.props.type === 'group') {
const groupId = this.props.phoneNumber;
const members = await GroupUtils.getGroupMembers(PubKey.cast(groupId));
const membersConvos = members.map(m =>
window.ConversationController.get(m.key)
);
this.setState({ closedMemberConversations: membersConvos });
}
}
public renderAvatar() { public renderAvatar() {
const { const {
avatarPath, avatarPath,
@ -82,10 +110,14 @@ export class ConversationListItem extends React.PureComponent<Props> {
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
isOnline, isPublic,
} = this.props; } = this.props;
const borderColor = isOnline ? Colors.ONLINE : Colors.OFFLINE; if (!isPublic && type === 'group') {
if (!this.state.closedMemberConversations) {
return <></>;
}
}
const iconSize = 36; const iconSize = 36;
@ -101,7 +133,8 @@ export class ConversationListItem extends React.PureComponent<Props> {
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
size={iconSize} size={iconSize}
borderColor={borderColor} isPublic={isPublic}
closedMemberConversations={this.state.closedMemberConversations}
/> />
</div> </div>
); );

@ -112,6 +112,7 @@ export class LeftPane extends React.Component<Props, State> {
const { const {
openConversationInternal, openConversationInternal,
conversations, conversations,
contacts,
searchResults, searchResults,
searchTerm, searchTerm,
isSecondaryDevice, isSecondaryDevice,
@ -119,12 +120,19 @@ export class LeftPane extends React.Component<Props, State> {
search, search,
clearSearch, clearSearch,
} = this.props; } = this.props;
// be sure to filter out secondary conversations
let filteredConversations = conversations;
if (conversations !== undefined) {
filteredConversations = conversations.filter(
conversation => !conversation.isSecondary
);
}
return ( return (
<LeftPaneMessageSection <LeftPaneMessageSection
contacts={this.props.contacts} contacts={contacts}
openConversationInternal={openConversationInternal} openConversationInternal={openConversationInternal}
conversations={conversations} conversations={filteredConversations}
searchResults={searchResults} searchResults={searchResults}
searchTerm={searchTerm} searchTerm={searchTerm}
isSecondaryDevice={isSecondaryDevice} isSecondaryDevice={isSecondaryDevice}

@ -79,7 +79,6 @@ export class UserDetailsDialog extends React.Component<Props, State> {
profileName={this.props.profileName} profileName={this.props.profileName}
size={size} size={size}
onAvatarClick={this.handleShowEnlargedDialog} onAvatarClick={this.handleShowEnlargedDialog}
borderWidth={size / 2}
/> />
); );
} }

@ -202,10 +202,9 @@ export class ConversationHeader extends React.Component<Props> {
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
isOnline, isPublic,
} = this.props; } = this.props;
const borderColor = isOnline ? Colors.ONLINE : Colors.OFFLINE_LIGHT;
const conversationType = isGroup ? 'group' : 'direct'; const conversationType = isGroup ? 'group' : 'direct';
return ( return (
@ -219,11 +218,10 @@ export class ConversationHeader extends React.Component<Props> {
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
size={28} size={28}
borderColor={borderColor}
borderWidth={0}
onAvatarClick={() => { onAvatarClick={() => {
this.onAvatarClickBound(phoneNumber); this.onAvatarClickBound(phoneNumber);
}} }}
isPublic={isPublic}
/> />
</span> </span>
); );

@ -101,6 +101,7 @@ class MemberItem extends React.Component<MemberItemProps> {
phoneNumber={this.props.member.authorPhoneNumber} phoneNumber={this.props.member.authorPhoneNumber}
profileName={this.props.member.authorProfileName} profileName={this.props.member.authorProfileName}
size={28} size={28}
isPublic={false}
/> />
); );
} }

@ -179,6 +179,7 @@ export class UpdateGroupNameDialog extends React.Component<Props, State> {
conversationType="group" conversationType="group"
i18n={this.props.i18n} i18n={this.props.i18n}
size={80} size={80}
isPublic={isPublic}
/> />
<div <div
className="image-upload-section" className="image-upload-section"

@ -43,6 +43,7 @@ export function renderAvatar({
i18n={i18n} i18n={i18n}
name={name} name={name}
size={size} size={size}
isPublic={false}
/> />
); );
} }

@ -77,19 +77,6 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
valuePasted: '', valuePasted: '',
}; };
const conversations = this.getCurrentConversations();
const realConversations: Array<ConversationListItemPropsType> = [];
if (conversations) {
conversations.forEach(conversation => {
const isRSS =
conversation.id &&
!!(conversation.id && conversation.id.match(/^rss:/));
return !isRSS && realConversations.push(conversation);
});
}
this.updateSearchBound = this.updateSearch.bind(this); this.updateSearchBound = this.updateSearch.bind(this);
this.handleOnPaste = this.handleOnPaste.bind(this); this.handleOnPaste = this.handleOnPaste.bind(this);
@ -112,29 +99,12 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
this.updateSearch(''); this.updateSearch('');
} }
public getCurrentConversations():
| Array<ConversationListItemPropsType>
| undefined {
const { conversations } = this.props;
let conversationList = conversations;
if (conversationList !== undefined) {
conversationList = conversationList.filter(
conversation => !conversation.isSecondary
);
}
return conversationList;
}
public renderRow = ({ public renderRow = ({
index, index,
key, key,
style, style,
}: RowRendererParamsType): JSX.Element => { }: RowRendererParamsType): JSX.Element => {
const { openConversationInternal } = this.props; const { conversations, openConversationInternal } = this.props;
const conversations = this.getCurrentConversations();
if (!conversations) { if (!conversations) {
throw new Error('renderRow: Tried to render without conversations'); throw new Error('renderRow: Tried to render without conversations');
@ -154,7 +124,11 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
}; };
public renderList(): JSX.Element | Array<JSX.Element | null> { public renderList(): JSX.Element | Array<JSX.Element | null> {
const { openConversationInternal, searchResults } = this.props; const {
conversations,
openConversationInternal,
searchResults,
} = this.props;
const contacts = searchResults?.contacts || []; const contacts = searchResults?.contacts || [];
if (searchResults) { if (searchResults) {
@ -168,7 +142,6 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
); );
} }
const conversations = this.getCurrentConversations();
if (!conversations) { if (!conversations) {
throw new Error( throw new Error(
'render: must provided conversations if no search results are provided' 'render: must provided conversations if no search results are provided'

@ -335,6 +335,7 @@ export class SessionGroupSettings extends React.Component<Props, any> {
phoneNumber={id} phoneNumber={id}
conversationType="group" conversationType="group"
size={80} size={80}
isPublic={isPublic}
/> />
<div className="invite-friends-container"> <div className="invite-friends-container">
{showInviteContacts && ( {showInviteContacts && (

@ -108,12 +108,8 @@ export const _getLeftPaneLists = (
const archivedConversations: Array<ConversationType> = []; const archivedConversations: Array<ConversationType> = [];
const allContacts: Array<ConversationType> = []; const allContacts: Array<ConversationType> = [];
const max = sorted.length;
let unreadCount = 0; let unreadCount = 0;
for (let conversation of sorted) {
for (let i = 0; i < max; i += 1) {
let conversation = sorted[i];
if (selectedConversation === conversation.id) { if (selectedConversation === conversation.id) {
conversation = { conversation = {
...conversation, ...conversation,

Loading…
Cancel
Save