Modernize ContactModal
This commit is contained in:
parent
1d2fcde49f
commit
c05d23e628
21 changed files with 426 additions and 493 deletions
|
@ -67,9 +67,6 @@ const {
|
|||
const {
|
||||
createCompositionArea,
|
||||
} = require('../../ts/state/roots/createCompositionArea');
|
||||
const {
|
||||
createContactModal,
|
||||
} = require('../../ts/state/roots/createContactModal');
|
||||
const {
|
||||
createConversationDetails,
|
||||
} = require('../../ts/state/roots/createConversationDetails');
|
||||
|
@ -363,7 +360,6 @@ exports.setup = (options = {}) => {
|
|||
createApp,
|
||||
createChatColorPicker,
|
||||
createCompositionArea,
|
||||
createContactModal,
|
||||
createConversationDetails,
|
||||
createConversationHeader,
|
||||
createForwardMessageModal,
|
||||
|
|
|
@ -8812,213 +8812,6 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
|
||||
// Module: Group Contact Details
|
||||
|
||||
$contact-modal-padding: 18px;
|
||||
.module-contact-modal {
|
||||
@include font-body-2;
|
||||
|
||||
min-width: 280px;
|
||||
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-title-2;
|
||||
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.module-contact-modal__info {
|
||||
text-align: center;
|
||||
max-width: 248px;
|
||||
margin-top: 8px;
|
||||
|
||||
@include light-theme {
|
||||
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-keyboard-mode {
|
||||
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__make-admin__bubble-icon {
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/group-outline-24.svg',
|
||||
$color-gray-75
|
||||
);
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/group-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: 24px;
|
||||
height: 24px;
|
||||
|
||||
@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: $color-ultramarine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-background-color {
|
||||
&__default {
|
||||
background-color: $color-black-alpha-40;
|
||||
|
|
148
stylesheets/components/ContactModal.scss
Normal file
148
stylesheets/components/ContactModal.scss
Normal file
|
@ -0,0 +1,148 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.ContactModal {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
&__name {
|
||||
@include font-title-2;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
&__info {
|
||||
text-align: center;
|
||||
max-width: 248px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__button {
|
||||
@include button-reset;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 7px 16px;
|
||||
width: 100%;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-gray-02;
|
||||
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-80;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include keyboard-mode {
|
||||
background-color: $color-gray-02;
|
||||
}
|
||||
|
||||
@include dark-keyboard-mode {
|
||||
background-color: $color-gray-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__bubble-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
&__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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__make-admin__bubble-icon {
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/group-outline-24.svg',
|
||||
$color-gray-75
|
||||
);
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/group-outline-24.svg',
|
||||
$color-gray-15
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__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-Modal.ContactModal__modal .ContactModal__modal__body {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
|
@ -87,7 +87,7 @@
|
|||
padding: 0 16px 16px 16px;
|
||||
border-top: 1px solid transparent;
|
||||
// If there's a header, just the body scrolls
|
||||
overflow-y: scroll; // scroll so that the padding is always there
|
||||
overflow-y: overlay;
|
||||
overflow-x: auto;
|
||||
|
||||
&--scrolled {
|
||||
|
@ -105,7 +105,7 @@
|
|||
&--no-header {
|
||||
padding: 16px;
|
||||
// If there's no header, the whole thing scrolls
|
||||
overflow-y: scroll; // scroll so that the padding is always there
|
||||
overflow-y: overlay;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
@import './components/ChatColorPicker.scss';
|
||||
@import './components/Checkbox.scss';
|
||||
@import './components/CompositionArea.scss';
|
||||
@import './components/ContactModal.scss';
|
||||
@import './components/ContactName.scss';
|
||||
@import './components/ContactPill.scss';
|
||||
@import './components/ContactPills.scss';
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ContactModalStateType } from '../state/ducks/globalModals';
|
||||
|
||||
type PropsType = {
|
||||
// ContactModal
|
||||
contactModalState?: ContactModalStateType;
|
||||
renderContactModal: () => JSX.Element;
|
||||
// ProfileEditor
|
||||
isProfileEditorVisible: boolean;
|
||||
renderProfileEditor: () => JSX.Element;
|
||||
};
|
||||
|
||||
export const GlobalModalContainer = ({
|
||||
// ContactModal
|
||||
contactModalState,
|
||||
renderContactModal,
|
||||
// ProfileEditor
|
||||
isProfileEditorVisible,
|
||||
renderProfileEditor,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
if (contactModalState) {
|
||||
return renderContactModal();
|
||||
}
|
||||
|
||||
if (isProfileEditorVisible) {
|
||||
return renderProfileEditor();
|
||||
}
|
||||
|
|
|
@ -28,15 +28,17 @@ const defaultContact: ConversationType = getDefaultConversation({
|
|||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
areWeAdmin: boolean('areWeAdmin', overrideProps.areWeAdmin || false),
|
||||
contact: overrideProps.contact || defaultContact,
|
||||
hideContactModal: action('hideContactModal'),
|
||||
i18n,
|
||||
isAdmin: boolean('isAdmin', overrideProps.isAdmin || false),
|
||||
isMember: boolean('isMember', overrideProps.isMember || true),
|
||||
onClose: action('onClose'),
|
||||
openConversation: action('openConversation'),
|
||||
removeMember: action('removeMember'),
|
||||
showSafetyNumber: action('showSafetyNumber'),
|
||||
openConversationInternal: action('openConversationInternal'),
|
||||
removeMemberFromGroup: action('removeMemberFromGroup'),
|
||||
showSafetyNumberInConversation: action('showSafetyNumberInConversation'),
|
||||
toggleAdmin: action('toggleAdmin'),
|
||||
updateSharedGroups: action('updateSharedGroups'),
|
||||
updateConversationModelSharedGroups: action(
|
||||
'updateConversationModelSharedGroups'
|
||||
),
|
||||
});
|
||||
|
||||
story.add('As non-admin', () => {
|
||||
|
|
|
@ -1,103 +1,73 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactPortal, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { About } from './About';
|
||||
import { Avatar } from '../Avatar';
|
||||
import { AvatarLightbox } from '../AvatarLightbox';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { Modal } from '../Modal';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { SharedGroupNames } from '../SharedGroupNames';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
|
||||
export type PropsType = {
|
||||
export type PropsDataType = {
|
||||
areWeAdmin: boolean;
|
||||
contact?: ConversationType;
|
||||
conversationId?: string;
|
||||
readonly i18n: LocalizerType;
|
||||
isAdmin: boolean;
|
||||
isMember: boolean;
|
||||
onClose: () => void;
|
||||
openConversation: (conversationId: string) => void;
|
||||
removeMember: (conversationId: string) => void;
|
||||
showSafetyNumber: (conversationId: string) => void;
|
||||
toggleAdmin: (conversationId: string) => void;
|
||||
updateSharedGroups: () => void;
|
||||
};
|
||||
|
||||
type PropsActionType = {
|
||||
hideContactModal: () => void;
|
||||
openConversationInternal: (
|
||||
options: Readonly<{
|
||||
conversationId: string;
|
||||
messageId?: string;
|
||||
switchToAssociatedView?: boolean;
|
||||
}>
|
||||
) => void;
|
||||
removeMemberFromGroup: (conversationId: string, contactId: string) => void;
|
||||
showSafetyNumberInConversation: (conversationId: string) => void;
|
||||
toggleAdmin: (conversationId: string, contactId: string) => void;
|
||||
updateConversationModelSharedGroups: (conversationId: string) => void;
|
||||
};
|
||||
|
||||
export type PropsType = PropsDataType & PropsActionType;
|
||||
|
||||
export const ContactModal = ({
|
||||
areWeAdmin,
|
||||
contact,
|
||||
conversationId,
|
||||
hideContactModal,
|
||||
i18n,
|
||||
isAdmin,
|
||||
isMember,
|
||||
onClose,
|
||||
openConversation,
|
||||
removeMember,
|
||||
showSafetyNumber,
|
||||
openConversationInternal,
|
||||
removeMemberFromGroup,
|
||||
showSafetyNumberInConversation,
|
||||
toggleAdmin,
|
||||
updateSharedGroups,
|
||||
}: PropsType): ReactPortal | null => {
|
||||
updateConversationModelSharedGroups,
|
||||
}: PropsType): JSX.Element => {
|
||||
if (!contact) {
|
||||
throw new Error('Contact modal opened without a matching contact');
|
||||
}
|
||||
|
||||
const [root, setRoot] = useState<HTMLElement | null>(null);
|
||||
const overlayRef = useRef<HTMLElement | null>(null);
|
||||
const closeButtonRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const [showingAvatar, setShowingAvatar] = useState(false);
|
||||
const [confirmToggleAdmin, setConfirmToggleAdmin] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
setRoot(div);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(div);
|
||||
setRoot(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Kick off the expensive hydration of the current sharedGroupNames
|
||||
updateSharedGroups();
|
||||
}, [updateSharedGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
if (root !== null && closeButtonRef.current) {
|
||||
closeButtonRef.current.focus();
|
||||
if (conversationId) {
|
||||
// Kick off the expensive hydration of the current sharedGroupNames
|
||||
updateConversationModelSharedGroups(conversationId);
|
||||
}
|
||||
}, [root]);
|
||||
}, [conversationId, updateConversationModelSharedGroups]);
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
let content: JSX.Element;
|
||||
if (showingAvatar) {
|
||||
content = (
|
||||
return (
|
||||
<AvatarLightbox
|
||||
avatarColor={contact.color}
|
||||
avatarPath={contact.avatarPath}
|
||||
|
@ -106,18 +76,16 @@ export const ContactModal = ({
|
|||
onClose={() => setShowingAvatar(false)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<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')}
|
||||
/>
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
moduleClassName="ContactModal__modal"
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={hideContactModal}
|
||||
>
|
||||
<div className="ContactModal">
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPath={contact.avatarPath}
|
||||
|
@ -133,53 +101,59 @@ export const ContactModal = ({
|
|||
unblurredAvatarPath={contact.unblurredAvatarPath}
|
||||
onClick={() => setShowingAvatar(true)}
|
||||
/>
|
||||
<div className="module-contact-modal__name">{contact.title}</div>
|
||||
<div className="ContactModal__name">{contact.title}</div>
|
||||
<div className="module-about__container">
|
||||
<About text={contact.about} />
|
||||
</div>
|
||||
{contact.phoneNumber && (
|
||||
<div className="module-contact-modal__info">
|
||||
{contact.phoneNumber}
|
||||
<div className="ContactModal__info">{contact.phoneNumber}</div>
|
||||
)}
|
||||
{!contact.isMe && (
|
||||
<div className="ContactModal__info">
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={contact.sharedGroupNames || []}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="module-contact-modal__info">
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={contact.sharedGroupNames || []}
|
||||
/>
|
||||
</div>
|
||||
<div className="module-contact-modal__button-container">
|
||||
<div className="ContactModal__button-container">
|
||||
<button
|
||||
type="button"
|
||||
className="module-contact-modal__button module-contact-modal__send-message"
|
||||
onClick={() => openConversation(contact.id)}
|
||||
className="ContactModal__button ContactModal__send-message"
|
||||
onClick={() => {
|
||||
hideContactModal();
|
||||
openConversationInternal({ conversationId: contact.id });
|
||||
}}
|
||||
>
|
||||
<div className="module-contact-modal__bubble-icon">
|
||||
<div className="module-contact-modal__send-message__bubble-icon" />
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__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)}
|
||||
className="ContactModal__button ContactModal__safety-number"
|
||||
onClick={() => {
|
||||
hideContactModal();
|
||||
showSafetyNumberInConversation(contact.id);
|
||||
}}
|
||||
>
|
||||
<div className="module-contact-modal__bubble-icon">
|
||||
<div className="module-contact-modal__safety-number__bubble-icon" />
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__safety-number__bubble-icon" />
|
||||
</div>
|
||||
<span>{i18n('showSafetyNumber')}</span>
|
||||
</button>
|
||||
)}
|
||||
{!contact.isMe && areWeAdmin && isMember && (
|
||||
{!contact.isMe && areWeAdmin && isMember && conversationId && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="module-contact-modal__button module-contact-modal__make-admin"
|
||||
onClick={() => toggleAdmin(contact.id)}
|
||||
className="ContactModal__button ContactModal__make-admin"
|
||||
onClick={() => setConfirmToggleAdmin(true)}
|
||||
>
|
||||
<div className="module-contact-modal__bubble-icon">
|
||||
<div className="module-contact-modal__make-admin__bubble-icon" />
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__make-admin__bubble-icon" />
|
||||
</div>
|
||||
{isAdmin ? (
|
||||
<span>{i18n('ContactModal--rm-admin')}</span>
|
||||
|
@ -189,34 +163,38 @@ export const ContactModal = ({
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="module-contact-modal__button module-contact-modal__remove-from-group"
|
||||
onClick={() => removeMember(contact.id)}
|
||||
className="ContactModal__button ContactModal__remove-from-group"
|
||||
onClick={() =>
|
||||
removeMemberFromGroup(conversationId, contact.id)
|
||||
}
|
||||
>
|
||||
<div className="module-contact-modal__bubble-icon">
|
||||
<div className="module-contact-modal__remove-from-group__bubble-icon" />
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__remove-from-group__bubble-icon" />
|
||||
</div>
|
||||
<span>{i18n('ContactModal--remove-from-group')}</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{confirmToggleAdmin && conversationId && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
action: () => toggleAdmin(conversationId, contact.id),
|
||||
text: isAdmin
|
||||
? i18n('ContactModal--rm-admin')
|
||||
: i18n('ContactModal--make-admin'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => setConfirmToggleAdmin(false)}
|
||||
>
|
||||
{isAdmin
|
||||
? i18n('ContactModal--rm-admin-info', [contact.title])
|
||||
: i18n('ContactModal--make-admin-info', [contact.title])}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return root
|
||||
? createPortal(
|
||||
<div
|
||||
ref={ref => {
|
||||
overlayRef.current = ref;
|
||||
}}
|
||||
role="presentation"
|
||||
className="module-contact-modal__overlay"
|
||||
onClick={onClickOverlay}
|
||||
>
|
||||
{content}
|
||||
</div>,
|
||||
root
|
||||
)
|
||||
: null;
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -60,7 +60,6 @@ export type StateProps = {
|
|||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||
setDisappearingMessages: (seconds: number) => void;
|
||||
showAllMedia: () => void;
|
||||
showContactModal: (conversationId: string) => void;
|
||||
showGroupChatColorEditor: () => void;
|
||||
showGroupLinkManagement: () => void;
|
||||
showGroupV2Permissions: () => void;
|
||||
|
@ -86,6 +85,7 @@ type ActionProps = {
|
|||
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
||||
replaceAvatar: ReplaceAvatarActionType;
|
||||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
showContactModal: (contactId: string, conversationId: string) => void;
|
||||
};
|
||||
|
||||
export type Props = StateProps & ActionProps;
|
||||
|
@ -329,6 +329,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
|
||||
<ConversationDetailsMembershipList
|
||||
canAddNewMembers={canEditGroupInfo}
|
||||
conversationId={conversation.id}
|
||||
i18n={i18n}
|
||||
memberships={memberships}
|
||||
showContactModal={showContactModal}
|
||||
|
|
|
@ -44,6 +44,7 @@ const createProps = (overrideProps: Partial<Props>): Props => ({
|
|||
canAddNewMembers: isBoolean(overrideProps.canAddNewMembers)
|
||||
? overrideProps.canAddNewMembers
|
||||
: false,
|
||||
conversationId: '123',
|
||||
i18n,
|
||||
memberships: overrideProps.memberships || [],
|
||||
showContactModal: action('showContactModal'),
|
||||
|
|
|
@ -19,10 +19,11 @@ export type GroupV2Membership = {
|
|||
|
||||
export type Props = {
|
||||
canAddNewMembers: boolean;
|
||||
conversationId: string;
|
||||
i18n: LocalizerType;
|
||||
maxShownMemberCount?: number;
|
||||
memberships: Array<GroupV2Membership>;
|
||||
showContactModal: (conversationId: string) => void;
|
||||
showContactModal: (contactId: string, conversationId: string) => void;
|
||||
startAddingNewMembers?: () => void;
|
||||
};
|
||||
|
||||
|
@ -67,6 +68,7 @@ function sortMemberships(
|
|||
|
||||
export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
|
||||
canAddNewMembers,
|
||||
conversationId,
|
||||
i18n,
|
||||
maxShownMemberCount = 5,
|
||||
memberships,
|
||||
|
@ -101,7 +103,7 @@ export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
|
|||
{sortedMemberships.slice(0, membersToShow).map(({ isAdmin, member }) => (
|
||||
<PanelRow
|
||||
key={member.id}
|
||||
onClick={() => showContactModal(member.id)}
|
||||
onClick={() => showContactModal(member.id, conversationId)}
|
||||
icon={
|
||||
<Avatar
|
||||
conversationType="direct"
|
||||
|
|
|
@ -58,6 +58,7 @@ import {
|
|||
import { AvatarDataType, getDefaultAvatars } from '../../types/Avatar';
|
||||
import { getAvatarData } from '../../util/getAvatarData';
|
||||
import { isSameAvatarData } from '../../util/isSameAvatarData';
|
||||
import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper';
|
||||
|
||||
import { NoopActionType } from './noop';
|
||||
|
||||
|
@ -780,6 +781,7 @@ export const actions = {
|
|||
openConversationInternal,
|
||||
removeAllConversations,
|
||||
removeCustomColorOnConversations,
|
||||
removeMemberFromGroup,
|
||||
repairNewestMessage,
|
||||
repairOldestMessage,
|
||||
replaceAvatar,
|
||||
|
@ -803,11 +805,14 @@ export const actions = {
|
|||
showArchivedConversations,
|
||||
showChooseGroupMembers,
|
||||
showInbox,
|
||||
showSafetyNumberInConversation,
|
||||
startComposing,
|
||||
startNewConversationFromPhoneNumber,
|
||||
startSettingGroupMetadata,
|
||||
toggleAdmin,
|
||||
toggleConversationInChooseMembers,
|
||||
toggleComposeEditingAvatar,
|
||||
updateConversationModelSharedGroups,
|
||||
verifyConversationsStoppingMessageSend,
|
||||
};
|
||||
|
||||
|
@ -1720,6 +1725,73 @@ function openConversationExternal(
|
|||
};
|
||||
}
|
||||
|
||||
function removeMemberFromGroup(
|
||||
conversationId: string,
|
||||
contactId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return dispatch => {
|
||||
const conversationModel = window.ConversationController.get(conversationId);
|
||||
if (conversationModel) {
|
||||
const idForLogging = conversationModel.idForLogging();
|
||||
longRunningTaskWrapper({
|
||||
name: 'removeMemberFromGroup',
|
||||
idForLogging,
|
||||
task: () => conversationModel.removeFromGroupV2(contactId),
|
||||
});
|
||||
}
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function toggleAdmin(
|
||||
conversationId: string,
|
||||
contactId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return dispatch => {
|
||||
const conversationModel = window.ConversationController.get(conversationId);
|
||||
if (conversationModel) {
|
||||
conversationModel.toggleAdmin(contactId);
|
||||
}
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function updateConversationModelSharedGroups(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return dispatch => {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (conversation && conversation.throttledUpdateSharedGroups) {
|
||||
conversation.throttledUpdateSharedGroups();
|
||||
}
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function showSafetyNumberInConversation(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return dispatch => {
|
||||
window.Whisper.events.trigger(
|
||||
'showSafetyNumberInConversation',
|
||||
conversationId
|
||||
);
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function showInbox(): ShowInboxActionType {
|
||||
return {
|
||||
type: 'SHOW_INBOX',
|
||||
|
|
|
@ -4,16 +4,33 @@
|
|||
// State
|
||||
|
||||
export type GlobalModalsStateType = {
|
||||
readonly contactModalState?: ContactModalStateType;
|
||||
readonly isProfileEditorVisible: boolean;
|
||||
readonly profileEditorHasError: boolean;
|
||||
};
|
||||
|
||||
// Actions
|
||||
|
||||
const HIDE_CONTACT_MODAL = 'globalModals/HIDE_CONTACT_MODAL';
|
||||
const SHOW_CONTACT_MODAL = 'globalModals/SHOW_CONTACT_MODAL';
|
||||
const TOGGLE_PROFILE_EDITOR = 'globalModals/TOGGLE_PROFILE_EDITOR';
|
||||
export const TOGGLE_PROFILE_EDITOR_ERROR =
|
||||
'globalModals/TOGGLE_PROFILE_EDITOR_ERROR';
|
||||
|
||||
export type ContactModalStateType = {
|
||||
contactId: string;
|
||||
conversationId?: string;
|
||||
};
|
||||
|
||||
type HideContactModalActionType = {
|
||||
type: typeof HIDE_CONTACT_MODAL;
|
||||
};
|
||||
|
||||
type ShowContactModalActionType = {
|
||||
type: typeof SHOW_CONTACT_MODAL;
|
||||
payload: ContactModalStateType;
|
||||
};
|
||||
|
||||
type ToggleProfileEditorActionType = {
|
||||
type: typeof TOGGLE_PROFILE_EDITOR;
|
||||
};
|
||||
|
@ -23,16 +40,39 @@ export type ToggleProfileEditorErrorActionType = {
|
|||
};
|
||||
|
||||
export type GlobalModalsActionType =
|
||||
| HideContactModalActionType
|
||||
| ShowContactModalActionType
|
||||
| ToggleProfileEditorActionType
|
||||
| ToggleProfileEditorErrorActionType;
|
||||
|
||||
// Action Creators
|
||||
|
||||
export const actions = {
|
||||
hideContactModal,
|
||||
showContactModal,
|
||||
toggleProfileEditor,
|
||||
toggleProfileEditorHasError,
|
||||
};
|
||||
|
||||
function hideContactModal(): HideContactModalActionType {
|
||||
return {
|
||||
type: HIDE_CONTACT_MODAL,
|
||||
};
|
||||
}
|
||||
|
||||
function showContactModal(
|
||||
contactId: string,
|
||||
conversationId?: string
|
||||
): ShowContactModalActionType {
|
||||
return {
|
||||
type: SHOW_CONTACT_MODAL,
|
||||
payload: {
|
||||
contactId,
|
||||
conversationId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function toggleProfileEditor(): ToggleProfileEditorActionType {
|
||||
return { type: TOGGLE_PROFILE_EDITOR };
|
||||
}
|
||||
|
@ -68,5 +108,19 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === SHOW_CONTACT_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
contactModalState: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === HIDE_CONTACT_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
contactModalState: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
// 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>
|
||||
);
|
|
@ -5,33 +5,18 @@ import { connect } from 'react-redux';
|
|||
import { mapDispatchToProps } from '../actions';
|
||||
import {
|
||||
ContactModal,
|
||||
PropsType,
|
||||
PropsDataType,
|
||||
} 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 removeMember: (conversationId: string) => void;
|
||||
readonly showSafetyNumber: (conversationId: string) => void;
|
||||
readonly toggleAdmin: (conversationId: string) => void;
|
||||
readonly updateSharedGroups: () => void;
|
||||
};
|
||||
const mapStateToProps = (state: StateType): PropsDataType => {
|
||||
const { contactId, conversationId } =
|
||||
state.globalModals.contactModalState || {};
|
||||
|
||||
const mapStateToProps = (
|
||||
state: StateType,
|
||||
props: SmartContactModalProps
|
||||
): PropsType => {
|
||||
const { contactId, currentConversationId } = props;
|
||||
|
||||
const currentConversation = getConversationSelector(state)(
|
||||
currentConversationId
|
||||
);
|
||||
const currentConversation = getConversationSelector(state)(conversationId);
|
||||
const contact = getConversationSelector(state)(contactId);
|
||||
|
||||
const areWeAdmin =
|
||||
|
@ -51,9 +36,9 @@ const mapStateToProps = (
|
|||
}
|
||||
|
||||
return {
|
||||
...props,
|
||||
areWeAdmin,
|
||||
contact,
|
||||
conversationId,
|
||||
i18n: getIntl(state),
|
||||
isAdmin,
|
||||
isMember,
|
||||
|
|
|
@ -25,7 +25,6 @@ export type SmartConversationDetailsProps = {
|
|||
loadRecentMediaItems: (limit: number) => void;
|
||||
setDisappearingMessages: (seconds: number) => void;
|
||||
showAllMedia: () => void;
|
||||
showContactModal: (conversationId: string) => void;
|
||||
showGroupChatColorEditor: () => void;
|
||||
showGroupLinkManagement: () => void;
|
||||
showGroupV2Permissions: () => void;
|
||||
|
|
|
@ -7,6 +7,7 @@ import { mapDispatchToProps } from '../actions';
|
|||
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
|
||||
import { StateType } from '../reducer';
|
||||
import { SmartProfileEditorModal } from './ProfileEditorModal';
|
||||
import { SmartContactModal } from './ContactModal';
|
||||
|
||||
const FilteredSmartProfileEditorModal = SmartProfileEditorModal;
|
||||
|
||||
|
@ -14,9 +15,14 @@ function renderProfileEditor(): JSX.Element {
|
|||
return <FilteredSmartProfileEditorModal />;
|
||||
}
|
||||
|
||||
function renderContactModal(): JSX.Element {
|
||||
return <SmartContactModal />;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
return {
|
||||
...state.globalModals,
|
||||
renderContactModal,
|
||||
renderProfileEditor,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12717,20 +12717,6 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-07-30T16:57:33.618Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/ContactModal.tsx",
|
||||
"line": " const overlayRef = useRef<HTMLElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-08-03T21:17:38.615Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/ContactModal.tsx",
|
||||
"line": " const closeButtonRef = useRef<HTMLElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-08-03T21:17:38.615Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/ConversationHeader.js",
|
||||
|
|
|
@ -646,22 +646,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
return this;
|
||||
}
|
||||
|
||||
getMuteExpirationLabel(): string | undefined {
|
||||
const muteExpiresAt = this.model.get('muteExpiresAt');
|
||||
if (!this.model.isMuted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const today = window.moment(Date.now());
|
||||
const expires = window.moment(muteExpiresAt);
|
||||
|
||||
if (today.isSame(expires, 'day')) {
|
||||
return expires.format('hh:mm A');
|
||||
}
|
||||
|
||||
return expires.format('M/D/YY, hh:mm A');
|
||||
}
|
||||
|
||||
setMuteExpiration(ms = 0): void {
|
||||
this.model.setMuteExpiration(
|
||||
ms >= Number.MAX_SAFE_INTEGER ? ms : Date.now() + ms
|
||||
|
@ -3298,81 +3282,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
}
|
||||
|
||||
showContactModal(contactId: string): void {
|
||||
if (this.contactModalView) {
|
||||
this.contactModalView.remove();
|
||||
this.contactModalView = undefined;
|
||||
}
|
||||
|
||||
this.previousFocus = document.activeElement as HTMLElement;
|
||||
|
||||
const hideContactModal = () => {
|
||||
if (this.contactModalView) {
|
||||
this.contactModalView.remove();
|
||||
this.contactModalView = undefined;
|
||||
if (this.previousFocus && this.previousFocus.focus) {
|
||||
this.previousFocus.focus();
|
||||
this.previousFocus = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.contactModalView = new Whisper.ReactWrapperView({
|
||||
JSX: window.Signal.State.Roots.createContactModal(window.reduxStore, {
|
||||
contactId,
|
||||
currentConversationId: this.model.id,
|
||||
onClose: hideContactModal,
|
||||
openConversation: (conversationId: string) => {
|
||||
hideContactModal();
|
||||
this.openConversation(conversationId);
|
||||
},
|
||||
removeMember: (conversationId: string) => {
|
||||
hideContactModal();
|
||||
this.model.removeFromGroupV2(conversationId);
|
||||
},
|
||||
showSafetyNumber: (conversationId: string) => {
|
||||
hideContactModal();
|
||||
this.showSafetyNumber(conversationId);
|
||||
},
|
||||
toggleAdmin: (conversationId: string) => {
|
||||
hideContactModal();
|
||||
|
||||
const isAdmin = this.model.isAdmin(conversationId);
|
||||
const conversationModel = window.ConversationController.get(
|
||||
conversationId
|
||||
);
|
||||
|
||||
if (!conversationModel) {
|
||||
log.info(
|
||||
'conversation_view/toggleAdmin: Could not find conversation to toggle admin privileges'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
window.showConfirmationDialog({
|
||||
cancelText: window.i18n('cancel'),
|
||||
message: isAdmin
|
||||
? window.i18n('ContactModal--rm-admin-info', [
|
||||
conversationModel.getTitle(),
|
||||
])
|
||||
: window.i18n('ContactModal--make-admin-info', [
|
||||
conversationModel.getTitle(),
|
||||
]),
|
||||
okText: isAdmin
|
||||
? window.i18n('ContactModal--rm-admin')
|
||||
: window.i18n('ContactModal--make-admin'),
|
||||
resolve: () => this.model.toggleAdmin(conversationId),
|
||||
});
|
||||
},
|
||||
updateSharedGroups: () => {
|
||||
const conversation = window.ConversationController.get(contactId);
|
||||
if (conversation && conversation.throttledUpdateSharedGroups) {
|
||||
conversation.throttledUpdateSharedGroups();
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
this.contactModalView.render();
|
||||
window.reduxActions.globalModals.showContactModal(contactId, this.model.id);
|
||||
}
|
||||
|
||||
showGroupLinkManagement(): void {
|
||||
|
|
|
@ -23,6 +23,9 @@ const ConversationStack = Whisper.View.extend({
|
|||
model: conversation,
|
||||
});
|
||||
this.listenTo(conversation, 'unload', () => this.onUnload(conversation));
|
||||
this.listenTo(conversation, 'showSafetyNumber', () =>
|
||||
view.showSafetyNumber()
|
||||
);
|
||||
view.$el.appendTo(this.el);
|
||||
|
||||
if (this.lastConversation && this.lastConversation !== conversation) {
|
||||
|
@ -119,6 +122,13 @@ Whisper.InboxView = Whisper.View.extend({
|
|||
this.focusConversation();
|
||||
});
|
||||
|
||||
window.Whisper.events.on('showSafetyNumberInConversation', id => {
|
||||
const conversation = window.ConversationController.get(id);
|
||||
if (conversation) {
|
||||
conversation.trigger('showSafetyNumber');
|
||||
}
|
||||
});
|
||||
|
||||
window.Whisper.events.on('loadingProgress', count => {
|
||||
const view = this.appLoadingScreen;
|
||||
if (view) {
|
||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -43,7 +43,6 @@ import { createStore } from './state/createStore';
|
|||
import { createApp } from './state/roots/createApp';
|
||||
import { createChatColorPicker } from './state/roots/createChatColorPicker';
|
||||
import { createCompositionArea } from './state/roots/createCompositionArea';
|
||||
import { createContactModal } from './state/roots/createContactModal';
|
||||
import { createConversationDetails } from './state/roots/createConversationDetails';
|
||||
import { createConversationHeader } from './state/roots/createConversationHeader';
|
||||
import { createForwardMessageModal } from './state/roots/createForwardMessageModal';
|
||||
|
@ -424,7 +423,6 @@ declare global {
|
|||
createApp: typeof createApp;
|
||||
createChatColorPicker: typeof createChatColorPicker;
|
||||
createCompositionArea: typeof createCompositionArea;
|
||||
createContactModal: typeof createContactModal;
|
||||
createConversationDetails: typeof createConversationDetails;
|
||||
createConversationHeader: typeof createConversationHeader;
|
||||
createForwardMessageModal: typeof createForwardMessageModal;
|
||||
|
|
Loading…
Reference in a new issue