From 1da4ae69c2f5bb99b2d722a5e93cf6e71db0d432 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 10 Sep 2020 15:52:36 +1000 Subject: [PATCH 1/9] render NoteToSelf list item as a normal conversation --- js/background.js | 8 ++++++++ js/models/conversations.js | 2 +- stylesheets/_modules.scss | 5 ----- ts/components/ConversationListItem.tsx | 7 ++++--- ts/components/session/ActionsPanel.tsx | 1 + ts/receiver/dataMessage.ts | 2 +- ts/util/getInitials.ts | 2 +- ts/window.d.ts | 1 + 8 files changed, 17 insertions(+), 11 deletions(-) diff --git a/js/background.js b/js/background.js index 8707c3a8b..094d63fee 100644 --- a/js/background.js +++ b/js/background.js @@ -550,6 +550,13 @@ window.dispatchEvent(new Event('storage_ready')); window.log.info('Cleanup: starting...'); + window.getOurDisplayName = () => { + const ourNumber = window.storage.get('primaryDevicePubKey'); + const conversation = ConversationController.get(ourNumber, 'private'); + const profile = conversation.getLokiProfile(); + return profile && profile.displayName; + }; + const results = await Promise.all([ window.Signal.Data.getOutgoingWithoutExpiresAt({ MessageCollection: Whisper.MessageCollection, @@ -690,6 +697,7 @@ window.showSeedDialog = window.owsDesktopApp.appView.showSeedDialog; window.showPasswordDialog = window.owsDesktopApp.appView.showPasswordDialog; + window.showEditProfileDialog = async callback => { const ourNumber = window.storage.get('primaryDevicePubKey'); const conversation = await ConversationController.getOrCreateAndWait( diff --git a/js/models/conversations.js b/js/models/conversations.js index 578604373..e2be0e221 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1995,7 +1995,7 @@ }); } - // if set to null, it will show a jazzIcon + // if set to null, it will show a placeholder with color and first letter await this.setProfileAvatar({ path: newProfile.avatar }); await this.updateProfileName(); diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 2d603f543..c595ca77b 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -1517,10 +1517,6 @@ background-color: $color-dark-70; } - .module-avatar { - background-color: $color-dark-85; - } - &--is-blocked { @include themify($themes) { border-left: 4px solid themed('destructive') !important; @@ -1579,7 +1575,6 @@ text-overflow: ellipsis; font-weight: 300; - color: $color-gray-05; } .module-conversation-list-item__header__name--with-unread { diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 0b2a71ef5..9e90ec3a4 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -139,7 +139,7 @@ export class ConversationListItem extends React.PureComponent { : null )} > - {isMe ? i18n('noteToSelf') : this.renderUser()} + {this.renderUser()} {this.renderUnread()} { @@ -347,18 +347,19 @@ export class ConversationListItem extends React.PureComponent { } private renderUser() { - const { name, phoneNumber, profileName } = this.props; + const { name, phoneNumber, profileName, isMe, i18n } = this.props; const shortenedPubkey = window.shortenPubkey(phoneNumber); const displayedPubkey = profileName ? shortenedPubkey : phoneNumber; + const displayName = isMe ? i18n('noteToSelf') : profileName; return (
{ phoneNumber={window.storage.get('primaryDevicePubKey')} size={28} onAvatarClick={handleClick} + profileName={window.getOurDisplayName()} /> ); } diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index ac0787d2a..907cd8daa 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -43,7 +43,7 @@ export async function updateProfile( isRaw: true, }); - // null => use jazzicon + // null => use placeholder with color and first letter let path = null; if (profileKey) { // Convert profileKey to ArrayBuffer, if needed diff --git a/ts/util/getInitials.ts b/ts/util/getInitials.ts index 1b38c0984..8f7e30b5e 100644 --- a/ts/util/getInitials.ts +++ b/ts/util/getInitials.ts @@ -17,5 +17,5 @@ export function getInitials(name?: string): string | undefined { return; } - return initials.slice(0, 2).join(''); + return initials[0]; } diff --git a/ts/window.d.ts b/ts/window.d.ts index 52a3f7e59..51387b0d3 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -80,6 +80,7 @@ declare global { setSettingValue: any; shortenPubkey: any; showEditProfileDialog: any; + getOurDisplayName: () => string | undefined; showPasswordDialog: any; showSeedDialog: any; storage: any; From 28eec84783455f7d660f82dc3c1e9620570a0fcd Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 10 Sep 2020 15:53:36 +1000 Subject: [PATCH 2/9] remove JazzIcon and use instead a colored circle with Initial --- ts/components/Avatar.tsx | 73 +++----- .../AvatarPlaceHolder/AvatarPlaceHolder.tsx | 101 +++++++++++ ts/components/AvatarPlaceHolder/index.ts | 1 + ts/components/JazzIcon/JazzIcon.tsx | 166 ------------------ ts/components/JazzIcon/Paper.tsx | 25 --- ts/components/JazzIcon/RNG.tsx | 21 --- ts/components/JazzIcon/index.tsx | 2 - 7 files changed, 122 insertions(+), 267 deletions(-) create mode 100644 ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx create mode 100644 ts/components/AvatarPlaceHolder/index.ts delete mode 100644 ts/components/JazzIcon/JazzIcon.tsx delete mode 100644 ts/components/JazzIcon/Paper.tsx delete mode 100644 ts/components/JazzIcon/RNG.tsx delete mode 100644 ts/components/JazzIcon/index.tsx diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 6adb05d16..4c85c5e65 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -1,9 +1,9 @@ import React from 'react'; import classNames from 'classnames'; -import { JazzIcon } from './JazzIcon'; import { getInitials } from '../util/getInitials'; import { LocalizerType } from '../types/Util'; +import { AvatarPlaceHolder } from './AvatarPlaceHolder'; interface Props { avatarPath?: string; @@ -48,30 +48,25 @@ export class Avatar extends React.PureComponent { } public renderIdenticon() { - const { phoneNumber, borderColor, borderWidth, size } = this.props; + const { phoneNumber, size, name, profileName } = this.props; if (!phoneNumber) { - return this.renderNoImage(); + throw new Error('Empty phoneNumber for identifcon'); } - const borderStyle = this.getBorderStyle(borderColor, borderWidth); - - // Generate the seed - const hash = phoneNumber.substring(0, 12); - const seed = parseInt(hash, 16) || 1234; - - return ; + const userName = profileName || name; + return ( + + ); } public renderImage() { - const { - avatarPath, - name, - phoneNumber, - profileName, - borderColor, - borderWidth, - } = this.props; + const { avatarPath, name, phoneNumber, profileName } = this.props; const { imageBroken } = this.state; if (!avatarPath || imageBroken) { @@ -82,11 +77,8 @@ export class Avatar extends React.PureComponent { !name && profileName ? ` ~${profileName}` : '' }`; - const borderStyle = this.getBorderStyle(borderColor, borderWidth); - return ( {window.i18n('contactAvatarAlt', { } public renderNoImage() { - const { - conversationType, - name, - noteToSelf, - size, - borderColor, - borderWidth, - } = this.props; + const { conversationType, name, noteToSelf, size } = this.props; const initials = getInitials(name); const isGroup = conversationType === 'group'; @@ -119,8 +104,6 @@ export class Avatar extends React.PureComponent { ); } - const borderStyle = this.getBorderStyle(borderColor, borderWidth); - if (!isGroup && initials) { return (
{ 'module-avatar__label', `module-avatar__label--${size}` )} - style={borderStyle} > {initials}
@@ -142,24 +124,17 @@ export class Avatar extends React.PureComponent { `module-avatar__icon--${conversationType}`, `module-avatar__icon--${size}` )} - style={borderStyle} /> ); } public render() { - const { - avatarPath, - color, - size, - noteToSelf, - conversationType, - } = this.props; + const { avatarPath, color, size, conversationType } = this.props; const { imageBroken } = this.state; // If it's a direct conversation then we must have an identicon const hasAvatar = avatarPath || conversationType === 'direct'; - const hasImage = !noteToSelf && hasAvatar && !imageBroken; + const hasImage = hasAvatar && !imageBroken; if ( size !== 28 && @@ -207,17 +182,9 @@ export class Avatar extends React.PureComponent { : this.renderIdenticon(); } - private getBorderStyle(_color?: string, _width?: number) { - //const borderWidth = typeof width === 'number' ? width : 3; - - // no border at all for now - return undefined; - /* return color - ? { - borderColor: color, - borderStyle: 'solid', - borderWidth: borderWidth, - } - : undefined; */ + private getAvatarColors(): Array { + // const theme = window.Events.getThemedSettings(); + // defined in session-android as `profile_picture_placeholder_colors` + return ['#5ff8b0', '#26cdb9', '#f3c615', '#fcac5a']; } } diff --git a/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx b/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx new file mode 100644 index 000000000..da9929a43 --- /dev/null +++ b/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { getInitials } from '../../util/getInitials'; + +interface Props { + diameter: number; + phoneNumber: string; + colors: Array; + name?: string; +} + +interface State { + sha512Seed?: string; +} + +export class AvatarPlaceHolder extends React.PureComponent { + public constructor(props: Props) { + super(props); + + this.state = { + sha512Seed: undefined, + }; + } + + public componentDidMount() { + void this.sha512(this.props.phoneNumber).then((sha512Seed: string) => { + this.setState({ sha512Seed }); + }); + } + + public componentDidUpdate(prevProps: Props, prevState: State) { + if (this.props.phoneNumber === prevProps.phoneNumber) { + return; + } + void this.sha512(this.props.phoneNumber).then((sha512Seed: string) => { + this.setState({ sha512Seed }); + }); + } + + public render() { + if (!this.state.sha512Seed) { + return <>; + } + + const { colors, diameter, phoneNumber, name } = this.props; + const r = diameter / 2; + const initial = + getInitials(name)?.toLocaleUpperCase() || + getInitials(phoneNumber)?.toLocaleUpperCase() || + '0'; + const viewBox = `0 0 ${diameter} ${diameter}`; + const fontSize = diameter * 0.5; + + // Generate the seed simulate the .hashCode as Java + const hash = parseInt(this.state.sha512Seed.substring(0, 12), 16) || 0; + + const bgColorIndex = hash % colors.length; + + const bgColor = colors[bgColorIndex]; + + return ( + + + + + {initial} + + + + ); + } + + private async sha512(str: string) { + // tslint:disable-next-line: await-promise + const buf = await crypto.subtle.digest( + 'SHA-512', + new TextEncoder().encode(str) + ); + + // tslint:disable: prefer-template restrict-plus-operands + return Array.prototype.map + .call(new Uint8Array(buf), (x: any) => ('00' + x.toString(16)).slice(-2)) + .join(''); + } +} diff --git a/ts/components/AvatarPlaceHolder/index.ts b/ts/components/AvatarPlaceHolder/index.ts new file mode 100644 index 000000000..5b9cfd23a --- /dev/null +++ b/ts/components/AvatarPlaceHolder/index.ts @@ -0,0 +1 @@ +export { AvatarPlaceHolder } from './AvatarPlaceHolder'; diff --git a/ts/components/JazzIcon/JazzIcon.tsx b/ts/components/JazzIcon/JazzIcon.tsx deleted file mode 100644 index 3cc17044b..000000000 --- a/ts/components/JazzIcon/JazzIcon.tsx +++ /dev/null @@ -1,166 +0,0 @@ -// Modified from https://github.com/redlanta/react-jazzicon - -import React from 'react'; -import Color from 'color'; -import { Paper } from './Paper'; -import { RNG } from './RNG'; - -const defaultColors = [ - '#01888c', // teal - '#fc7500', // bright orange - '#034f5d', // dark teal - '#E784BA', // light pink - '#81C8B6', // bright green - '#c7144c', // raspberry - '#f3c100', // goldenrod - '#1598f2', // lightning blue - '#2465e1', // sail blue - '#f19e02', // gold -]; - -const isColor = (str: string) => /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(str); -const isColors = (arr: Array) => { - if (!Array.isArray(arr)) { - return false; - } - - if (arr.every(value => typeof value === 'string' && isColor(value))) { - return true; - } - - return false; -}; - -interface Props { - diameter: number; - seed: number; - paperStyles?: Object; - svgStyles?: Object; - shapeCount?: number; - wobble?: number; - colors?: Array; -} - -// tslint:disable-next-line no-http-string -const svgns = 'http://www.w3.org/2000/svg'; -const shapeCount = 4; -const wobble = 30; - -export class JazzIcon extends React.PureComponent { - public render() { - const { - colors: customColors, - diameter, - paperStyles, - seed, - svgStyles, - } = this.props; - - const generator = new RNG(seed); - - const colors = customColors || defaultColors; - - const newColours = this.hueShift( - this.colorsForIcon(colors).slice(), - generator - ); - const shapesArr = Array(shapeCount).fill(null); - const shuffledColours = this.shuffleArray(newColours, generator); - - return ( - - - {shapesArr.map((_, i) => - this.genShape( - shuffledColours[i + 1], - diameter, - i, - shapeCount - 1, - generator - ) - )} - - - ); - } - - private hueShift(colors: Array, generator: RNG) { - const amount = generator.random() * 30 - wobble / 2; - - return colors.map(hex => - Color(hex) - .rotate(amount) - .hex() - ); - } - - private genShape( - colour: string, - diameter: number, - i: number, - total: number, - generator: RNG - ) { - const center = diameter / 2; - const firstRot = generator.random(); - const angle = Math.PI * 2 * firstRot; - const velocity = - (diameter / total) * generator.random() + (i * diameter) / total; - const tx = Math.cos(angle) * velocity; - const ty = Math.sin(angle) * velocity; - const translate = `translate(${tx} ${ty})`; - - // Third random is a shape rotation on top of all of that. - const secondRot = generator.random(); - const rot = firstRot * 360 + secondRot * 180; - const rotate = `rotate(${rot.toFixed(1)} ${center} ${center})`; - const transform = `${translate} ${rotate}`; - - return ( - - ); - } - - private colorsForIcon(arr: Array) { - if (isColors(arr)) { - return arr; - } - - return defaultColors; - } - - private shuffleArray(array: Array, generator: RNG) { - let currentIndex = array.length; - const newArray = [...array]; - - // While there remain elements to shuffle... - while (currentIndex > 0) { - // Pick a remaining element... - const randomIndex = generator.next() % currentIndex; - currentIndex -= 1; - // And swap it with the current element. - const temporaryValue = newArray[currentIndex]; - newArray[currentIndex] = newArray[randomIndex]; - newArray[randomIndex] = temporaryValue; - } - - return newArray; - } -} diff --git a/ts/components/JazzIcon/Paper.tsx b/ts/components/JazzIcon/Paper.tsx deleted file mode 100644 index ee3791637..000000000 --- a/ts/components/JazzIcon/Paper.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -const styles = { - borderRadius: '50%', - display: 'inline-block', - margin: 0, - overflow: 'hidden', - padding: 0, -}; - -// @ts-ignore -export const Paper = ({ children, color, diameter, style: styleOverrides }) => ( -
- {children} -
-); diff --git a/ts/components/JazzIcon/RNG.tsx b/ts/components/JazzIcon/RNG.tsx deleted file mode 100644 index b6c18ac6b..000000000 --- a/ts/components/JazzIcon/RNG.tsx +++ /dev/null @@ -1,21 +0,0 @@ -export class RNG { - private _seed: number; - constructor(seed: number) { - this._seed = seed % 2147483647; - if (this._seed <= 0) { - this._seed += 2147483646; - } - } - - public next() { - return (this._seed = (this._seed * 16807) % 2147483647); - } - - public nextFloat() { - return (this.next() - 1) / 2147483646; - } - - public random() { - return this.nextFloat(); - } -} diff --git a/ts/components/JazzIcon/index.tsx b/ts/components/JazzIcon/index.tsx deleted file mode 100644 index 204774dc6..000000000 --- a/ts/components/JazzIcon/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import { JazzIcon } from './JazzIcon'; -export { JazzIcon }; From bc8999e0b61422dc58d143ef35466c709ac6f282 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 10 Sep 2020 16:16:46 +1000 Subject: [PATCH 3/9] fix tests --- test/views/inbox_view_test.js | 1 + ts/components/Avatar.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/views/inbox_view_test.js b/test/views/inbox_view_test.js index dea936399..c16b24ef4 100644 --- a/test/views/inbox_view_test.js +++ b/test/views/inbox_view_test.js @@ -22,6 +22,7 @@ describe('InboxView', () => { textsecure.storage.user.getNumber(), 'private' ); + window.getOurDisplayName = () => 'display name test'; inboxView = new Whisper.InboxView({ model: {}, window, diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 4c85c5e65..28762e5e0 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -51,7 +51,8 @@ export class Avatar extends React.PureComponent { const { phoneNumber, size, name, profileName } = this.props; if (!phoneNumber) { - throw new Error('Empty phoneNumber for identifcon'); + window.log.error('Empty phoneNumber for identicon'); + return <>; } const userName = profileName || name; From 35ea6af27fa0041d813cf48e94662c676f216de0 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 11 Sep 2020 15:06:13 +1000 Subject: [PATCH 4/9] Add group avatar as on mobile: with multiple group members avatar --- js/models/conversations.d.ts | 2 + stylesheets/_avatar.scss | 11 +-- stylesheets/themes.scss | 13 ++-- ts/components/Avatar.tsx | 38 +++++++++-- .../AvatarPlaceHolder/AvatarPlaceHolder.tsx | 7 +- .../AvatarPlaceHolder/ClosedGroupAvatar.tsx | 67 +++++++++++++++++++ ts/components/AvatarPlaceHolder/index.ts | 1 + ts/components/ConversationListItem.tsx | 43 ++++++++++-- ts/components/LeftPane.tsx | 12 +++- ts/components/UserDetailsDialog.tsx | 1 - .../conversation/ConversationHeader.tsx | 6 +- ts/components/conversation/MemberList.tsx | 1 + .../conversation/UpdateGroupNameDialog.tsx | 1 + ts/components/conversation/_contactUtil.tsx | 1 + .../session/LeftPaneMessageSection.tsx | 39 ++--------- .../session/SessionGroupSettings.tsx | 1 + ts/state/selectors/conversations.ts | 6 +- 17 files changed, 180 insertions(+), 70 deletions(-) create mode 100644 ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx diff --git a/js/models/conversations.d.ts b/js/models/conversations.d.ts index cc0432437..4556d85d2 100644 --- a/js/models/conversations.d.ts +++ b/js/models/conversations.d.ts @@ -15,6 +15,8 @@ interface ConversationAttributes { timestamp: number; // timestamp of what? groupAdmins?: Array; isKickedFromGroup?: boolean; + avatarPath?: string; + isMe?: boolean; } export interface ConversationModel diff --git a/stylesheets/_avatar.scss b/stylesheets/_avatar.scss index 02efb2fe3..caa899939 100644 --- a/stylesheets/_avatar.scss +++ b/stylesheets/_avatar.scss @@ -9,6 +9,7 @@ img { object-fit: cover; border-radius: 50%; + border: 1px solid $borderAvatarColor; } } @@ -167,8 +168,6 @@ @include color-svg('../images/note-28.svg', $color-white); } -// Module: Avatar - .module-avatar__label { color: $color-gray-05; } @@ -189,8 +188,10 @@ background-color: $color-gray-75; } -.module-avatar--no-image { - @include themify($themes) { - background-color: themed('steelColorShade'); +.module-avatar__icon-closed { + .module-avatar:last-child { + position: absolute; + right: 0px; + bottom: 0px; } } diff --git a/stylesheets/themes.scss b/stylesheets/themes.scss index 45abec93d..cf5b80caf 100644 --- a/stylesheets/themes.scss +++ b/stylesheets/themes.scss @@ -6,6 +6,11 @@ $destructive: #ff453a; $accentLightTheme: #00e97b; $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: ( light: ( accent: $accentLightTheme, @@ -40,7 +45,7 @@ $themes: ( conversationItemHasUnread: #fcfcfc, conversationItemSelected: #f0f0f0, clickableHovered: #dfdfdf, - sessionBorder: 1px solid #f1f1f1, + sessionBorder: 1px solid $borderLightTheme, sessionUnreadBorder: 4px solid $accentLightTheme, leftpaneOverlayBackground: $white, // scrollbars @@ -52,8 +57,6 @@ $themes: ( // context menu contextMenuBackground: #f5f5f5, filterSessionText: brightness(0) saturate(100%), - steelColor: #6b6b78, - steelColorShade: #5a5a63, lastSeenIndicatorColor: #62656a, lastSeenIndicatorTextColor: #070c14, quoteBottomBarBackground: #f0f0f0, @@ -91,7 +94,7 @@ $themes: ( conversationItemHasUnread: #2c2c2c, conversationItemSelected: #404040, clickableHovered: #414347, - sessionBorder: 1px solid rgba($white, 0.06), + sessionBorder: 1px solid $borderDarkTheme, sessionUnreadBorder: 4px solid $accentDarkTheme, leftpaneOverlayBackground: linear-gradient(180deg, #171717 0%, #121212 100%), // scrollbars @@ -103,8 +106,6 @@ $themes: ( // context menu contextMenuBackground: #212121, filterSessionText: none, - steelColor: #6b6b78, - steelColorShade: #5a5a63, lastSeenIndicatorColor: #353535, lastSeenIndicatorTextColor: #a8a9aa, quoteBottomBarBackground: #404040, diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 28762e5e0..0ff7cb3f6 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -3,19 +3,20 @@ import classNames from 'classnames'; import { getInitials } from '../util/getInitials'; import { LocalizerType } from '../types/Util'; -import { AvatarPlaceHolder } from './AvatarPlaceHolder'; +import { AvatarPlaceHolder, ClosedGroupAvatar } from './AvatarPlaceHolder'; +import { ConversationAttributes } from '../../js/models/conversations'; interface Props { avatarPath?: string; color?: string; conversationType: 'group' | 'direct'; + isPublic?: boolean; noteToSelf?: boolean; name?: string; phoneNumber?: string; profileName?: string; size: number; - borderColor?: string; - borderWidth?: number; + closedMemberConversations?: Array; i18n?: LocalizerType; onAvatarClick?: () => void; } @@ -40,8 +41,9 @@ export class Avatar extends React.PureComponent { } public handleImageError() { - // tslint:disable-next-line no-console - console.log('Avatar: Image failed to load; failing over to placeholder'); + window.log.warn( + 'Avatar: Image failed to load; failing over to placeholder' + ); this.setState({ imageBroken: true, }); @@ -62,6 +64,7 @@ export class Avatar extends React.PureComponent { diameter={size} name={userName} colors={this.getAvatarColors()} + borderColor={this.getAvatarBorderColor()} /> ); } @@ -88,7 +91,15 @@ export class Avatar extends React.PureComponent { } public renderNoImage() { - const { conversationType, name, noteToSelf, size } = this.props; + const { + conversationType, + closedMemberConversations, + isPublic, + name, + noteToSelf, + size, + i18n, + } = this.props; const initials = getInitials(name); const isGroup = conversationType === 'group'; @@ -118,6 +129,17 @@ export class Avatar extends React.PureComponent { ); } + if (isGroup && !isPublic && closedMemberConversations) { + const forcedI18n = i18n || window.i18n; + return ( + + ); + } + return (
{ // defined in session-android as `profile_picture_placeholder_colors` return ['#5ff8b0', '#26cdb9', '#f3c615', '#fcac5a']; } + + private getAvatarBorderColor(): string { + return '#000A'; // borderAvatarColor in themes.scss + } } diff --git a/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx b/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx index da9929a43..056be82bd 100644 --- a/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx +++ b/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx @@ -5,6 +5,7 @@ interface Props { diameter: number; phoneNumber: string; colors: Array; + borderColor: string; name?: string; } @@ -41,7 +42,7 @@ export class AvatarPlaceHolder extends React.PureComponent { return <>; } - const { colors, diameter, phoneNumber, name } = this.props; + const { borderColor, colors, diameter, phoneNumber, name } = this.props; const r = diameter / 2; const initial = getInitials(name)?.toLocaleUpperCase() || @@ -66,8 +67,8 @@ export class AvatarPlaceHolder extends React.PureComponent { r={r} fill={bgColor} shape-rendering="geometricPrecision" - // stroke="black" - // stroke-width="1" + stroke={borderColor} + stroke-width="1" /> ; + i18n: LocalizerType; +} + +export class ClosedGroupAvatar extends React.PureComponent { + public render() { + const { conversations, size, i18n } = this.props; + + if (conversations.length === 1) { + const conv = conversations[0]; + return ( + + ); + } 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 ( +
+ + +
+ ); + } else { + return <>; + } + } +} diff --git a/ts/components/AvatarPlaceHolder/index.ts b/ts/components/AvatarPlaceHolder/index.ts index 5b9cfd23a..f6f819bd6 100644 --- a/ts/components/AvatarPlaceHolder/index.ts +++ b/ts/components/AvatarPlaceHolder/index.ts @@ -1 +1,2 @@ export { AvatarPlaceHolder } from './AvatarPlaceHolder'; +export { ClosedGroupAvatar } from './ClosedGroupAvatar'; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 9e90ec3a4..a8bfe939f 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -10,7 +10,7 @@ import { Timestamp } from './conversation/Timestamp'; import { ContactName } from './conversation/ContactName'; import { TypingAnimation } from './conversation/TypingAnimation'; -import { Colors, LocalizerType } from '../types/Util'; +import { LocalizerType } from '../types/Util'; import { getBlockMenuItem, getClearNicknameMenuItem, @@ -20,6 +20,9 @@ import { getInviteContactMenuItem, getLeaveGroupMenuItem, } from '../session/utils/Menu'; +import { ConversationAttributes } from '../../js/models/conversations'; +import { GroupUtils } from '../session/utils'; +import { PubKey } from '../session/types'; export type PropsData = { id: string; @@ -71,7 +74,32 @@ type PropsHousekeeping = { type Props = PropsData & PropsHousekeeping; -export class ConversationListItem extends React.PureComponent { +type State = { + closedMemberConversations?: Array; +}; + +export class ConversationListItem extends React.PureComponent { + 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() { const { avatarPath, @@ -82,10 +110,14 @@ export class ConversationListItem extends React.PureComponent { name, phoneNumber, profileName, - isOnline, + isPublic, } = this.props; - const borderColor = isOnline ? Colors.ONLINE : Colors.OFFLINE; + if (!isPublic && type === 'group') { + if (!this.state.closedMemberConversations) { + return <>; + } + } const iconSize = 36; @@ -101,7 +133,8 @@ export class ConversationListItem extends React.PureComponent { phoneNumber={phoneNumber} profileName={profileName} size={iconSize} - borderColor={borderColor} + isPublic={isPublic} + closedMemberConversations={this.state.closedMemberConversations} />
); diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index a4c4c600c..dbf97a13e 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -112,6 +112,7 @@ export class LeftPane extends React.Component { const { openConversationInternal, conversations, + contacts, searchResults, searchTerm, isSecondaryDevice, @@ -119,12 +120,19 @@ export class LeftPane extends React.Component { search, clearSearch, } = this.props; + // be sure to filter out secondary conversations + let filteredConversations = conversations; + if (conversations !== undefined) { + filteredConversations = conversations.filter( + conversation => !conversation.isSecondary + ); + } return ( { profileName={this.props.profileName} size={size} onAvatarClick={this.handleShowEnlargedDialog} - borderWidth={size / 2} /> ); } diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index c097a2ad1..451e2576c 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -202,10 +202,9 @@ export class ConversationHeader extends React.Component { name, phoneNumber, profileName, - isOnline, + isPublic, } = this.props; - const borderColor = isOnline ? Colors.ONLINE : Colors.OFFLINE_LIGHT; const conversationType = isGroup ? 'group' : 'direct'; return ( @@ -219,11 +218,10 @@ export class ConversationHeader extends React.Component { phoneNumber={phoneNumber} profileName={profileName} size={28} - borderColor={borderColor} - borderWidth={0} onAvatarClick={() => { this.onAvatarClickBound(phoneNumber); }} + isPublic={isPublic} /> ); diff --git a/ts/components/conversation/MemberList.tsx b/ts/components/conversation/MemberList.tsx index 69a7c2d58..2d494a54c 100644 --- a/ts/components/conversation/MemberList.tsx +++ b/ts/components/conversation/MemberList.tsx @@ -101,6 +101,7 @@ class MemberItem extends React.Component { phoneNumber={this.props.member.authorPhoneNumber} profileName={this.props.member.authorProfileName} size={28} + isPublic={false} /> ); } diff --git a/ts/components/conversation/UpdateGroupNameDialog.tsx b/ts/components/conversation/UpdateGroupNameDialog.tsx index ad7f380e7..b0aa7a34d 100644 --- a/ts/components/conversation/UpdateGroupNameDialog.tsx +++ b/ts/components/conversation/UpdateGroupNameDialog.tsx @@ -179,6 +179,7 @@ export class UpdateGroupNameDialog extends React.Component { conversationType="group" i18n={this.props.i18n} size={80} + isPublic={isPublic} />
); } diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index e66eb8e56..a7c1a405d 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -77,19 +77,6 @@ export class LeftPaneMessageSection extends React.Component { valuePasted: '', }; - const conversations = this.getCurrentConversations(); - - const realConversations: Array = []; - 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.handleOnPaste = this.handleOnPaste.bind(this); @@ -112,29 +99,12 @@ export class LeftPaneMessageSection extends React.Component { this.updateSearch(''); } - public getCurrentConversations(): - | Array - | undefined { - const { conversations } = this.props; - - let conversationList = conversations; - if (conversationList !== undefined) { - conversationList = conversationList.filter( - conversation => !conversation.isSecondary - ); - } - - return conversationList; - } - public renderRow = ({ index, key, style, }: RowRendererParamsType): JSX.Element => { - const { openConversationInternal } = this.props; - - const conversations = this.getCurrentConversations(); + const { conversations, openConversationInternal } = this.props; if (!conversations) { throw new Error('renderRow: Tried to render without conversations'); @@ -154,7 +124,11 @@ export class LeftPaneMessageSection extends React.Component { }; public renderList(): JSX.Element | Array { - const { openConversationInternal, searchResults } = this.props; + const { + conversations, + openConversationInternal, + searchResults, + } = this.props; const contacts = searchResults?.contacts || []; if (searchResults) { @@ -168,7 +142,6 @@ export class LeftPaneMessageSection extends React.Component { ); } - const conversations = this.getCurrentConversations(); if (!conversations) { throw new Error( 'render: must provided conversations if no search results are provided' diff --git a/ts/components/session/SessionGroupSettings.tsx b/ts/components/session/SessionGroupSettings.tsx index c94f09f9a..4b2f0f196 100644 --- a/ts/components/session/SessionGroupSettings.tsx +++ b/ts/components/session/SessionGroupSettings.tsx @@ -335,6 +335,7 @@ export class SessionGroupSettings extends React.Component { phoneNumber={id} conversationType="group" size={80} + isPublic={isPublic} />
{showInviteContacts && ( diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index f2189f24e..63b86af9e 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -108,12 +108,8 @@ export const _getLeftPaneLists = ( const archivedConversations: Array = []; const allContacts: Array = []; - const max = sorted.length; let unreadCount = 0; - - for (let i = 0; i < max; i += 1) { - let conversation = sorted[i]; - + for (let conversation of sorted) { if (selectedConversation === conversation.id) { conversation = { ...conversation, From 9584d0a1afe3ba35deaecab3bb385991ca4c34a1 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 14 Sep 2020 14:29:37 +1000 Subject: [PATCH 5/9] fix closed group avatar for conversationlistItem --- stylesheets/_avatar.scss | 1 + stylesheets/themes.scss | 2 +- ts/components/Avatar.tsx | 2 +- .../AvatarPlaceHolder/ClosedGroupAvatar.tsx | 25 ++++++++++++++++++- ts/components/ConversationListItem.tsx | 20 +++++++-------- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/stylesheets/_avatar.scss b/stylesheets/_avatar.scss index caa899939..ba61d4076 100644 --- a/stylesheets/_avatar.scss +++ b/stylesheets/_avatar.scss @@ -55,6 +55,7 @@ @include color-svg('../images/profile-individual.svg', $color-white); } +.module-avatar__icon-closed .module-avatar--28, .module-avatar--28 { height: 28px; width: 28px; diff --git a/stylesheets/themes.scss b/stylesheets/themes.scss index cf5b80caf..0c9835a55 100644 --- a/stylesheets/themes.scss +++ b/stylesheets/themes.scss @@ -9,7 +9,7 @@ $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 +$borderAvatarColor: #00000059; // search for references on ts TODO: make this exposed on ts $themes: ( light: ( diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 0ff7cb3f6..d354cacc1 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -212,6 +212,6 @@ export class Avatar extends React.PureComponent { } private getAvatarBorderColor(): string { - return '#000A'; // borderAvatarColor in themes.scss + return '#00000059'; // borderAvatarColor in themes.scss } } diff --git a/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx b/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx index 63ee61afc..b4a5b5902 100644 --- a/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx +++ b/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx @@ -30,7 +30,30 @@ export class ClosedGroupAvatar extends React.PureComponent { ); } 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; + // Always use the size directly under the one requested + let avatarsDiameter = 0; + switch (size) { + case 36: { + avatarsDiameter = 28; + break; + } + case 48: { + avatarsDiameter = 36; + break; + } + case 80: { + avatarsDiameter = 48; + break; + } + case 300: { + avatarsDiameter = 80; + break; + } + default: + throw new Error( + `Invalid size request for closed group avatar: ${size}` + ); + } const conv1 = conversations[0]; const conv2 = conversations[1]; // use the 2 first members as group avatars diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index a8bfe939f..a87b219e9 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -23,6 +23,7 @@ import { import { ConversationAttributes } from '../../js/models/conversations'; import { GroupUtils } from '../session/utils'; import { PubKey } from '../session/types'; +import { UserUtil } from '../util'; export type PropsData = { id: string; @@ -91,11 +92,15 @@ export class ConversationListItem extends React.PureComponent { 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) + let members = await GroupUtils.getGroupMembers(PubKey.cast(groupId)); + const ourPrimary = await UserUtil.getPrimary(); + members = members.filter(m => m.key !== ourPrimary.key); + members.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + const membersConvos = members.map( + m => window.ConversationController.get(m.key).cachedProps ); - + // no need to forward more than 2 conversation for rendering the group avatar + membersConvos.slice(0, 2); this.setState({ closedMemberConversations: membersConvos }); } } @@ -113,12 +118,6 @@ export class ConversationListItem extends React.PureComponent { isPublic, } = this.props; - if (!isPublic && type === 'group') { - if (!this.state.closedMemberConversations) { - return <>; - } - } - const iconSize = 36; return ( @@ -341,7 +340,6 @@ export class ConversationListItem extends React.PureComponent { style, mentionedUs, } = this.props; - const triggerId = `conversation-item-${phoneNumber}-ctxmenu`; return ( From b845ba96423d2e5b8a2d838c044876130890e9ed Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 14 Sep 2020 16:06:24 +1000 Subject: [PATCH 6/9] use a HOC to fetch closed group conversations for avatar rendering --- js/modules/signal.js | 8 +-- js/views/conversation_view.js | 4 +- ts/components/ConversationListItem.tsx | 40 +++---------- ts/components/SearchResults.tsx | 6 +- .../conversation/ConversationHeader.tsx | 12 +++- .../session/LeftPaneContactSection.tsx | 4 +- .../session/LeftPaneMessageSection.tsx | 4 +- .../session/SessionGroupSettings.tsx | 10 +++- .../usingClosedConversationDetails.tsx | 60 +++++++++++++++++++ 9 files changed, 101 insertions(+), 47 deletions(-) create mode 100644 ts/components/session/usingClosedConversationDetails.tsx diff --git a/js/modules/signal.js b/js/modules/signal.js index a5c459f29..6a4206726 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -30,10 +30,10 @@ const { const { ContactListItem } = require('../../ts/components/ContactListItem'); const { ContactName } = require('../../ts/components/conversation/ContactName'); const { - ConversationHeader, + ConversationHeaderWithDetails, } = require('../../ts/components/conversation/ConversationHeader'); const { - SessionGroupSettings, + SessionGroupSettingsWithDetails, } = require('../../ts/components/session/SessionGroupSettings'); const { EmbeddedContact, @@ -275,8 +275,8 @@ exports.setup = (options = {}) => { ContactDetail, ContactListItem, ContactName, - ConversationHeader, - SessionGroupSettings, + ConversationHeaderWithDetails, + SessionGroupSettingsWithDetails, SettingsView, EmbeddedContact, Emojify, diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 097556d16..a63b0e8d9 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -325,7 +325,7 @@ }; this.titleView = new Whisper.ReactWrapperView({ className: 'title-wrapper', - Component: window.Signal.Components.ConversationHeader, + Component: window.Signal.Components.ConversationHeaderWithDetails, props: getHeaderProps(), }); this.updateHeader = () => this.titleView.update(getHeaderProps()); @@ -359,7 +359,7 @@ if (!this.groupSettings) { this.groupSettings = new Whisper.ReactWrapperView({ className: 'group-settings', - Component: window.Signal.Components.SessionGroupSettings, + Component: window.Signal.Components.SessionGroupSettingsWithDetails, props: getGroupSettingsProps(this.model), }); this.$('.conversation-content-right').append(this.groupSettings.el); diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index a87b219e9..48206c2e9 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -20,10 +20,8 @@ import { getInviteContactMenuItem, getLeaveGroupMenuItem, } from '../session/utils/Menu'; -import { ConversationAttributes } from '../../js/models/conversations'; -import { GroupUtils } from '../session/utils'; -import { PubKey } from '../session/types'; -import { UserUtil } from '../util'; + +import { usingClosedConversationDetails } from './session/usingClosedConversationDetails'; export type PropsData = { id: string; @@ -57,6 +55,7 @@ export type PropsData = { isSecondary?: boolean; isGroupInvitation?: boolean; isKickedFromGroup?: boolean; + closedMemberConversations?: any; // this is added by usingClosedConversationDetails }; type PropsHousekeeping = { @@ -75,34 +74,9 @@ type PropsHousekeeping = { type Props = PropsData & PropsHousekeeping; -type State = { - closedMemberConversations?: Array; -}; - -export class ConversationListItem extends React.PureComponent { +class ConversationListItem extends React.PureComponent { 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; - let members = await GroupUtils.getGroupMembers(PubKey.cast(groupId)); - const ourPrimary = await UserUtil.getPrimary(); - members = members.filter(m => m.key !== ourPrimary.key); - members.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); - const membersConvos = members.map( - m => window.ConversationController.get(m.key).cachedProps - ); - // no need to forward more than 2 conversation for rendering the group avatar - membersConvos.slice(0, 2); - this.setState({ closedMemberConversations: membersConvos }); - } } public renderAvatar() { @@ -133,7 +107,7 @@ export class ConversationListItem extends React.PureComponent { profileName={profileName} size={iconSize} isPublic={isPublic} - closedMemberConversations={this.state.closedMemberConversations} + closedMemberConversations={this.props.closedMemberConversations} />
); @@ -399,3 +373,7 @@ export class ConversationListItem extends React.PureComponent { ); } } + +export const ConversationListItemWithDetails = usingClosedConversationDetails( + ConversationListItem +); diff --git a/ts/components/SearchResults.tsx b/ts/components/SearchResults.tsx index 8532a3984..0791320e5 100644 --- a/ts/components/SearchResults.tsx +++ b/ts/components/SearchResults.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { - ConversationListItem, + ConversationListItemWithDetails, PropsData as ConversationListItemPropsType, } from './ConversationListItem'; import { @@ -62,7 +62,7 @@ export class SearchResults extends React.Component { {i18n('conversationsHeader')}
{conversations.map(conversation => ( - {
{header}
{items.map(contact => ( - void; i18n: LocalizerType; + closedMemberConversations?: any; // this is added by usingClosedConversationDetails } -export class ConversationHeader extends React.Component { +class ConversationHeader extends React.Component { public showMenuBound: (event: React.MouseEvent) => void; public onAvatarClickBound: (userPubKey: string) => void; public menuTriggerRef: React.RefObject; @@ -196,6 +198,7 @@ export class ConversationHeader extends React.Component { public renderAvatar() { const { avatarPath, + closedMemberConversations, i18n, isGroup, isMe, @@ -217,11 +220,12 @@ export class ConversationHeader extends React.Component { name={name} phoneNumber={phoneNumber} profileName={profileName} - size={28} + size={36} onAvatarClick={() => { this.onAvatarClickBound(phoneNumber); }} isPublic={isPublic} + closedMemberConversations={closedMemberConversations} /> ); @@ -497,3 +501,7 @@ export class ConversationHeader extends React.Component { ); } } + +export const ConversationHeaderWithDetails = usingClosedConversationDetails( + ConversationHeader +); diff --git a/ts/components/session/LeftPaneContactSection.tsx b/ts/components/session/LeftPaneContactSection.tsx index 4490062e3..8ff7e85fc 100644 --- a/ts/components/session/LeftPaneContactSection.tsx +++ b/ts/components/session/LeftPaneContactSection.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { - ConversationListItem, + ConversationListItemWithDetails, PropsData as ConversationListItemPropsType, } from '../ConversationListItem'; import { PropsData as SearchResultsProps } from '../SearchResults'; @@ -116,7 +116,7 @@ export class LeftPaneContactSection extends React.Component { const item = contacts[index]; return ( - { const conversation = conversations[index]; return ( - void; onInviteContacts: () => void; @@ -33,7 +35,7 @@ interface Props { onSetDisappearingMessages: (seconds: number) => void; } -export class SessionGroupSettings extends React.Component { +class SessionGroupSettings extends React.Component { public constructor(props: Props) { super(props); @@ -309,6 +311,7 @@ export class SessionGroupSettings extends React.Component { private renderHeader() { const { + closedMemberConversations, id, onGoBack, onInviteContacts, @@ -336,6 +339,7 @@ export class SessionGroupSettings extends React.Component { conversationType="group" size={80} isPublic={isPublic} + closedMemberConversations={closedMemberConversations} />
{showInviteContacts && ( @@ -350,3 +354,7 @@ export class SessionGroupSettings extends React.Component { ); } } + +export const SessionGroupSettingsWithDetails = usingClosedConversationDetails( + SessionGroupSettings +); diff --git a/ts/components/session/usingClosedConversationDetails.tsx b/ts/components/session/usingClosedConversationDetails.tsx new file mode 100644 index 000000000..62f88cf7c --- /dev/null +++ b/ts/components/session/usingClosedConversationDetails.tsx @@ -0,0 +1,60 @@ +import { GroupUtils } from '../../session/utils'; +import { UserUtil } from '../../util'; +import { PubKey } from '../../session/types'; +import React from 'react'; +import { ConversationAttributes } from '../../../js/models/conversations'; +type State = { + closedMemberConversations?: Array; +}; + +export function usingClosedConversationDetails(WrappedComponent: any) { + return class extends React.Component { + constructor(props: any) { + super(props); + this.state = { + closedMemberConversations: undefined, + }; + } + + public componentDidMount() { + void this.fetchClosedConversationDetails(); + } + + public render() { + return ( + + ); + } + + private async fetchClosedConversationDetails() { + const { + isPublic, + type, + conversationType, + isGroup, + phoneNumber, + id, + } = this.props; + if ( + !isPublic && + (conversationType === 'group' || type === 'group' || isGroup) + ) { + console.warn('fetchClosedConversationDetails'); + const groupId = id || phoneNumber; + let members = await GroupUtils.getGroupMembers(PubKey.cast(groupId)); + const ourPrimary = await UserUtil.getPrimary(); + members = members.filter(m => m.key !== ourPrimary.key); + members.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + const membersConvos = members.map( + m => window.ConversationController.get(m.key).cachedProps + ); + // no need to forward more than 2 conversation for rendering the group avatar + membersConvos.slice(0, 2); + this.setState({ closedMemberConversations: membersConvos }); + } + } + }; +} From 217cc04ad145270ca3d37e8576b053d4b0099697 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 14 Sep 2020 16:06:58 +1000 Subject: [PATCH 7/9] cleanup avatar rendering and add a 64px size --- images/profile-group.svg | 22 ----- images/profile-individual.svg | 22 ----- stylesheets/_avatar.scss | 95 +++---------------- ts/components/Avatar.tsx | 32 +------ .../AvatarPlaceHolder/ClosedGroupAvatar.tsx | 6 +- 5 files changed, 18 insertions(+), 159 deletions(-) delete mode 100644 images/profile-group.svg delete mode 100644 images/profile-individual.svg diff --git a/images/profile-group.svg b/images/profile-group.svg deleted file mode 100644 index a6c9788bc..000000000 --- a/images/profile-group.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - Group/group-28 - Created with Sketch. - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/images/profile-individual.svg b/images/profile-individual.svg deleted file mode 100644 index f28b9ea45..000000000 --- a/images/profile-individual.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - Profile/profile-28 - Created with Sketch. - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/stylesheets/_avatar.scss b/stylesheets/_avatar.scss index ba61d4076..3f3fd2eda 100644 --- a/stylesheets/_avatar.scss +++ b/stylesheets/_avatar.scss @@ -47,14 +47,6 @@ width: 100%; } -.module-avatar__icon--group { - @include color-svg('../images/profile-group.svg', $color-white); -} - -.module-avatar__icon--direct { - @include color-svg('../images/profile-individual.svg', $color-white); -} - .module-avatar__icon-closed .module-avatar--28, .module-avatar--28 { height: 28px; @@ -66,16 +58,7 @@ } } -.module-avatar__label--28 { - font-size: 14px; - line-height: 28px; -} - -.module-avatar__icon--28 { - height: 16px; - width: 16px; -} - +.module-avatar__icon-closed .module-avatar--36, .module-avatar--36 { height: 36px; width: 36px; @@ -86,19 +69,7 @@ } } -.module-avatar__label--36 { - margin-top: 1px; - width: 36px; - font-size: 16px; - letter-spacing: 0.19px; - line-height: 36px; -} - -.module-avatar__icon--36 { - height: 20px; - width: 20px; -} - +.module-avatar__icon-closed .module-avatar--48, .module-avatar--48 { height: 48px; width: 48px; @@ -109,18 +80,18 @@ } } -.module-avatar__label--48 { - width: 48px; - font-size: 20px; - letter-spacing: 0.19px; - line-height: 48px; -} +.module-avatar__icon-closed .module-avatar--64, +.module-avatar--64 { + height: 64px; + width: 64px; -.module-avatar__icon--48 { - height: 26px; - width: 26px; + img { + height: 64px; + width: 64px; + } } +.module-avatar__icon-closed .module-avatar--80, .module-avatar--80 { height: 80px; width: 80px; @@ -131,17 +102,6 @@ } } -.module-avatar__label--80 { - width: 80px; - font-size: 40px; - line-height: 82px; -} - -.module-avatar__icon--80 { - height: 42px; - width: 42px; -} - .module-avatar--300 { height: 300px; width: 300px; @@ -152,39 +112,6 @@ } } -.module-avatar__label--300 { - width: 300px; - font-size: 150px; - line-height: 302px; -} - -.module-avatar__icon--300 { - height: 158px; - width: 158px; -} - -.module-avatar__icon--note-to-self { - width: 70%; - height: 70%; - @include color-svg('../images/note-28.svg', $color-white); -} - -.module-avatar__label { - color: $color-gray-05; -} - -.module-avatar__icon--group { - background-color: $color-gray-05; -} - -.module-avatar__icon--direct { - background-color: $color-gray-05; -} - -.module-avatar__icon--note-to-self { - background-color: $color-gray-05; -} - .module-avatar__icon--crown-wrapper { background-color: $color-gray-75; } diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index d354cacc1..98eb04b59 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -95,40 +95,12 @@ export class Avatar extends React.PureComponent { conversationType, closedMemberConversations, isPublic, - name, - noteToSelf, size, i18n, } = this.props; - const initials = getInitials(name); const isGroup = conversationType === 'group'; - if (noteToSelf) { - return ( -
- ); - } - - if (!isGroup && initials) { - return ( -
- {initials} -
- ); - } - if (isGroup && !isPublic && closedMemberConversations) { const forcedI18n = i18n || window.i18n; return ( @@ -144,8 +116,7 @@ export class Avatar extends React.PureComponent {
); @@ -163,6 +134,7 @@ export class Avatar extends React.PureComponent { size !== 28 && size !== 36 && size !== 48 && + size !== 64 && size !== 80 && size !== 300 ) { diff --git a/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx b/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx index b4a5b5902..6b4224c75 100644 --- a/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx +++ b/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx @@ -41,10 +41,14 @@ export class ClosedGroupAvatar extends React.PureComponent { avatarsDiameter = 36; break; } - case 80: { + case 64: { avatarsDiameter = 48; break; } + case 80: { + avatarsDiameter = 64; + break; + } case 300: { avatarsDiameter = 80; break; From afb1296c3d5790cb7752887fc381a8f6c34a49f6 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 14 Sep 2020 16:39:43 +1000 Subject: [PATCH 8/9] center avatar on group panel --- stylesheets/_session.scss | 5 ----- stylesheets/_session_group_panel.scss | 7 ++----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index abcad5b3c..79e868d7d 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -1514,11 +1514,6 @@ input { } } -.invite-friends-container { - height: $session-icon-size-lg; - width: $session-icon-size-lg; -} - .module-message-detail { .module-message { pointer-events: none; diff --git a/stylesheets/_session_group_panel.scss b/stylesheets/_session_group_panel.scss index a0b0610b4..21f7a9619 100644 --- a/stylesheets/_session_group_panel.scss +++ b/stylesheets/_session_group_panel.scss @@ -18,6 +18,8 @@ &-header { margin-top: $session-margin-lg; + margin-inline-start: $session-margin-sm; + margin-inline-end: $session-margin-sm; width: -webkit-fill-available; display: flex; flex-direction: row; @@ -26,11 +28,6 @@ .module-avatar { margin: auto; } - - .session-icon-button, - .invite-friends-container { - margin: 0 $session-margin-md; - } } h2 { From bc1aff747a87471a0f6ce97c57661526e4d94b70 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 14 Sep 2020 16:45:06 +1000 Subject: [PATCH 9/9] refresh avatar for closed group on new props --- ts/components/Avatar.tsx | 12 ++++-------- .../session/usingClosedConversationDetails.tsx | 5 ++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 98eb04b59..69863c2a6 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -111,15 +111,11 @@ export class Avatar extends React.PureComponent { /> ); } - - return ( -
+ console.warn( + 'renderNoImage should not happen with something else than a closed group' ); + + return
; } public render() { diff --git a/ts/components/session/usingClosedConversationDetails.tsx b/ts/components/session/usingClosedConversationDetails.tsx index 62f88cf7c..8d80fdefd 100644 --- a/ts/components/session/usingClosedConversationDetails.tsx +++ b/ts/components/session/usingClosedConversationDetails.tsx @@ -20,6 +20,10 @@ export function usingClosedConversationDetails(WrappedComponent: any) { void this.fetchClosedConversationDetails(); } + public componentDidUpdate() { + void this.fetchClosedConversationDetails(); + } + public render() { return (