New contact popup when clicking on group member or avatar
This commit is contained in:
parent
cd599f92c8
commit
d593f74241
27 changed files with 717 additions and 44 deletions
|
@ -3901,5 +3901,17 @@
|
|||
"countMutedConversationsDescription": {
|
||||
"message": "Count muted conversations in badge count",
|
||||
"description": "Description for counting muted conversations in badge setting"
|
||||
},
|
||||
"ContactModal--message": {
|
||||
"message": "Message",
|
||||
"description": "Button text for send message button in Group Contact Details modal"
|
||||
},
|
||||
"ContactModal--make-admin": {
|
||||
"message": "Make admin",
|
||||
"description": "Button text for make admin button in Group Contact Details modal"
|
||||
},
|
||||
"ContactModal--remove-from-group": {
|
||||
"message": "Remove from group",
|
||||
"description": "Button text for remove from group button in Group Contact Details modal"
|
||||
}
|
||||
}
|
||||
|
|
3
images/icons/v2/leave-group-outline-16.svg
Normal file
3
images/icons/v2/leave-group-outline-16.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.08342 10.875V16.5H4.83342V10.875H6.08342ZM14.0001 0.666664H7.33342C6.67037 0.666664 6.03449 0.930056 5.56565 1.3989C5.09681 1.86774 4.83342 2.50362 4.83342 3.16666V7.125H6.08342V3.16666C6.08342 2.83514 6.21511 2.5172 6.44953 2.28278C6.68395 2.04836 7.00189 1.91666 7.33342 1.91666H14.0001C14.3316 1.91666 14.6495 2.04836 14.884 2.28278C15.1184 2.5172 15.2501 2.83514 15.2501 3.16666V16.5H16.5001V3.16666C16.5001 2.50362 16.2367 1.86774 15.7679 1.3989C15.299 0.930056 14.6631 0.666664 14.0001 0.666664ZM8.92175 4.40416L8.03842 5.2875L10.5184 7.76833L11.3676 8.375H0.666748V9.625H11.3676L10.4151 10.3058L8.03592 12.7142L8.92425 13.5933L13.4901 8.9725L8.92175 4.40416Z" fill="#DEDEDE"/>
|
||||
</svg>
|
After Width: | Height: | Size: 799 B |
|
@ -36,6 +36,9 @@ const {
|
|||
ContactDetail,
|
||||
} = require('../../ts/components/conversation/ContactDetail');
|
||||
const { ContactListItem } = require('../../ts/components/ContactListItem');
|
||||
const {
|
||||
ContactModal,
|
||||
} = require('../../ts/components/conversation/ContactModal');
|
||||
const { Emojify } = require('../../ts/components/conversation/Emojify');
|
||||
const { ErrorModal } = require('../../ts/components/ErrorModal');
|
||||
const { Lightbox } = require('../../ts/components/Lightbox');
|
||||
|
@ -63,6 +66,9 @@ const { createTimeline } = require('../../ts/state/roots/createTimeline');
|
|||
const {
|
||||
createCompositionArea,
|
||||
} = require('../../ts/state/roots/createCompositionArea');
|
||||
const {
|
||||
createContactModal,
|
||||
} = require('../../ts/state/roots/createContactModal');
|
||||
const {
|
||||
createConversationHeader,
|
||||
} = require('../../ts/state/roots/createConversationHeader');
|
||||
|
@ -298,6 +304,7 @@ exports.setup = (options = {}) => {
|
|||
ConfirmationModal,
|
||||
ContactDetail,
|
||||
ContactListItem,
|
||||
ContactModal,
|
||||
Emojify,
|
||||
ErrorModal,
|
||||
getCallingNotificationText,
|
||||
|
@ -317,6 +324,7 @@ exports.setup = (options = {}) => {
|
|||
const Roots = {
|
||||
createCallManager,
|
||||
createCompositionArea,
|
||||
createContactModal,
|
||||
createConversationHeader,
|
||||
createLeftPane,
|
||||
createSafetyNumberViewer,
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
this.ourNumber = textsecure.storage.user.getNumber();
|
||||
this.listenBack = options.listenBack;
|
||||
this.loading = false;
|
||||
this.conversation = options.conversation;
|
||||
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
},
|
||||
|
@ -27,34 +28,23 @@
|
|||
this.contactView = null;
|
||||
}
|
||||
|
||||
const formattedContact = this.model.format();
|
||||
|
||||
this.contactView = new Whisper.ReactWrapperView({
|
||||
className: 'contact-wrapper',
|
||||
Component: window.Signal.Components.ContactListItem,
|
||||
props: {
|
||||
...this.model.format(),
|
||||
onClick: this.showIdentity.bind(this),
|
||||
...formattedContact,
|
||||
onClick: () =>
|
||||
this.conversation.trigger(
|
||||
'show-contact-modal',
|
||||
formattedContact.id
|
||||
),
|
||||
},
|
||||
});
|
||||
this.$el.append(this.contactView.el);
|
||||
return this;
|
||||
},
|
||||
showIdentity() {
|
||||
if (this.model.isMe() || this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.render();
|
||||
|
||||
this.panelView = new Whisper.KeyVerificationPanelView({
|
||||
model: this.model,
|
||||
onLoad: view => {
|
||||
this.loading = false;
|
||||
this.listenBack(view);
|
||||
this.render();
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
className: 'members',
|
||||
toInclude: {
|
||||
listenBack: options.listenBack,
|
||||
conversation: options.conversation,
|
||||
},
|
||||
});
|
||||
this.member_list_view.render();
|
||||
|
|
|
@ -1404,9 +1404,22 @@
|
|||
}
|
||||
|
||||
.module-message__author-avatar {
|
||||
@include button-reset;
|
||||
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: calc(100% + 8px);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
||||
.module-avatar {
|
||||
@include keyboard-mode {
|
||||
box-shadow: 0 0 0 3px $ultramarine-ui-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-message__typing-container {
|
||||
|
@ -4156,11 +4169,33 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
|||
height: 58px;
|
||||
width: 58px;
|
||||
}
|
||||
|
||||
.module-avatar__icon--80.module-avatar__icon--direct {
|
||||
height: 62px;
|
||||
width: 62px;
|
||||
}
|
||||
|
||||
.module-avatar--96 {
|
||||
height: 96px;
|
||||
width: 96px;
|
||||
|
||||
img {
|
||||
height: 96px;
|
||||
width: 96px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-avatar__label--96 {
|
||||
width: 96px;
|
||||
font-size: 48px;
|
||||
line-height: 96px;
|
||||
}
|
||||
|
||||
.module-avatar__icon--96 {
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.module-avatar--112 {
|
||||
height: 112px;
|
||||
width: 112px;
|
||||
|
@ -9657,6 +9692,188 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
|
||||
// Module: Group Contact Details
|
||||
$contact-modal-padding: 18px;
|
||||
.module-contact-modal {
|
||||
@include font-body-2;
|
||||
|
||||
min-width: 250px;
|
||||
padding: $contact-modal-padding;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
@include popper-shadow();
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@include light-theme() {
|
||||
background: $color-white;
|
||||
color: $color-gray-90;
|
||||
}
|
||||
|
||||
@include dark-theme() {
|
||||
background: $color-gray-75;
|
||||
color: $color-gray-05;
|
||||
}
|
||||
|
||||
&__overlay {
|
||||
background: $color-black-alpha-40;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
|
||||
.module-contact-modal__name {
|
||||
@include font-body-1-bold;
|
||||
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.module-contact-modal__profile-and-number {
|
||||
color: $color-gray-60;
|
||||
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
}
|
||||
|
||||
.module-contact-modal__button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin: 12px 0 15px -$contact-modal-padding;
|
||||
width: calc(100% + (#{$contact-modal-padding} * 2));
|
||||
}
|
||||
|
||||
.module-contact-modal__button {
|
||||
@include button-reset;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 7px $contact-modal-padding;
|
||||
width: 100%;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-gray-15;
|
||||
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-60;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include keyboard-mode {
|
||||
background-color: $color-gray-15;
|
||||
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-60;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-contact-modal__bubble-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.module-contact-modal__send-message__bubble-icon {
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/message-outline-24.svg',
|
||||
$color-gray-75
|
||||
);
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/message-outline-24.svg',
|
||||
$color-gray-15
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.module-contact-modal__safety-number__bubble-icon {
|
||||
height: 18px;
|
||||
width: 17px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/safety-number-outline-24.svg',
|
||||
$color-gray-75
|
||||
);
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/safety-number-outline-24.svg',
|
||||
$color-gray-15
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.module-contact-modal__remove-from-group__bubble-icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/leave-group-outline-16.svg',
|
||||
$color-gray-75
|
||||
);
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/leave-group-outline-16.svg',
|
||||
$color-gray-15
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.module-contact-modal__close-button {
|
||||
@include button-reset;
|
||||
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 12px;
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-75);
|
||||
|
||||
@include dark-theme() {
|
||||
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-25);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include keyboard-mode {
|
||||
background-color: $ultramarine-ui-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-background-color {
|
||||
&__default {
|
||||
background-color: $color-black-alpha-40;
|
||||
|
|
|
@ -983,6 +983,10 @@ type WhatIsThis = typeof window.WhatIsThis;
|
|||
if (className.includes('module-main-header__search__input')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (className.includes('module-contact-modal')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// These add listeners to document, but we'll run first
|
||||
|
|
|
@ -18,7 +18,7 @@ export type Props = {
|
|||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
size: 28 | 32 | 52 | 80 | 112;
|
||||
size: 28 | 32 | 52 | 80 | 96 | 112;
|
||||
|
||||
onClick?: () => unknown;
|
||||
|
||||
|
@ -147,7 +147,7 @@ export class Avatar extends React.Component<Props, State> {
|
|||
|
||||
const hasImage = !noteToSelf && avatarPath && !imageBroken;
|
||||
|
||||
if (![28, 32, 52, 80, 112].includes(size)) {
|
||||
if (![28, 32, 52, 80, 96, 112].includes(size)) {
|
||||
throw new Error(`Size ${size} is not supported!`);
|
||||
}
|
||||
|
||||
|
|
83
ts/components/conversation/ContactModal.stories.tsx
Normal file
83
ts/components/conversation/ContactModal.stories.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import { ContactModal, PropsType } from './ContactModal';
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const story = storiesOf('Components/Conversation/ContactModal', module);
|
||||
|
||||
const defaultContact: ConversationType = {
|
||||
id: 'abcdef',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
title: 'Pauline Oliveros',
|
||||
type: 'direct',
|
||||
phoneNumber: '(333) 444-5515',
|
||||
};
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
areWeAdmin: boolean('areWeAdmin', overrideProps.areWeAdmin || false),
|
||||
contact: overrideProps.contact || defaultContact,
|
||||
i18n,
|
||||
isMember: boolean('isMember', overrideProps.isMember || true),
|
||||
onClose: action('onClose'),
|
||||
openConversation: action('openConversation'),
|
||||
removeMember: action('removeMember'),
|
||||
showSafetyNumber: action('showSafetyNumber'),
|
||||
});
|
||||
|
||||
story.add('As non-admin', () => {
|
||||
const props = createProps({
|
||||
areWeAdmin: false,
|
||||
});
|
||||
|
||||
return <ContactModal {...props} />;
|
||||
});
|
||||
|
||||
story.add('As admin', () => {
|
||||
const props = createProps({
|
||||
areWeAdmin: true,
|
||||
});
|
||||
return <ContactModal {...props} />;
|
||||
});
|
||||
|
||||
story.add('As admin, viewing non-member of group', () => {
|
||||
const props = createProps({
|
||||
isMember: false,
|
||||
});
|
||||
|
||||
return <ContactModal {...props} />;
|
||||
});
|
||||
|
||||
story.add('Without phone number', () => {
|
||||
const props = createProps({
|
||||
contact: {
|
||||
...defaultContact,
|
||||
phoneNumber: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return <ContactModal {...props} />;
|
||||
});
|
||||
|
||||
story.add('Viewing self', () => {
|
||||
const props = createProps({
|
||||
contact: {
|
||||
...defaultContact,
|
||||
isMe: true,
|
||||
},
|
||||
});
|
||||
|
||||
return <ContactModal {...props} />;
|
||||
});
|
159
ts/components/conversation/ContactModal.tsx
Normal file
159
ts/components/conversation/ContactModal.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactPortal } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { Avatar } from '../Avatar';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
areWeAdmin: boolean;
|
||||
contact?: ConversationType;
|
||||
readonly i18n: LocalizerType;
|
||||
isMember: boolean;
|
||||
onClose: () => void;
|
||||
openConversation: (conversationId: string) => void;
|
||||
removeMember: (conversationId: string) => void;
|
||||
showSafetyNumber: (conversationId: string) => void;
|
||||
};
|
||||
|
||||
export const ContactModal = ({
|
||||
areWeAdmin,
|
||||
contact,
|
||||
i18n,
|
||||
isMember,
|
||||
onClose,
|
||||
openConversation,
|
||||
removeMember,
|
||||
showSafetyNumber,
|
||||
}: PropsType): ReactPortal | null => {
|
||||
if (!contact) {
|
||||
throw new Error('Contact modal opened without a matching contact');
|
||||
}
|
||||
|
||||
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
||||
const overlayRef = React.useRef<HTMLElement | null>(null);
|
||||
const closeButtonRef = React.useRef<HTMLElement | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
setRoot(div);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(div);
|
||||
setRoot(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (root !== null && closeButtonRef.current) {
|
||||
closeButtonRef.current.focus();
|
||||
}
|
||||
}, [root]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keyup', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keyup', handler);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
const onClickOverlay = (e: React.MouseEvent<HTMLElement>) => {
|
||||
if (e.target === overlayRef.current) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return root
|
||||
? createPortal(
|
||||
<div
|
||||
ref={ref => {
|
||||
overlayRef.current = ref;
|
||||
}}
|
||||
role="presentation"
|
||||
className="module-contact-modal__overlay"
|
||||
onClick={onClickOverlay}
|
||||
>
|
||||
<div className="module-contact-modal">
|
||||
<button
|
||||
ref={r => {
|
||||
closeButtonRef.current = r;
|
||||
}}
|
||||
type="button"
|
||||
className="module-contact-modal__close-button"
|
||||
onClick={onClose}
|
||||
aria-label={i18n('close')}
|
||||
/>
|
||||
<Avatar
|
||||
avatarPath={contact.avatarPath}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={contact.name}
|
||||
profileName={contact.profileName}
|
||||
size={96}
|
||||
title={contact.title}
|
||||
/>
|
||||
<div className="module-contact-modal__name">{contact.title}</div>
|
||||
{contact.phoneNumber && (
|
||||
<div className="module-contact-modal__profile-and-number">
|
||||
{contact.phoneNumber}
|
||||
</div>
|
||||
)}
|
||||
<div className="module-contact-modal__button-container">
|
||||
<button
|
||||
type="button"
|
||||
className="module-contact-modal__button module-contact-modal__send-message"
|
||||
onClick={() => openConversation(contact.id)}
|
||||
>
|
||||
<div className="module-contact-modal__bubble-icon">
|
||||
<div className="module-contact-modal__send-message__bubble-icon" />
|
||||
</div>
|
||||
<span>{i18n('ContactModal--message')}</span>
|
||||
</button>
|
||||
{!contact.isMe && (
|
||||
<button
|
||||
type="button"
|
||||
className="module-contact-modal__button module-contact-modal__safety-number"
|
||||
onClick={() => showSafetyNumber(contact.id)}
|
||||
>
|
||||
<div className="module-contact-modal__bubble-icon">
|
||||
<div className="module-contact-modal__safety-number__bubble-icon" />
|
||||
</div>
|
||||
<span>{i18n('showSafetyNumber')}</span>
|
||||
</button>
|
||||
)}
|
||||
{!contact.isMe && areWeAdmin && isMember && (
|
||||
<button
|
||||
type="button"
|
||||
className="module-contact-modal__button module-contact-modal__remove-from-group"
|
||||
onClick={() => removeMember(contact.id)}
|
||||
>
|
||||
<div className="module-contact-modal__bubble-icon">
|
||||
<div className="module-contact-modal__remove-from-group__bubble-icon" />
|
||||
</div>
|
||||
<span>{i18n('ContactModal--remove-from-group')}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
root
|
||||
)
|
||||
: null;
|
||||
};
|
|
@ -43,6 +43,7 @@ const renderEmojiPicker: Props['renderEmojiPicker'] = ({
|
|||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
attachments: overrideProps.attachments,
|
||||
authorId: overrideProps.authorId || 'some-id',
|
||||
authorColor: overrideProps.authorColor || 'blue',
|
||||
authorAvatarPath: overrideProps.authorAvatarPath,
|
||||
authorTitle: text('authorTitle', overrideProps.authorTitle || ''),
|
||||
|
@ -85,6 +86,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||
selectMessage: action('selectMessage'),
|
||||
showContactDetail: action('showContactDetail'),
|
||||
showContactModal: action('showContactModal'),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
),
|
||||
|
|
|
@ -102,6 +102,7 @@ export type PropsData = {
|
|||
timestamp: number;
|
||||
status?: MessageStatusType;
|
||||
contact?: ContactType;
|
||||
authorId: string;
|
||||
authorTitle: string;
|
||||
authorName?: string;
|
||||
authorProfileName?: string;
|
||||
|
@ -170,6 +171,7 @@ export type PropsActions = {
|
|||
contact: ContactType;
|
||||
signalAccount?: string;
|
||||
}) => void;
|
||||
showContactModal: (contactId: string) => void;
|
||||
|
||||
showVisualAttachment: (options: {
|
||||
attachment: AttachmentType;
|
||||
|
@ -1055,6 +1057,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
public renderAvatar(): JSX.Element | undefined {
|
||||
const {
|
||||
authorAvatarPath,
|
||||
authorId,
|
||||
authorName,
|
||||
authorPhoneNumber,
|
||||
authorProfileName,
|
||||
|
@ -1064,6 +1067,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
conversationType,
|
||||
direction,
|
||||
i18n,
|
||||
showContactModal,
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
|
@ -1071,12 +1075,16 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
conversationType !== 'group' ||
|
||||
direction === 'outgoing'
|
||||
) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return (
|
||||
<div className="module-message__author-avatar">
|
||||
<button
|
||||
type="button"
|
||||
className="module-message__author-avatar"
|
||||
onClick={() => showContactModal(authorId)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Avatar
|
||||
avatarPath={authorAvatarPath}
|
||||
color={authorColor}
|
||||
|
@ -1088,7 +1096,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
title={authorTitle}
|
||||
size={28}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ const i18n = setupI18n('en', enMessages);
|
|||
const story = storiesOf('Components/Conversation/MessageDetail', module);
|
||||
|
||||
const defaultMessage: MessageProps = {
|
||||
authorId: 'some-id',
|
||||
authorTitle: 'Max',
|
||||
canReply: true,
|
||||
canDeleteForEveryone: true,
|
||||
|
@ -41,6 +42,7 @@ const defaultMessage: MessageProps = {
|
|||
retrySend: () => null,
|
||||
scrollToQuotedMessage: () => null,
|
||||
showContactDetail: () => null,
|
||||
showContactModal: () => null,
|
||||
showExpiredIncomingTapToViewToast: () => null,
|
||||
showExpiredOutgoingTapToViewToast: () => null,
|
||||
showMessageDetail: () => null,
|
||||
|
|
|
@ -20,6 +20,7 @@ const i18n = setupI18n('en', enMessages);
|
|||
const story = storiesOf('Components/Conversation/Quote', module);
|
||||
|
||||
const defaultMessageProps: MessagesProps = {
|
||||
authorId: 'some-id',
|
||||
authorTitle: 'Person X',
|
||||
canReply: true,
|
||||
canDeleteForEveryone: true,
|
||||
|
@ -45,6 +46,7 @@ const defaultMessageProps: MessagesProps = {
|
|||
scrollToQuotedMessage: () => null,
|
||||
selectMessage: () => null,
|
||||
showContactDetail: () => null,
|
||||
showContactModal: () => null,
|
||||
showExpiredIncomingTapToViewToast: () => null,
|
||||
showExpiredOutgoingTapToViewToast: () => null,
|
||||
showMessageDetail: () => null,
|
||||
|
|
|
@ -230,6 +230,7 @@ const actions = () => ({
|
|||
showMessageDetail: action('showMessageDetail'),
|
||||
openConversation: action('openConversation'),
|
||||
showContactDetail: action('showContactDetail'),
|
||||
showContactModal: action('showContactModal'),
|
||||
showVisualAttachment: action('showVisualAttachment'),
|
||||
downloadAttachment: action('downloadAttachment'),
|
||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||
|
|
|
@ -47,6 +47,7 @@ const getDefaultProps = () => ({
|
|||
showMessageDetail: action('showMessageDetail'),
|
||||
openConversation: action('openConversation'),
|
||||
showContactDetail: action('showContactDetail'),
|
||||
showContactModal: action('showContactModal'),
|
||||
showVisualAttachment: action('showVisualAttachment'),
|
||||
downloadAttachment: action('downloadAttachment'),
|
||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||
|
|
|
@ -1139,6 +1139,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
areWePending: Boolean(
|
||||
ourConversationId && this.isMemberPending(ourConversationId)
|
||||
),
|
||||
areWeAdmin: this.areWeAdmin(),
|
||||
canChangeTimer: this.canChangeTimer(),
|
||||
avatarPath: this.getAvatarPath()!,
|
||||
color,
|
||||
|
@ -1437,6 +1438,24 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
}
|
||||
}
|
||||
|
||||
async removeFromGroupV2(conversationId: string): Promise<void> {
|
||||
if (this.isGroupV2() && this.isMemberPending(conversationId)) {
|
||||
await this.modifyGroupV2({
|
||||
name: 'removePendingMember',
|
||||
createGroupChange: () => this.removePendingMember(conversationId),
|
||||
});
|
||||
} else if (this.isGroupV2() && this.isMember(conversationId)) {
|
||||
await this.modifyGroupV2({
|
||||
name: 'removeFromGroup',
|
||||
createGroupChange: () => this.removeMember(conversationId),
|
||||
});
|
||||
} else {
|
||||
window.log.error(
|
||||
`removeFromGroupV2: Member ${conversationId} is neither a member nor a pending member of the group`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async syncMessageRequestResponse(response: number): Promise<void> {
|
||||
// In GroupsV2, this may modify the server. We only want to continue if those
|
||||
// server updates were successful.
|
||||
|
@ -4013,6 +4032,14 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
return true;
|
||||
}
|
||||
|
||||
return this.areWeAdmin();
|
||||
}
|
||||
|
||||
areWeAdmin(): boolean {
|
||||
if (!this.isGroupV2()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const memberEnum = window.textsecure.protobuf.Member.Role;
|
||||
const members = this.get('membersV2') || [];
|
||||
const myId = window.ConversationController.getOurConversationId();
|
||||
|
@ -4021,12 +4048,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
return false;
|
||||
}
|
||||
|
||||
const isAdministrator = me.role === memberEnum.ADMINISTRATOR;
|
||||
if (isAdministrator) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return me.role === memberEnum.ADMINISTRATOR;
|
||||
}
|
||||
|
||||
// Set of items to captureChanges on:
|
||||
|
|
|
@ -753,6 +753,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
canReply: this.canReply(),
|
||||
canDeleteForEveryone: this.canDeleteForEveryone(),
|
||||
canDownload: this.canDownload(),
|
||||
authorId: contact.id,
|
||||
authorTitle: contact.title,
|
||||
authorColor,
|
||||
authorName: contact.name,
|
||||
|
|
|
@ -46,6 +46,7 @@ export type ConversationType = {
|
|||
firstName?: string;
|
||||
profileName?: string;
|
||||
avatarPath?: string;
|
||||
areWeAdmin?: boolean;
|
||||
areWePending?: boolean;
|
||||
canChangeTimer?: boolean;
|
||||
color?: ColorType;
|
||||
|
|
21
ts/state/roots/createContactModal.tsx
Normal file
21
ts/state/roots/createContactModal.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { Store } from 'redux';
|
||||
|
||||
import {
|
||||
SmartContactModal,
|
||||
SmartContactModalProps,
|
||||
} from '../smart/ContactModal';
|
||||
|
||||
export const createContactModal = (
|
||||
store: Store,
|
||||
props: SmartContactModalProps
|
||||
): React.ReactElement => (
|
||||
<Provider store={store}>
|
||||
<SmartContactModal {...props} />
|
||||
</Provider>
|
||||
);
|
55
ts/state/smart/ContactModal.tsx
Normal file
55
ts/state/smart/ContactModal.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import {
|
||||
ContactModal,
|
||||
PropsType,
|
||||
} from '../../components/conversation/ContactModal';
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
|
||||
export type SmartContactModalProps = {
|
||||
contactId: string;
|
||||
currentConversationId: string;
|
||||
readonly onClose: () => unknown;
|
||||
readonly openConversation: (conversationId: string) => void;
|
||||
readonly showSafetyNumber: (conversationId: string) => void;
|
||||
readonly removeMember: (conversationId: string) => void;
|
||||
};
|
||||
|
||||
const mapStateToProps = (
|
||||
state: StateType,
|
||||
props: SmartContactModalProps
|
||||
): PropsType => {
|
||||
const { contactId, currentConversationId } = props;
|
||||
|
||||
const currentConversation = getConversationSelector(state)(
|
||||
currentConversationId
|
||||
);
|
||||
const contact = getConversationSelector(state)(contactId);
|
||||
const isMember =
|
||||
contact && currentConversation && currentConversation.members
|
||||
? currentConversation.members.includes(contact)
|
||||
: false;
|
||||
|
||||
const areWeAdmin =
|
||||
currentConversation && currentConversation.areWeAdmin
|
||||
? currentConversation.areWeAdmin
|
||||
: false;
|
||||
|
||||
return {
|
||||
...props,
|
||||
areWeAdmin,
|
||||
contact,
|
||||
i18n: getIntl(state),
|
||||
isMember,
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartContactModal = smart(ContactModal);
|
|
@ -16,6 +16,7 @@ const memberMahershala: ConversationType = {
|
|||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
const memberShia: ConversationType = {
|
||||
|
@ -28,6 +29,7 @@ const memberShia: ConversationType = {
|
|||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
const members: Array<ConversationType> = [memberMahershala, memberShia];
|
||||
|
@ -42,6 +44,7 @@ const singleMember: ConversationType = {
|
|||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
describe('MemberRepository', () => {
|
||||
|
|
|
@ -23,6 +23,7 @@ const me: ConversationType = {
|
|||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
const members: Array<ConversationType> = [
|
||||
|
@ -35,6 +36,7 @@ const members: Array<ConversationType> = [
|
|||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
},
|
||||
{
|
||||
id: '333222',
|
||||
|
@ -45,6 +47,7 @@ const members: Array<ConversationType> = [
|
|||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
},
|
||||
me,
|
||||
];
|
||||
|
|
|
@ -46,6 +46,7 @@ const memberMahershala: ConversationType = {
|
|||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
const memberShia: ConversationType = {
|
||||
|
@ -57,6 +58,7 @@ const memberShia: ConversationType = {
|
|||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
const members: Array<ConversationType> = [memberMahershala, memberShia];
|
||||
|
|
|
@ -370,7 +370,7 @@
|
|||
"rule": "jQuery-append(",
|
||||
"path": "js/views/contact_list_view.js",
|
||||
"line": " this.$el.append(this.contactView.el);",
|
||||
"lineNumber": 38,
|
||||
"lineNumber": 45,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
|
@ -469,7 +469,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/group_member_list_view.js",
|
||||
"line": " this.$('.container').append(this.member_list_view.el);",
|
||||
"lineNumber": 28,
|
||||
"lineNumber": 29,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -478,7 +478,7 @@
|
|||
"rule": "jQuery-append(",
|
||||
"path": "js/views/group_member_list_view.js",
|
||||
"line": " this.$('.container').append(this.member_list_view.el);",
|
||||
"lineNumber": 28,
|
||||
"lineNumber": 29,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -14708,6 +14708,22 @@
|
|||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Only used to focus the element."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/ContactModal.js",
|
||||
"line": " const overlayRef = react_1.default.useRef(null);",
|
||||
"lineNumber": 16,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-11-09T17:48:12.173Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/ContactModal.js",
|
||||
"line": " const closeButtonRef = react_1.default.useRef(null);",
|
||||
"lineNumber": 17,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-11-10T21:27:04.909Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/ConversationHeader.js",
|
||||
|
@ -14792,23 +14808,23 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
|
||||
"lineNumber": 221,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-09-08T20:19:01.913Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||
"lineNumber": 223,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-09-08T20:19:01.913Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||
"lineNumber": 225,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-09-08T20:19:01.913Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " > = React.createRef();",
|
||||
"lineNumber": 227,
|
||||
"lineNumber": 229,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-08-28T19:36:40.817Z"
|
||||
},
|
||||
|
@ -15160,4 +15176,4 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-09-08T23:07:22.682Z"
|
||||
}
|
||||
]
|
||||
]
|
|
@ -298,6 +298,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
this.listenTo(this.model, 'attach-file', this.onChooseAttachment);
|
||||
this.listenTo(this.model, 'escape-pressed', this.resetPanel);
|
||||
this.listenTo(this.model, 'show-message-details', this.showMessageDetail);
|
||||
this.listenTo(this.model, 'show-contact-modal', this.showContactModal);
|
||||
this.listenTo(this.model, 'toggle-reply', (messageId: any) => {
|
||||
const target = this.quote || !messageId ? null : messageId;
|
||||
this.setQuoteMessage(target);
|
||||
|
@ -750,6 +751,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
const showMessageDetail = (messageId: any) => {
|
||||
this.showMessageDetail(messageId);
|
||||
};
|
||||
const showContactModal = (contactId: string) => {
|
||||
this.showContactModal(contactId);
|
||||
};
|
||||
const openConversation = (conversationId: any, messageId: any) => {
|
||||
this.openConversation(conversationId, messageId);
|
||||
};
|
||||
|
@ -941,6 +945,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
retrySend,
|
||||
scrollToQuotedMessage,
|
||||
showContactDetail,
|
||||
showContactModal,
|
||||
showIdentity,
|
||||
showMessageDetail,
|
||||
showVisualAttachment,
|
||||
|
@ -1222,6 +1227,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
if (this.captionEditorView) {
|
||||
this.captionEditorView.remove();
|
||||
}
|
||||
if (this.contactModalView) {
|
||||
this.contactModalView.remove();
|
||||
}
|
||||
if (this.stickerButtonView) {
|
||||
this.stickerButtonView.remove();
|
||||
}
|
||||
|
@ -2252,6 +2260,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
// we pass this in to allow nested panels
|
||||
listenBack: this.listenBack.bind(this),
|
||||
needVerify: options.needVerify,
|
||||
conversation: this.model,
|
||||
});
|
||||
|
||||
this.listenBack(view);
|
||||
|
@ -2592,6 +2601,49 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
window.Signal.Backbone.Views.Lightbox.show(this.lightboxGalleryView.el);
|
||||
},
|
||||
|
||||
showContactModal(contactId: string) {
|
||||
if (this.contactModalView) {
|
||||
this.contactModalView.remove();
|
||||
this.contactModalView = null;
|
||||
}
|
||||
|
||||
this.previousFocus = document.activeElement;
|
||||
|
||||
const hideContactModal = () => {
|
||||
if (this.contactModalView) {
|
||||
this.contactModalView.remove();
|
||||
this.contactModalView = null;
|
||||
if (this.previousFocus && this.previousFocus.focus) {
|
||||
this.previousFocus.focus();
|
||||
this.previousFocus = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.contactModalView = new Whisper.ReactWrapperView({
|
||||
className: 'progress-modal-wrapper',
|
||||
JSX: window.Signal.State.Roots.createContactModal(window.reduxStore, {
|
||||
contactId,
|
||||
currentConversationId: this.model.id,
|
||||
onClose: hideContactModal,
|
||||
openConversation: (conversationId: string) => {
|
||||
hideContactModal();
|
||||
this.openConversation(conversationId);
|
||||
},
|
||||
showSafetyNumber: (conversationId: string) => {
|
||||
hideContactModal();
|
||||
this.showSafetyNumber(conversationId);
|
||||
},
|
||||
removeMember: (conversationId: string) => {
|
||||
hideContactModal();
|
||||
this.model.removeFromGroupV2(conversationId);
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
this.contactModalView.render();
|
||||
},
|
||||
|
||||
showMessageDetail(messageId: any) {
|
||||
const message = this.model.messageCollection.get(messageId);
|
||||
if (!message) {
|
||||
|
|
4
ts/window.d.ts
vendored
4
ts/window.d.ts
vendored
|
@ -34,6 +34,7 @@ import { ReduxActions } from './state/types';
|
|||
import { createStore } from './state/createStore';
|
||||
import { createCallManager } from './state/roots/createCallManager';
|
||||
import { createCompositionArea } from './state/roots/createCompositionArea';
|
||||
import { createContactModal } from './state/roots/createContactModal';
|
||||
import { createConversationHeader } from './state/roots/createConversationHeader';
|
||||
import { createLeftPane } from './state/roots/createLeftPane';
|
||||
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
||||
|
@ -65,6 +66,7 @@ import { combineNames } from './util';
|
|||
import { BatcherType } from './util/batcher';
|
||||
import { ErrorModal } from './components/ErrorModal';
|
||||
import { ProgressModal } from './components/ProgressModal';
|
||||
import { ContactModal } from './components/conversation/ContactModal';
|
||||
|
||||
export { Long } from 'long';
|
||||
|
||||
|
@ -394,6 +396,7 @@ declare global {
|
|||
CaptionEditor: any;
|
||||
ContactDetail: any;
|
||||
ErrorModal: typeof ErrorModal;
|
||||
ContactModal: typeof ContactModal;
|
||||
Lightbox: any;
|
||||
LightboxGallery: any;
|
||||
MediaGallery: any;
|
||||
|
@ -425,6 +428,7 @@ declare global {
|
|||
Roots: {
|
||||
createCallManager: typeof createCallManager;
|
||||
createCompositionArea: typeof createCompositionArea;
|
||||
createContactModal: typeof createContactModal;
|
||||
createConversationHeader: typeof createConversationHeader;
|
||||
createLeftPane: typeof createLeftPane;
|
||||
createSafetyNumberViewer: typeof createSafetyNumberViewer;
|
||||
|
|
Loading…
Reference in a new issue