Merge pull request #1336 from Bilb/remove-jazzicon

pull/1339/head
Audric Ackermann 5 years ago committed by GitHub
commit 56a81ccc93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>Group/group-28</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M18.7272727,13.2857143 C20.6890909,13.2857143 22.2609091,11.6585714 22.2609091,9.64285714 C22.2609091,7.62714286 20.6890909,6 18.7272727,6 C16.7654545,6 15.1818182,7.62714286 15.1818182,9.64285714 C15.1818182,11.6585714 16.7654545,13.2857143 18.7272727,13.2857143 Z M9.27272727,13.2857143 C11.2345455,13.2857143 12.8063636,11.6585714 12.8063636,9.64285714 C12.8063636,7.62714286 11.2345455,6 9.27272727,6 C7.31090909,6 5.72727273,7.62714286 5.72727273,9.64285714 C5.72727273,11.6585714 7.31090909,13.2857143 9.27272727,13.2857143 Z M9.27272727,15.7142857 C6.51909091,15.7142857 1,17.135 1,19.9642857 L1,23 L17.5454545,23 L17.5454545,19.9642857 C17.5454545,17.135 12.0263636,15.7142857 9.27272727,15.7142857 Z M18.7272727,15.7142857 C18.3845455,15.7142857 17.9945455,15.7385714 17.5809091,15.775 C18.9518182,16.795 19.9090909,18.1671429 19.9090909,19.9642857 L19.9090909,23 L27,23 L27,19.9642857 C27,17.135 21.4809091,15.7142857 18.7272727,15.7142857 Z" id="path-1"></path>
<rect id="path-3" x="0" y="0" width="28" height="28"></rect>
</defs>
<g id="Group/group-28" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Color/UI/Black" mask="url(#mask-2)">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<use id="fill" fill="#000000" fill-rule="evenodd" xlink:href="#path-3"></use>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>Profile/profile-28</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M14,14 C16.7625,14 19,11.7625 19,9 C19,6.2375 16.7625,4 14,4 C11.2375,4 9,6.2375 9,9 C9,11.7625 11.2375,14 14,14 Z M14,16.5 C10.6625,16.5 4,18.175 4,21.5 L4,24 L24,24 L24,21.5 C24,18.175 17.3375,16.5 14,16.5 Z" id="path-1"></path>
<rect id="path-3" x="0" y="0" width="28" height="28"></rect>
</defs>
<g id="Profile/profile-28" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Color/UI/Black" mask="url(#mask-2)">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<use id="fill" fill="#000000" fill-rule="evenodd" xlink:href="#path-3"></use>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@ -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(

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

@ -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();

@ -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,

@ -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);

@ -9,6 +9,7 @@
img {
object-fit: cover;
border-radius: 50%;
border: 1px solid $borderAvatarColor;
}
}
@ -46,14 +47,7 @@
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;
width: 28px;
@ -64,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;
@ -84,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;
@ -107,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;
@ -129,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;
@ -150,47 +112,14 @@
}
}
.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
.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;
}
.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;
}
}

@ -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 {

@ -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;

@ -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 {

@ -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: #00000059; // 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,

@ -22,6 +22,7 @@ describe('InboxView', () => {
textsecure.storage.user.getNumber(),
'private'
);
window.getOurDisplayName = () => 'display name test';
inboxView = new Whisper.InboxView({
model: {},
window,

@ -1,21 +1,22 @@
import React from 'react';
import classNames from 'classnames';
import { JazzIcon } from './JazzIcon';
import { getInitials } from '../util/getInitials';
import { LocalizerType } from '../types/Util';
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<ConversationAttributes>;
i18n?: LocalizerType;
onAvatarClick?: () => void;
}
@ -40,38 +41,36 @@ export class Avatar extends React.PureComponent<Props, State> {
}
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,
});
}
public renderIdenticon() {
const { phoneNumber, borderColor, borderWidth, size } = this.props;
const { phoneNumber, size, name, profileName } = this.props;
if (!phoneNumber) {
return this.renderNoImage();
window.log.error('Empty phoneNumber for identicon');
return <></>;
}
const borderStyle = this.getBorderStyle(borderColor, borderWidth);
// Generate the seed
const hash = phoneNumber.substring(0, 12);
const seed = parseInt(hash, 16) || 1234;
return <JazzIcon seed={seed} diameter={size} paperStyles={borderStyle} />;
const userName = profileName || name;
return (
<AvatarPlaceHolder
phoneNumber={phoneNumber}
diameter={size}
name={userName}
colors={this.getAvatarColors()}
borderColor={this.getAvatarBorderColor()}
/>
);
}
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 +81,8 @@ export class Avatar extends React.PureComponent<Props, State> {
!name && profileName ? ` ~${profileName}` : ''
}`;
const borderStyle = this.getBorderStyle(borderColor, borderWidth);
return (
<img
style={borderStyle}
onError={this.handleImageErrorBound}
alt={window.i18n('contactAvatarAlt', [title])}
src={avatarPath}
@ -97,74 +93,44 @@ export class Avatar extends React.PureComponent<Props, State> {
public renderNoImage() {
const {
conversationType,
name,
noteToSelf,
closedMemberConversations,
isPublic,
size,
borderColor,
borderWidth,
i18n,
} = this.props;
const initials = getInitials(name);
const isGroup = conversationType === 'group';
if (noteToSelf) {
if (isGroup && !isPublic && closedMemberConversations) {
const forcedI18n = i18n || window.i18n;
return (
<div
className={classNames(
'module-avatar__icon',
'module-avatar__icon--note-to-self',
`module-avatar__icon--${size}`
)}
<ClosedGroupAvatar
size={size}
conversations={closedMemberConversations}
i18n={forcedI18n}
/>
);
}
const borderStyle = this.getBorderStyle(borderColor, borderWidth);
if (!isGroup && initials) {
return (
<div
className={classNames(
'module-avatar__label',
`module-avatar__label--${size}`
)}
style={borderStyle}
>
{initials}
</div>
);
}
return (
<div
className={classNames(
'module-avatar__icon',
`module-avatar__icon--${conversationType}`,
`module-avatar__icon--${size}`
)}
style={borderStyle}
/>
console.warn(
'renderNoImage should not happen with something else than a closed group'
);
return <div className={classNames('module-avatar__icon')} />;
}
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 &&
size !== 36 &&
size !== 48 &&
size !== 64 &&
size !== 80 &&
size !== 300
) {
@ -207,17 +173,13 @@ export class Avatar extends React.PureComponent<Props, State> {
: 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<string> {
// const theme = window.Events.getThemedSettings();
// defined in session-android as `profile_picture_placeholder_colors`
return ['#5ff8b0', '#26cdb9', '#f3c615', '#fcac5a'];
}
private getAvatarBorderColor(): string {
return '#00000059'; // borderAvatarColor in themes.scss
}
}

@ -0,0 +1,102 @@
import React from 'react';
import { getInitials } from '../../util/getInitials';
interface Props {
diameter: number;
phoneNumber: string;
colors: Array<string>;
borderColor: string;
name?: string;
}
interface State {
sha512Seed?: string;
}
export class AvatarPlaceHolder extends React.PureComponent<Props, State> {
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 { borderColor, 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 (
<svg viewBox={viewBox}>
<g id="UrTavla">
<circle
cx={r}
cy={r}
r={r}
fill={bgColor}
shape-rendering="geometricPrecision"
stroke={borderColor}
stroke-width="1"
/>
<text
font-size={fontSize}
x="50%"
y="50%"
fill="white"
text-anchor="middle"
stroke="white"
stroke-width={1}
alignment-baseline="central"
>
{initial}
</text>
</g>
</svg>
);
}
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('');
}
}

@ -0,0 +1,94 @@
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
// 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 64: {
avatarsDiameter = 48;
break;
}
case 80: {
avatarsDiameter = 64;
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
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 <></>;
}
}
}

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

@ -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,
@ -21,6 +21,8 @@ import {
getLeaveGroupMenuItem,
} from '../session/utils/Menu';
import { usingClosedConversationDetails } from './session/usingClosedConversationDetails';
export type PropsData = {
id: string;
phoneNumber: string;
@ -53,6 +55,7 @@ export type PropsData = {
isSecondary?: boolean;
isGroupInvitation?: boolean;
isKickedFromGroup?: boolean;
closedMemberConversations?: any; // this is added by usingClosedConversationDetails
};
type PropsHousekeeping = {
@ -71,7 +74,11 @@ type PropsHousekeeping = {
type Props = PropsData & PropsHousekeeping;
export class ConversationListItem extends React.PureComponent<Props> {
class ConversationListItem extends React.PureComponent<Props> {
public constructor(props: Props) {
super(props);
}
public renderAvatar() {
const {
avatarPath,
@ -82,11 +89,9 @@ export class ConversationListItem extends React.PureComponent<Props> {
name,
phoneNumber,
profileName,
isOnline,
isPublic,
} = this.props;
const borderColor = isOnline ? Colors.ONLINE : Colors.OFFLINE;
const iconSize = 36;
return (
@ -101,7 +106,8 @@ export class ConversationListItem extends React.PureComponent<Props> {
phoneNumber={phoneNumber}
profileName={profileName}
size={iconSize}
borderColor={borderColor}
isPublic={isPublic}
closedMemberConversations={this.props.closedMemberConversations}
/>
</div>
);
@ -139,7 +145,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
: null
)}
>
{isMe ? i18n('noteToSelf') : this.renderUser()}
{this.renderUser()}
</div>
{this.renderUnread()}
{
@ -308,7 +314,6 @@ export class ConversationListItem extends React.PureComponent<Props> {
style,
mentionedUs,
} = this.props;
const triggerId = `conversation-item-${phoneNumber}-ctxmenu`;
return (
@ -347,18 +352,19 @@ export class ConversationListItem extends React.PureComponent<Props> {
}
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 (
<div className="module-conversation__user">
<ContactName
phoneNumber={displayedPubkey}
name={name}
profileName={profileName}
profileName={displayName}
module="module-conversation__user"
i18n={window.i18n}
boldProfileName={true}
@ -367,3 +373,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
);
}
}
export const ConversationListItemWithDetails = usingClosedConversationDetails(
ConversationListItem
);

@ -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<string>) => {
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<string>;
}
// 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<Props> {
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 (
<Paper color={shuffledColours[0]} diameter={diameter} style={paperStyles}>
<svg
xmlns={svgns}
x="0"
y="0"
height={diameter}
width={diameter}
style={svgStyles}
>
{shapesArr.map((_, i) =>
this.genShape(
shuffledColours[i + 1],
diameter,
i,
shapeCount - 1,
generator
)
)}
</svg>
</Paper>
);
}
private hueShift(colors: Array<string>, 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 (
<rect
key={i}
x="0"
y="0"
rx="0"
ry="0"
height={diameter}
width={diameter}
transform={transform}
fill={colour}
/>
);
}
private colorsForIcon(arr: Array<string>) {
if (isColors(arr)) {
return arr;
}
return defaultColors;
}
private shuffleArray<T>(array: Array<T>, 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;
}
}

@ -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 }) => (
<div
className="paper"
style={{
...styles,
backgroundColor: color,
height: diameter,
width: diameter,
...(styleOverrides || {}),
}}
>
{children}
</div>
);

@ -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();
}
}

@ -1,2 +0,0 @@
import { JazzIcon } from './JazzIcon';
export { JazzIcon };

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

@ -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<Props> {
{i18n('conversationsHeader')}
</div>
{conversations.map(conversation => (
<ConversationListItem
<ConversationListItemWithDetails
key={conversation.phoneNumber}
{...conversation}
onClick={openConversation}
@ -106,7 +106,7 @@ export class SearchResults extends React.Component<Props> {
<div className="module-search-results__contacts">
<div className="module-search-results__contacts-header">{header}</div>
{items.map(contact => (
<ConversationListItem
<ConversationListItemWithDetails
key={contact.phoneNumber}
{...contact}
onClick={openConversation}

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

@ -16,6 +16,7 @@ import {
SessionButtonType,
} from '../session/SessionButton';
import * as Menu from '../../session/utils/Menu';
import { usingClosedConversationDetails } from '../session/usingClosedConversationDetails';
export interface TimerOption {
name: string;
@ -91,9 +92,10 @@ interface Props {
onUpdateGroupName: () => void;
i18n: LocalizerType;
closedMemberConversations?: any; // this is added by usingClosedConversationDetails
}
export class ConversationHeader extends React.Component<Props> {
class ConversationHeader extends React.Component<Props> {
public showMenuBound: (event: React.MouseEvent<HTMLDivElement>) => void;
public onAvatarClickBound: (userPubKey: string) => void;
public menuTriggerRef: React.RefObject<any>;
@ -196,16 +198,16 @@ export class ConversationHeader extends React.Component<Props> {
public renderAvatar() {
const {
avatarPath,
closedMemberConversations,
i18n,
isGroup,
isMe,
name,
phoneNumber,
profileName,
isOnline,
isPublic,
} = this.props;
const borderColor = isOnline ? Colors.ONLINE : Colors.OFFLINE_LIGHT;
const conversationType = isGroup ? 'group' : 'direct';
return (
@ -218,12 +220,12 @@ export class ConversationHeader extends React.Component<Props> {
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
size={28}
borderColor={borderColor}
borderWidth={0}
size={36}
onAvatarClick={() => {
this.onAvatarClickBound(phoneNumber);
}}
isPublic={isPublic}
closedMemberConversations={closedMemberConversations}
/>
</span>
);
@ -499,3 +501,7 @@ export class ConversationHeader extends React.Component<Props> {
);
}
}
export const ConversationHeaderWithDetails = usingClosedConversationDetails(
ConversationHeader
);

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

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

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

@ -149,6 +149,7 @@ export class ActionsPanel extends React.Component<Props, State> {
phoneNumber={window.storage.get('primaryDevicePubKey')}
size={28}
onAvatarClick={handleClick}
profileName={window.getOurDisplayName()}
/>
);
}

@ -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<Props, State> {
const item = contacts[index];
return (
<ConversationListItem
<ConversationListItemWithDetails
key={key}
style={style}
{...item}

@ -4,7 +4,7 @@ import { AutoSizer, List } from 'react-virtualized';
import { MainViewController } from '../MainViewController';
import {
ConversationListItem,
ConversationListItemWithDetails,
PropsData as ConversationListItemPropsType,
} from '../ConversationListItem';
import { ConversationType } from '../../state/ducks/conversations';
@ -77,19 +77,6 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
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.handleOnPaste = this.handleOnPaste.bind(this);
@ -112,29 +99,12 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
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 = ({
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');
@ -143,7 +113,7 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
const conversation = conversations[index];
return (
<ConversationListItem
<ConversationListItemWithDetails
key={key}
style={style}
{...conversation}
@ -154,7 +124,11 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
};
public renderList(): JSX.Element | Array<JSX.Element | null> {
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<Props, State> {
);
}
const conversations = this.getCurrentConversations();
if (!conversations) {
throw new Error(
'render: must provided conversations if no search results are provided'

@ -10,6 +10,7 @@ import { SessionDropdown } from './SessionDropdown';
import { MediaGallery } from '../conversation/media-gallery/MediaGallery';
import _ from 'lodash';
import { TimerOption } from '../conversation/ConversationHeader';
import { usingClosedConversationDetails } from '../session/usingClosedConversationDetails';
interface Props {
id: string;
@ -23,6 +24,7 @@ interface Props {
amMod: boolean;
isKickedFromGroup: boolean;
isBlocked: boolean;
closedMemberConversations?: any; // this is added by usingClosedConversationDetails
onGoBack: () => void;
onInviteContacts: () => void;
@ -33,7 +35,7 @@ interface Props {
onSetDisappearingMessages: (seconds: number) => void;
}
export class SessionGroupSettings extends React.Component<Props, any> {
class SessionGroupSettings extends React.Component<Props, any> {
public constructor(props: Props) {
super(props);
@ -309,6 +311,7 @@ export class SessionGroupSettings extends React.Component<Props, any> {
private renderHeader() {
const {
closedMemberConversations,
id,
onGoBack,
onInviteContacts,
@ -335,6 +338,8 @@ export class SessionGroupSettings extends React.Component<Props, any> {
phoneNumber={id}
conversationType="group"
size={80}
isPublic={isPublic}
closedMemberConversations={closedMemberConversations}
/>
<div className="invite-friends-container">
{showInviteContacts && (
@ -349,3 +354,7 @@ export class SessionGroupSettings extends React.Component<Props, any> {
);
}
}
export const SessionGroupSettingsWithDetails = usingClosedConversationDetails(
SessionGroupSettings
);

@ -0,0 +1,63 @@
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<ConversationAttributes>;
};
export function usingClosedConversationDetails(WrappedComponent: any) {
return class extends React.Component<any, State> {
constructor(props: any) {
super(props);
this.state = {
closedMemberConversations: undefined,
};
}
public componentDidMount() {
void this.fetchClosedConversationDetails();
}
public componentDidUpdate() {
void this.fetchClosedConversationDetails();
}
public render() {
return (
<WrappedComponent
closedMemberConversations={this.state.closedMemberConversations}
{...this.props}
/>
);
}
private async fetchClosedConversationDetails() {
const {
isPublic,
type,
conversationType,
isGroup,
phoneNumber,
id,
} = this.props;
if (
!isPublic &&
(conversationType === 'group' || type === 'group' || isGroup)
) {
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 });
}
}
};
}

@ -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

@ -108,12 +108,8 @@ export const _getLeftPaneLists = (
const archivedConversations: Array<ConversationType> = [];
const allContacts: Array<ConversationType> = [];
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,

@ -17,5 +17,5 @@ export function getInitials(name?: string): string | undefined {
return;
}
return initials.slice(0, 2).join('');
return initials[0];
}

1
ts/window.d.ts vendored

@ -80,6 +80,7 @@ declare global {
setSettingValue: any;
shortenPubkey: any;
showEditProfileDialog: any;
getOurDisplayName: () => string | undefined;
showPasswordDialog: any;
showSeedDialog: any;
storage: any;

Loading…
Cancel
Save