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
|
@ -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…
Add table
Add a link
Reference in a new issue