New AvatarPopup component
This commit is contained in:
parent
195de96269
commit
dd1f9b055f
19 changed files with 432 additions and 30 deletions
|
@ -166,6 +166,10 @@
|
||||||
"description":
|
"description":
|
||||||
"Only available on development modes, menu option to open up the standalone device setup sequence"
|
"Only available on development modes, menu option to open up the standalone device setup sequence"
|
||||||
},
|
},
|
||||||
|
"avatarMenuViewArchive": {
|
||||||
|
"message": "View Archive",
|
||||||
|
"description": "One of the menu options available in the Avatar Popup menu"
|
||||||
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"message": "Loading...",
|
"message": "Loading...",
|
||||||
"description":
|
"description":
|
||||||
|
|
1
images/icons/v2/archive-outline-16.svg
Normal file
1
images/icons/v2/archive-outline-16.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><title>archive-outline-16</title><path d="M9.75,9H6.25a.75.75,0,0,1-.75-.75h0a.75.75,0,0,1,.75-.75h3.5a.75.75,0,0,1,.75.75h0A.75.75,0,0,1,9.75,9Zm5.229-6.5v3a.5.5,0,0,1-.5.5H14v7a1,1,0,0,1-1,1H3a1,1,0,0,1-1-1V6H1.479a.5.5,0,0,1-.5-.5v-3a.5.5,0,0,1,.5-.5h13A.5.5,0,0,1,14.979,2.5ZM2,5H13.979V3h-12V5ZM13,6H3v7H13Z"/></svg>
|
After Width: | Height: | Size: 404 B |
1
images/icons/v2/archive-solid-16.svg
Normal file
1
images/icons/v2/archive-solid-16.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><title>archive-solid-16</title><path d="M14.479,5h-13a.5.5,0,0,1-.5-.5v-2a.5.5,0,0,1,.5-.5h13a.5.5,0,0,1,.5.5v2A.5.5,0,0,1,14.479,5ZM14,6v7a1,1,0,0,1-1,1H3a1,1,0,0,1-1-1V6ZM10.5,8.25a.75.75,0,0,0-.75-.75H6.25a.75.75,0,0,0,0,1.5h3.5A.75.75,0,0,0,10.5,8.25Z"/></svg>
|
After Width: | Height: | Size: 347 B |
1
images/icons/v2/settings-outline-16.svg
Normal file
1
images/icons/v2/settings-outline-16.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Export" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>settings-16</title><path d="M8,5.5A2.5,2.5,0,1,1,5.5,8,2.5,2.5,0,0,1,8,5.5m0-1A3.5,3.5,0,1,0,11.5,8,3.5,3.5,0,0,0,8,4.5ZM8,1.66a.48.48,0,0,1,.44.26L9,3a1.48,1.48,0,0,0,1.32.79,1.59,1.59,0,0,0,.43-.06L12,3.39l.15,0a.5.5,0,0,1,.47.64l-.36,1.2A1.5,1.5,0,0,0,13,7l1.1.6a.5.5,0,0,1,0,.88L13,9a1.5,1.5,0,0,0-.73,1.75l.36,1.2a.5.5,0,0,1-.47.64l-.15,0-1.2-.36a1.59,1.59,0,0,0-.43-.06A1.48,1.48,0,0,0,9,13l-.6,1.1a.5.5,0,0,1-.88,0L7,13a1.48,1.48,0,0,0-1.32-.79,1.59,1.59,0,0,0-.43.06L4,12.61l-.15,0A.5.5,0,0,1,3.39,12l.36-1.2A1.5,1.5,0,0,0,3,9l-1.1-.6a.5.5,0,0,1,0-.88L3,7a1.5,1.5,0,0,0,.73-1.75L3.39,4a.5.5,0,0,1,.47-.64l.15,0,1.2.36a1.59,1.59,0,0,0,.43.06A1.48,1.48,0,0,0,7,3l.6-1.1A.48.48,0,0,1,8,1.66m0-1a1.47,1.47,0,0,0-1.32.79l-.6,1.1a.5.5,0,0,1-.44.26.36.36,0,0,1-.14,0L4.3,2.43a1.72,1.72,0,0,0-.44-.06A1.5,1.5,0,0,0,2.43,4.3l.36,1.2a.49.49,0,0,1-.24.58l-1.1.6a1.5,1.5,0,0,0,0,2.64l1.1.6a.49.49,0,0,1,.24.58l-.36,1.2a1.5,1.5,0,0,0,1.43,1.93,1.72,1.72,0,0,0,.44-.06l1.2-.36a.36.36,0,0,1,.14,0,.5.5,0,0,1,.44.26l.6,1.1a1.5,1.5,0,0,0,2.64,0l.6-1.1a.5.5,0,0,1,.44-.26.36.36,0,0,1,.14,0l1.2.36a1.72,1.72,0,0,0,.44.06,1.5,1.5,0,0,0,1.43-1.93l-.36-1.2a.49.49,0,0,1,.24-.58l1.1-.6a1.5,1.5,0,0,0,0-2.64l-1.1-.6a.49.49,0,0,1-.24-.58l.36-1.2a1.5,1.5,0,0,0-1.43-1.93,1.72,1.72,0,0,0-.44.06l-1.2.36a.36.36,0,0,1-.14,0,.5.5,0,0,1-.44-.26l-.6-1.1A1.47,1.47,0,0,0,8,.66Z"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
images/icons/v2/settings-solid-16.svg
Normal file
1
images/icons/v2/settings-solid-16.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Source" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>settings-solid-16</title><path d="M14.55,6.68l-1.1-.6a.49.49,0,0,1-.24-.58l.36-1.2A1.51,1.51,0,0,0,11.7,2.43l-1.2.36a.49.49,0,0,1-.58-.24l-.6-1.1a1.5,1.5,0,0,0-2.64,0l-.6,1.1a.49.49,0,0,1-.58.24L4.3,2.43A1.51,1.51,0,0,0,2.43,4.3l.36,1.2a.49.49,0,0,1-.24.58l-1.1.6a1.5,1.5,0,0,0,0,2.64l1.1.6a.49.49,0,0,1,.24.58l-.36,1.2A1.51,1.51,0,0,0,4.3,13.57l1.2-.36a.49.49,0,0,1,.58.24l.6,1.1a1.5,1.5,0,0,0,2.64,0l.6-1.1a.49.49,0,0,1,.58-.24l1.2.36a1.51,1.51,0,0,0,1.87-1.87l-.36-1.2a.49.49,0,0,1,.24-.58l1.1-.6A1.5,1.5,0,0,0,14.55,6.68ZM8,12a4,4,0,1,1,4-4A4,4,0,0,1,8,12Z"/></svg>
|
After Width: | Height: | Size: 648 B |
|
@ -1939,7 +1939,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
getProfileName() {
|
getProfileName() {
|
||||||
if (this.isPrivate() && !this.get('name')) {
|
if (this.isPrivate()) {
|
||||||
return this.get('profileName');
|
return this.get('profileName');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
3
main.js
3
main.js
|
@ -524,6 +524,8 @@ async function showSettingsWindow() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addDarkOverlay();
|
||||||
|
|
||||||
const size = mainWindow.getSize();
|
const size = mainWindow.getSize();
|
||||||
const options = {
|
const options = {
|
||||||
width: Math.min(500, size[0]),
|
width: Math.min(500, size[0]),
|
||||||
|
@ -557,7 +559,6 @@ async function showSettingsWindow() {
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsWindow.once('ready-to-show', () => {
|
settingsWindow.once('ready-to-show', () => {
|
||||||
addDarkOverlay();
|
|
||||||
settingsWindow.show();
|
settingsWindow.show();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,7 +122,7 @@
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
font-size: $font-size-small;
|
@include font-body-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
@ -12,14 +12,14 @@ body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
color: $color-gray-95;
|
color: $color-gray-90;
|
||||||
|
|
||||||
@include font-body-1;
|
@include font-body-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light-theme {
|
body.light-theme {
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
color: $color-gray-95;
|
color: $color-gray-90;
|
||||||
}
|
}
|
||||||
body.dark-theme {
|
body.dark-theme {
|
||||||
background-color: $color-gray-95;
|
background-color: $color-gray-95;
|
||||||
|
|
|
@ -131,8 +131,7 @@
|
||||||
// Other
|
// Other
|
||||||
|
|
||||||
@mixin popper-shadow() {
|
@mixin popper-shadow() {
|
||||||
box-shadow: 0 0 0 1px $color-black-alpha-05,
|
box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.3), 0px 0px 8px rgba(0, 0, 0, 0.05);
|
||||||
0 8px 20px 0 $color-black-alpha-20;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin button-reset {
|
@mixin button-reset {
|
||||||
|
|
|
@ -3159,6 +3159,9 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-avatar--with-click {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
.module-avatar--no-image {
|
.module-avatar--no-image {
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
background-color: $color-conversation-grey;
|
background-color: $color-conversation-grey;
|
||||||
|
@ -6343,7 +6346,141 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Third-party module: react-contextmenu
|
// Module: Avatar Popup
|
||||||
|
|
||||||
|
.module-avatar-popup {
|
||||||
|
min-width: 240px;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
|
||||||
|
@include popper-shadow;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-90;
|
||||||
|
background-color: $color-white;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-05;
|
||||||
|
background-color: $color-gray-75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-avatar-popup__profile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-avatar-popup__profile {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-avatar-popup__profile__text {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-avatar-popup__profile__name {
|
||||||
|
@include font-body-2-bold;
|
||||||
|
}
|
||||||
|
.module-avatar-popup__profile__number {
|
||||||
|
@include font-caption;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-60;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-avatar-popup__divider {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
height: 1px;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-gray-15;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-gray-60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-avatar-popup__item {
|
||||||
|
@include font-body-2;
|
||||||
|
@include button-reset;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-gray-05;
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
background-color: $color-gray-05;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-gray-60;
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
background-color: $color-gray-05;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-avatar-popup__item__icon {
|
||||||
|
margin-left: 6px;
|
||||||
|
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
.module-avatar-popup__item__icon-settings {
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/settings-outline-16.svg',
|
||||||
|
$color-gray-75
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/settings-solid-16.svg',
|
||||||
|
$color-gray-15
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.module-avatar-popup__item__icon-archive {
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/archive-outline-16.svg',
|
||||||
|
$color-gray-75
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/archive-solid-16.svg',
|
||||||
|
$color-gray-15
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-avatar-popup__item__text {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Third-party module: react-contextmenu*/
|
||||||
|
|
||||||
.react-contextmenu {
|
.react-contextmenu {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
|
@ -35,12 +35,12 @@
|
||||||
margin: 0 0 20px 20px;
|
margin: 0 0 20px 20px;
|
||||||
}
|
}
|
||||||
.synced_at {
|
.synced_at {
|
||||||
font-size: $font-size-small;
|
@include font-body-2;
|
||||||
color: $color-gray-60;
|
color: $color-gray-60;
|
||||||
}
|
}
|
||||||
.sync_failed {
|
.sync_failed {
|
||||||
display: none;
|
display: none;
|
||||||
font-size: $font-size-small;
|
@include font-body-2;
|
||||||
color: $color-accent-red;
|
color: $color-accent-red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -216,4 +216,3 @@ $color-signal-blue-tint-alpha-50: rgba($color-ios-blue-tint, 0.5);
|
||||||
|
|
||||||
$left-pane-width: 320px;
|
$left-pane-width: 320px;
|
||||||
$header-height: 48px;
|
$header-height: 48px;
|
||||||
$font-size-small: (13/14) + em;
|
|
||||||
|
|
|
@ -4,16 +4,22 @@ import classNames from 'classnames';
|
||||||
import { getInitials } from '../util/getInitials';
|
import { getInitials } from '../util/getInitials';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
interface Props {
|
export interface Props {
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
conversationType: 'group' | 'direct';
|
conversationType: 'group' | 'direct';
|
||||||
i18n: LocalizerType;
|
|
||||||
noteToSelf?: boolean;
|
noteToSelf?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
size: 28 | 52 | 80;
|
size: 28 | 52 | 80;
|
||||||
|
|
||||||
|
onClick?: () => unknown;
|
||||||
|
|
||||||
|
// Matches Popper's RefHandler type
|
||||||
|
innerRef?: (ref: HTMLElement | null) => void;
|
||||||
|
|
||||||
|
i18n: LocalizerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -63,9 +69,15 @@ export class Avatar extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderNoImage() {
|
public renderNoImage() {
|
||||||
const { conversationType, name, noteToSelf, size } = this.props;
|
const {
|
||||||
|
conversationType,
|
||||||
|
name,
|
||||||
|
noteToSelf,
|
||||||
|
profileName,
|
||||||
|
size,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const initials = getInitials(name);
|
const initials = getInitials(name || profileName);
|
||||||
const isGroup = conversationType === 'group';
|
const isGroup = conversationType === 'group';
|
||||||
|
|
||||||
if (noteToSelf) {
|
if (noteToSelf) {
|
||||||
|
@ -105,7 +117,14 @@ export class Avatar extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { avatarPath, color, size, noteToSelf } = this.props;
|
const {
|
||||||
|
avatarPath,
|
||||||
|
color,
|
||||||
|
innerRef,
|
||||||
|
noteToSelf,
|
||||||
|
onClick,
|
||||||
|
size,
|
||||||
|
} = this.props;
|
||||||
const { imageBroken } = this.state;
|
const { imageBroken } = this.state;
|
||||||
|
|
||||||
const hasImage = !noteToSelf && avatarPath && !imageBroken;
|
const hasImage = !noteToSelf && avatarPath && !imageBroken;
|
||||||
|
@ -114,14 +133,20 @@ export class Avatar extends React.Component<Props, State> {
|
||||||
throw new Error(`Size ${size} is not supported!`);
|
throw new Error(`Size ${size} is not supported!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const role = onClick ? 'button' : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-avatar',
|
'module-avatar',
|
||||||
`module-avatar--${size}`,
|
`module-avatar--${size}`,
|
||||||
hasImage ? 'module-avatar--with-image' : 'module-avatar--no-image',
|
hasImage ? 'module-avatar--with-image' : 'module-avatar--no-image',
|
||||||
!hasImage ? `module-avatar--${color}` : null
|
!hasImage ? `module-avatar--${color}` : null,
|
||||||
|
onClick ? 'module-avatar--with-click' : null
|
||||||
)}
|
)}
|
||||||
|
ref={innerRef}
|
||||||
|
role={role}
|
||||||
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{hasImage ? this.renderImage() : this.renderNoImage()}
|
{hasImage ? this.renderImage() : this.renderNoImage()}
|
||||||
</div>
|
</div>
|
||||||
|
|
48
ts/components/AvatarPopup.md
Normal file
48
ts/components/AvatarPopup.md
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
### With avatar
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<AvatarPopup
|
||||||
|
color="pink"
|
||||||
|
profileName="John Smith"
|
||||||
|
phoneNumber="(800) 555-0001"
|
||||||
|
avatarPath={util.gifObjectUrl}
|
||||||
|
conversationType="direct"
|
||||||
|
onViewPreferences={(...args) => console.log('onViewPreferences', args)}
|
||||||
|
onViewArchive={(...args) => console.log('onViewArchive', args)}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
||||||
|
### With no avatar
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<AvatarPopup
|
||||||
|
color="green"
|
||||||
|
profileName="John Smith"
|
||||||
|
phoneNumber="(800) 555-0001"
|
||||||
|
conversationType="direct"
|
||||||
|
onViewPreferences={(...args) => console.log('onViewPreferences', args)}
|
||||||
|
onViewArchive={(...args) => console.log('onViewArchive', args)}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
||||||
|
### With empty profileName
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<AvatarPopup
|
||||||
|
color="green"
|
||||||
|
profileName={null}
|
||||||
|
phoneNumber="(800) 555-0001"
|
||||||
|
conversationType="direct"
|
||||||
|
onViewPreferences={(...args) => console.log('onViewPreferences', args)}
|
||||||
|
onViewArchive={(...args) => console.log('onViewArchive', args)}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
71
ts/components/AvatarPopup.tsx
Normal file
71
ts/components/AvatarPopup.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import { Avatar, Props as AvatarProps } from './Avatar';
|
||||||
|
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
|
|
||||||
|
onViewPreferences: () => unknown;
|
||||||
|
onViewArchive: () => unknown;
|
||||||
|
|
||||||
|
// Matches Popper's RefHandler type
|
||||||
|
innerRef?: (ref: HTMLElement | null) => void;
|
||||||
|
style: React.CSSProperties;
|
||||||
|
} & AvatarProps;
|
||||||
|
|
||||||
|
export const AvatarPopup = (props: Props) => {
|
||||||
|
const {
|
||||||
|
i18n,
|
||||||
|
profileName,
|
||||||
|
phoneNumber,
|
||||||
|
onViewPreferences,
|
||||||
|
onViewArchive,
|
||||||
|
style,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const hasProfileName = !isEmpty(profileName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={style} className="module-avatar-popup">
|
||||||
|
<div className="module-avatar-popup__profile">
|
||||||
|
<Avatar {...props} size={52} />
|
||||||
|
<div className="module-avatar-popup__profile__text">
|
||||||
|
<div className="module-avatar-popup__profile__name">
|
||||||
|
{hasProfileName ? profileName : phoneNumber}
|
||||||
|
</div>
|
||||||
|
{hasProfileName ? (
|
||||||
|
<div className="module-avatar-popup__profile__number">
|
||||||
|
{phoneNumber}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className="module-avatar-popup__divider" />
|
||||||
|
<button className="module-avatar-popup__item" onClick={onViewPreferences}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-avatar-popup__item__icon',
|
||||||
|
'module-avatar-popup__item__icon-settings'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="module-avatar-popup__item__text">
|
||||||
|
{i18n('mainMenuSettings')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="module-avatar-popup__item" onClick={onViewArchive}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-avatar-popup__item__icon',
|
||||||
|
'module-avatar-popup__item__icon-archive'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="module-avatar-popup__item__text">
|
||||||
|
{i18n('avatarMenuViewArchive')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,8 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
import { showSettings } from '../shims/Whisper';
|
||||||
import { Avatar } from './Avatar';
|
import { Avatar } from './Avatar';
|
||||||
|
import { AvatarPopup } from './AvatarPopup';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
export interface PropsType {
|
export interface PropsType {
|
||||||
|
@ -42,15 +46,36 @@ export interface PropsType {
|
||||||
|
|
||||||
clearConversationSearch: () => void;
|
clearConversationSearch: () => void;
|
||||||
clearSearch: () => void;
|
clearSearch: () => void;
|
||||||
|
|
||||||
|
showArchivedConversations: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MainHeader extends React.Component<PropsType> {
|
interface StateType {
|
||||||
|
showingAvatarPopup: boolean;
|
||||||
|
popperRoot: HTMLDivElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MainHeader extends React.Component<PropsType, StateType> {
|
||||||
private readonly inputRef: React.RefObject<HTMLInputElement>;
|
private readonly inputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
|
||||||
constructor(props: PropsType) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.inputRef = React.createRef();
|
this.inputRef = React.createRef();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showingAvatarPopup: false,
|
||||||
|
popperRoot: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
const popperRoot = document.createElement('div');
|
||||||
|
document.body.appendChild(popperRoot);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
popperRoot,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: PropsType) {
|
public componentDidUpdate(prevProps: PropsType) {
|
||||||
|
@ -65,6 +90,50 @@ export class MainHeader extends React.Component<PropsType> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public handleOutsideClick = ({ target }: MouseEvent) => {
|
||||||
|
const { popperRoot, showingAvatarPopup } = this.state;
|
||||||
|
|
||||||
|
if (
|
||||||
|
showingAvatarPopup &&
|
||||||
|
popperRoot &&
|
||||||
|
!popperRoot.contains(target as Node)
|
||||||
|
) {
|
||||||
|
this.hideAvatarPopup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public handleOutsideKeyUp = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
this.hideAvatarPopup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public showAvatarPopup = () => {
|
||||||
|
this.setState({
|
||||||
|
showingAvatarPopup: true,
|
||||||
|
});
|
||||||
|
document.addEventListener('click', this.handleOutsideClick);
|
||||||
|
document.addEventListener('keydown', this.handleOutsideKeyUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
public hideAvatarPopup = () => {
|
||||||
|
document.removeEventListener('click', this.handleOutsideClick);
|
||||||
|
document.removeEventListener('keydown', this.handleOutsideKeyUp);
|
||||||
|
this.setState({
|
||||||
|
showingAvatarPopup: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
const { popperRoot } = this.state;
|
||||||
|
|
||||||
|
if (popperRoot) {
|
||||||
|
document.body.removeChild(popperRoot);
|
||||||
|
document.removeEventListener('click', this.handleOutsideClick);
|
||||||
|
document.removeEventListener('keydown', this.handleOutsideKeyUp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line member-ordering
|
// tslint:disable-next-line member-ordering
|
||||||
public search = debounce((searchTerm: string) => {
|
public search = debounce((searchTerm: string) => {
|
||||||
const {
|
const {
|
||||||
|
@ -177,6 +246,7 @@ export class MainHeader extends React.Component<PropsType> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// tslint:disable-next-line:max-func-body-length
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const {
|
||||||
avatarPath,
|
avatarPath,
|
||||||
|
@ -188,7 +258,9 @@ export class MainHeader extends React.Component<PropsType> {
|
||||||
searchConversationId,
|
searchConversationId,
|
||||||
searchConversationName,
|
searchConversationName,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
|
showArchivedConversations,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const { showingAvatarPopup, popperRoot } = this.state;
|
||||||
|
|
||||||
const placeholder = searchConversationName
|
const placeholder = searchConversationName
|
||||||
? i18n('searchIn', [searchConversationName])
|
? i18n('searchIn', [searchConversationName])
|
||||||
|
@ -196,16 +268,53 @@ export class MainHeader extends React.Component<PropsType> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-main-header">
|
<div className="module-main-header">
|
||||||
<Avatar
|
<Manager>
|
||||||
avatarPath={avatarPath}
|
<Reference>
|
||||||
color={color}
|
{({ ref }) => (
|
||||||
conversationType="direct"
|
<Avatar
|
||||||
i18n={i18n}
|
avatarPath={avatarPath}
|
||||||
name={name}
|
color={color}
|
||||||
phoneNumber={phoneNumber}
|
conversationType="direct"
|
||||||
profileName={profileName}
|
i18n={i18n}
|
||||||
size={28}
|
name={name}
|
||||||
/>
|
phoneNumber={phoneNumber}
|
||||||
|
profileName={profileName}
|
||||||
|
size={28}
|
||||||
|
innerRef={ref}
|
||||||
|
onClick={this.showAvatarPopup}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Reference>
|
||||||
|
{showingAvatarPopup && popperRoot
|
||||||
|
? createPortal(
|
||||||
|
<Popper placement="bottom-end">
|
||||||
|
{({ ref, style }) => (
|
||||||
|
<AvatarPopup
|
||||||
|
innerRef={ref}
|
||||||
|
i18n={i18n}
|
||||||
|
style={style}
|
||||||
|
color={color}
|
||||||
|
conversationType="direct"
|
||||||
|
name={name}
|
||||||
|
phoneNumber={phoneNumber}
|
||||||
|
profileName={profileName}
|
||||||
|
avatarPath={avatarPath}
|
||||||
|
size={28}
|
||||||
|
onViewPreferences={() => {
|
||||||
|
showSettings();
|
||||||
|
this.hideAvatarPopup();
|
||||||
|
}}
|
||||||
|
onViewArchive={() => {
|
||||||
|
showArchivedConversations();
|
||||||
|
this.hideAvatarPopup();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Popper>,
|
||||||
|
popperRoot
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</Manager>
|
||||||
<div className="module-main-header__search">
|
<div className="module-main-header__search">
|
||||||
{searchConversationId ? (
|
{searchConversationId ? (
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -11,3 +11,8 @@ export function getBubbleProps(attributes: any) {
|
||||||
|
|
||||||
return model.getPropsForBubble();
|
return model.getPropsForBubble();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function showSettings() {
|
||||||
|
// @ts-ignore
|
||||||
|
window.showSettings();
|
||||||
|
}
|
||||||
|
|
|
@ -7542,7 +7542,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/MainHeader.js",
|
"path": "ts/components/MainHeader.js",
|
||||||
"line": " this.inputRef = react_1.default.createRef();",
|
"line": " this.inputRef = react_1.default.createRef();",
|
||||||
"lineNumber": 87,
|
"lineNumber": 118,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-08-09T21:17:57.798Z",
|
"updated": "2019-08-09T21:17:57.798Z",
|
||||||
"reasonDetail": "Used only to set focus"
|
"reasonDetail": "Used only to set focus"
|
||||||
|
@ -7551,7 +7551,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/MainHeader.tsx",
|
"path": "ts/components/MainHeader.tsx",
|
||||||
"line": " this.inputRef = React.createRef();",
|
"line": " this.inputRef = React.createRef();",
|
||||||
"lineNumber": 53,
|
"lineNumber": 64,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-08-09T21:17:57.798Z",
|
"updated": "2019-08-09T21:17:57.798Z",
|
||||||
"reasonDetail": "Used only to set focus"
|
"reasonDetail": "Used only to set focus"
|
||||||
|
|
Loading…
Add table
Reference in a new issue