New contact popup when clicking on group member or avatar

This commit is contained in:
Chris Svenningsen 2020-11-11 09:36:05 -08:00 committed by GitHub
parent cd599f92c8
commit d593f74241
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 717 additions and 44 deletions

View file

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

View 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

View file

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

View file

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

View file

@ -21,6 +21,7 @@
className: 'members',
toInclude: {
listenBack: options.listenBack,
conversation: options.conversation,
},
});
this.member_list_view.render();

View file

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

View file

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

View file

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

View 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} />;
});

View 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;
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -46,6 +46,7 @@ export type ConversationType = {
firstName?: string;
profileName?: string;
avatarPath?: string;
areWeAdmin?: boolean;
areWePending?: boolean;
canChangeTimer?: boolean;
color?: ColorType;

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

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

View file

@ -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', () => {

View file

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

View file

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

View file

@ -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"
}
]
]

View file

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

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