Modernize ContactModal

This commit is contained in:
Josh Perez 2021-09-21 18:37:10 -04:00 committed by GitHub
parent 1d2fcde49f
commit c05d23e628
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 426 additions and 493 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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