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 {
|
const {
|
||||||
createCompositionArea,
|
createCompositionArea,
|
||||||
} = require('../../ts/state/roots/createCompositionArea');
|
} = require('../../ts/state/roots/createCompositionArea');
|
||||||
const {
|
|
||||||
createContactModal,
|
|
||||||
} = require('../../ts/state/roots/createContactModal');
|
|
||||||
const {
|
const {
|
||||||
createConversationDetails,
|
createConversationDetails,
|
||||||
} = require('../../ts/state/roots/createConversationDetails');
|
} = require('../../ts/state/roots/createConversationDetails');
|
||||||
|
@ -363,7 +360,6 @@ exports.setup = (options = {}) => {
|
||||||
createApp,
|
createApp,
|
||||||
createChatColorPicker,
|
createChatColorPicker,
|
||||||
createCompositionArea,
|
createCompositionArea,
|
||||||
createContactModal,
|
|
||||||
createConversationDetails,
|
createConversationDetails,
|
||||||
createConversationHeader,
|
createConversationHeader,
|
||||||
createForwardMessageModal,
|
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 {
|
.module-background-color {
|
||||||
&__default {
|
&__default {
|
||||||
background-color: $color-black-alpha-40;
|
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;
|
padding: 0 16px 16px 16px;
|
||||||
border-top: 1px solid transparent;
|
border-top: 1px solid transparent;
|
||||||
// If there's a header, just the body scrolls
|
// 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;
|
overflow-x: auto;
|
||||||
|
|
||||||
&--scrolled {
|
&--scrolled {
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
&--no-header {
|
&--no-header {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
// If there's no header, the whole thing scrolls
|
// 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;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
@import './components/ChatColorPicker.scss';
|
@import './components/ChatColorPicker.scss';
|
||||||
@import './components/Checkbox.scss';
|
@import './components/Checkbox.scss';
|
||||||
@import './components/CompositionArea.scss';
|
@import './components/CompositionArea.scss';
|
||||||
|
@import './components/ContactModal.scss';
|
||||||
@import './components/ContactName.scss';
|
@import './components/ContactName.scss';
|
||||||
@import './components/ContactPill.scss';
|
@import './components/ContactPill.scss';
|
||||||
@import './components/ContactPills.scss';
|
@import './components/ContactPills.scss';
|
||||||
|
|
|
@ -1,17 +1,29 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { ContactModalStateType } from '../state/ducks/globalModals';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
|
// ContactModal
|
||||||
|
contactModalState?: ContactModalStateType;
|
||||||
|
renderContactModal: () => JSX.Element;
|
||||||
// ProfileEditor
|
// ProfileEditor
|
||||||
isProfileEditorVisible: boolean;
|
isProfileEditorVisible: boolean;
|
||||||
renderProfileEditor: () => JSX.Element;
|
renderProfileEditor: () => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GlobalModalContainer = ({
|
export const GlobalModalContainer = ({
|
||||||
|
// ContactModal
|
||||||
|
contactModalState,
|
||||||
|
renderContactModal,
|
||||||
// ProfileEditor
|
// ProfileEditor
|
||||||
isProfileEditorVisible,
|
isProfileEditorVisible,
|
||||||
renderProfileEditor,
|
renderProfileEditor,
|
||||||
}: PropsType): JSX.Element | null => {
|
}: PropsType): JSX.Element | null => {
|
||||||
|
if (contactModalState) {
|
||||||
|
return renderContactModal();
|
||||||
|
}
|
||||||
|
|
||||||
if (isProfileEditorVisible) {
|
if (isProfileEditorVisible) {
|
||||||
return renderProfileEditor();
|
return renderProfileEditor();
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,15 +28,17 @@ const defaultContact: ConversationType = getDefaultConversation({
|
||||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
areWeAdmin: boolean('areWeAdmin', overrideProps.areWeAdmin || false),
|
areWeAdmin: boolean('areWeAdmin', overrideProps.areWeAdmin || false),
|
||||||
contact: overrideProps.contact || defaultContact,
|
contact: overrideProps.contact || defaultContact,
|
||||||
|
hideContactModal: action('hideContactModal'),
|
||||||
i18n,
|
i18n,
|
||||||
isAdmin: boolean('isAdmin', overrideProps.isAdmin || false),
|
isAdmin: boolean('isAdmin', overrideProps.isAdmin || false),
|
||||||
isMember: boolean('isMember', overrideProps.isMember || true),
|
isMember: boolean('isMember', overrideProps.isMember || true),
|
||||||
onClose: action('onClose'),
|
openConversationInternal: action('openConversationInternal'),
|
||||||
openConversation: action('openConversation'),
|
removeMemberFromGroup: action('removeMemberFromGroup'),
|
||||||
removeMember: action('removeMember'),
|
showSafetyNumberInConversation: action('showSafetyNumberInConversation'),
|
||||||
showSafetyNumber: action('showSafetyNumber'),
|
|
||||||
toggleAdmin: action('toggleAdmin'),
|
toggleAdmin: action('toggleAdmin'),
|
||||||
updateSharedGroups: action('updateSharedGroups'),
|
updateConversationModelSharedGroups: action(
|
||||||
|
'updateConversationModelSharedGroups'
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
story.add('As non-admin', () => {
|
story.add('As non-admin', () => {
|
||||||
|
|
|
@ -1,103 +1,73 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { ReactPortal, useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
|
|
||||||
import { About } from './About';
|
import { About } from './About';
|
||||||
import { Avatar } from '../Avatar';
|
import { Avatar } from '../Avatar';
|
||||||
import { AvatarLightbox } from '../AvatarLightbox';
|
import { AvatarLightbox } from '../AvatarLightbox';
|
||||||
import { ConversationType } from '../../state/ducks/conversations';
|
import { ConversationType } from '../../state/ducks/conversations';
|
||||||
|
import { Modal } from '../Modal';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
import { SharedGroupNames } from '../SharedGroupNames';
|
import { SharedGroupNames } from '../SharedGroupNames';
|
||||||
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsDataType = {
|
||||||
areWeAdmin: boolean;
|
areWeAdmin: boolean;
|
||||||
contact?: ConversationType;
|
contact?: ConversationType;
|
||||||
|
conversationId?: string;
|
||||||
readonly i18n: LocalizerType;
|
readonly i18n: LocalizerType;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isMember: 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 = ({
|
export const ContactModal = ({
|
||||||
areWeAdmin,
|
areWeAdmin,
|
||||||
contact,
|
contact,
|
||||||
|
conversationId,
|
||||||
|
hideContactModal,
|
||||||
i18n,
|
i18n,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isMember,
|
isMember,
|
||||||
onClose,
|
openConversationInternal,
|
||||||
openConversation,
|
removeMemberFromGroup,
|
||||||
removeMember,
|
showSafetyNumberInConversation,
|
||||||
showSafetyNumber,
|
|
||||||
toggleAdmin,
|
toggleAdmin,
|
||||||
updateSharedGroups,
|
updateConversationModelSharedGroups,
|
||||||
}: PropsType): ReactPortal | null => {
|
}: PropsType): JSX.Element => {
|
||||||
if (!contact) {
|
if (!contact) {
|
||||||
throw new Error('Contact modal opened without a matching 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 [showingAvatar, setShowingAvatar] = useState(false);
|
||||||
|
const [confirmToggleAdmin, setConfirmToggleAdmin] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const div = document.createElement('div');
|
if (conversationId) {
|
||||||
document.body.appendChild(div);
|
|
||||||
setRoot(div);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.body.removeChild(div);
|
|
||||||
setRoot(null);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Kick off the expensive hydration of the current sharedGroupNames
|
// Kick off the expensive hydration of the current sharedGroupNames
|
||||||
updateSharedGroups();
|
updateConversationModelSharedGroups(conversationId);
|
||||||
}, [updateSharedGroups]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (root !== null && closeButtonRef.current) {
|
|
||||||
closeButtonRef.current.focus();
|
|
||||||
}
|
}
|
||||||
}, [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) {
|
if (showingAvatar) {
|
||||||
content = (
|
return (
|
||||||
<AvatarLightbox
|
<AvatarLightbox
|
||||||
avatarColor={contact.color}
|
avatarColor={contact.color}
|
||||||
avatarPath={contact.avatarPath}
|
avatarPath={contact.avatarPath}
|
||||||
|
@ -106,18 +76,16 @@ export const ContactModal = ({
|
||||||
onClose={() => setShowingAvatar(false)}
|
onClose={() => setShowingAvatar(false)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
content = (
|
|
||||||
<div className="module-contact-modal">
|
return (
|
||||||
<button
|
<Modal
|
||||||
ref={r => {
|
moduleClassName="ContactModal__modal"
|
||||||
closeButtonRef.current = r;
|
hasXButton
|
||||||
}}
|
i18n={i18n}
|
||||||
type="button"
|
onClose={hideContactModal}
|
||||||
className="module-contact-modal__close-button"
|
>
|
||||||
onClick={onClose}
|
<div className="ContactModal">
|
||||||
aria-label={i18n('close')}
|
|
||||||
/>
|
|
||||||
<Avatar
|
<Avatar
|
||||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||||
avatarPath={contact.avatarPath}
|
avatarPath={contact.avatarPath}
|
||||||
|
@ -133,53 +101,59 @@ export const ContactModal = ({
|
||||||
unblurredAvatarPath={contact.unblurredAvatarPath}
|
unblurredAvatarPath={contact.unblurredAvatarPath}
|
||||||
onClick={() => setShowingAvatar(true)}
|
onClick={() => setShowingAvatar(true)}
|
||||||
/>
|
/>
|
||||||
<div className="module-contact-modal__name">{contact.title}</div>
|
<div className="ContactModal__name">{contact.title}</div>
|
||||||
<div className="module-about__container">
|
<div className="module-about__container">
|
||||||
<About text={contact.about} />
|
<About text={contact.about} />
|
||||||
</div>
|
</div>
|
||||||
{contact.phoneNumber && (
|
{contact.phoneNumber && (
|
||||||
<div className="module-contact-modal__info">
|
<div className="ContactModal__info">{contact.phoneNumber}</div>
|
||||||
{contact.phoneNumber}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div className="module-contact-modal__info">
|
{!contact.isMe && (
|
||||||
|
<div className="ContactModal__info">
|
||||||
<SharedGroupNames
|
<SharedGroupNames
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
sharedGroupNames={contact.sharedGroupNames || []}
|
sharedGroupNames={contact.sharedGroupNames || []}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="module-contact-modal__button-container">
|
)}
|
||||||
|
<div className="ContactModal__button-container">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="module-contact-modal__button module-contact-modal__send-message"
|
className="ContactModal__button ContactModal__send-message"
|
||||||
onClick={() => openConversation(contact.id)}
|
onClick={() => {
|
||||||
|
hideContactModal();
|
||||||
|
openConversationInternal({ conversationId: contact.id });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="module-contact-modal__bubble-icon">
|
<div className="ContactModal__bubble-icon">
|
||||||
<div className="module-contact-modal__send-message__bubble-icon" />
|
<div className="ContactModal__send-message__bubble-icon" />
|
||||||
</div>
|
</div>
|
||||||
<span>{i18n('ContactModal--message')}</span>
|
<span>{i18n('ContactModal--message')}</span>
|
||||||
</button>
|
</button>
|
||||||
{!contact.isMe && (
|
{!contact.isMe && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="module-contact-modal__button module-contact-modal__safety-number"
|
className="ContactModal__button ContactModal__safety-number"
|
||||||
onClick={() => showSafetyNumber(contact.id)}
|
onClick={() => {
|
||||||
|
hideContactModal();
|
||||||
|
showSafetyNumberInConversation(contact.id);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="module-contact-modal__bubble-icon">
|
<div className="ContactModal__bubble-icon">
|
||||||
<div className="module-contact-modal__safety-number__bubble-icon" />
|
<div className="ContactModal__safety-number__bubble-icon" />
|
||||||
</div>
|
</div>
|
||||||
<span>{i18n('showSafetyNumber')}</span>
|
<span>{i18n('showSafetyNumber')}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!contact.isMe && areWeAdmin && isMember && (
|
{!contact.isMe && areWeAdmin && isMember && conversationId && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="module-contact-modal__button module-contact-modal__make-admin"
|
className="ContactModal__button ContactModal__make-admin"
|
||||||
onClick={() => toggleAdmin(contact.id)}
|
onClick={() => setConfirmToggleAdmin(true)}
|
||||||
>
|
>
|
||||||
<div className="module-contact-modal__bubble-icon">
|
<div className="ContactModal__bubble-icon">
|
||||||
<div className="module-contact-modal__make-admin__bubble-icon" />
|
<div className="ContactModal__make-admin__bubble-icon" />
|
||||||
</div>
|
</div>
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<span>{i18n('ContactModal--rm-admin')}</span>
|
<span>{i18n('ContactModal--rm-admin')}</span>
|
||||||
|
@ -189,34 +163,38 @@ export const ContactModal = ({
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="module-contact-modal__button module-contact-modal__remove-from-group"
|
className="ContactModal__button ContactModal__remove-from-group"
|
||||||
onClick={() => removeMember(contact.id)}
|
onClick={() =>
|
||||||
|
removeMemberFromGroup(conversationId, contact.id)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="module-contact-modal__bubble-icon">
|
<div className="ContactModal__bubble-icon">
|
||||||
<div className="module-contact-modal__remove-from-group__bubble-icon" />
|
<div className="ContactModal__remove-from-group__bubble-icon" />
|
||||||
</div>
|
</div>
|
||||||
<span>{i18n('ContactModal--remove-from-group')}</span>
|
<span>{i18n('ContactModal--remove-from-group')}</span>
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{confirmToggleAdmin && conversationId && (
|
||||||
);
|
<ConfirmationDialog
|
||||||
}
|
actions={[
|
||||||
|
{
|
||||||
return root
|
action: () => toggleAdmin(conversationId, contact.id),
|
||||||
? createPortal(
|
text: isAdmin
|
||||||
<div
|
? i18n('ContactModal--rm-admin')
|
||||||
ref={ref => {
|
: i18n('ContactModal--make-admin'),
|
||||||
overlayRef.current = ref;
|
},
|
||||||
}}
|
]}
|
||||||
role="presentation"
|
i18n={i18n}
|
||||||
className="module-contact-modal__overlay"
|
onClose={() => setConfirmToggleAdmin(false)}
|
||||||
onClick={onClickOverlay}
|
|
||||||
>
|
>
|
||||||
{content}
|
{isAdmin
|
||||||
</div>,
|
? i18n('ContactModal--rm-admin-info', [contact.title])
|
||||||
root
|
: i18n('ContactModal--make-admin-info', [contact.title])}
|
||||||
)
|
</ConfirmationDialog>
|
||||||
: null;
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,7 +60,6 @@ export type StateProps = {
|
||||||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||||
setDisappearingMessages: (seconds: number) => void;
|
setDisappearingMessages: (seconds: number) => void;
|
||||||
showAllMedia: () => void;
|
showAllMedia: () => void;
|
||||||
showContactModal: (conversationId: string) => void;
|
|
||||||
showGroupChatColorEditor: () => void;
|
showGroupChatColorEditor: () => void;
|
||||||
showGroupLinkManagement: () => void;
|
showGroupLinkManagement: () => void;
|
||||||
showGroupV2Permissions: () => void;
|
showGroupV2Permissions: () => void;
|
||||||
|
@ -86,6 +85,7 @@ type ActionProps = {
|
||||||
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
||||||
replaceAvatar: ReplaceAvatarActionType;
|
replaceAvatar: ReplaceAvatarActionType;
|
||||||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||||
|
showContactModal: (contactId: string, conversationId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = StateProps & ActionProps;
|
export type Props = StateProps & ActionProps;
|
||||||
|
@ -329,6 +329,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
|
|
||||||
<ConversationDetailsMembershipList
|
<ConversationDetailsMembershipList
|
||||||
canAddNewMembers={canEditGroupInfo}
|
canAddNewMembers={canEditGroupInfo}
|
||||||
|
conversationId={conversation.id}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
memberships={memberships}
|
memberships={memberships}
|
||||||
showContactModal={showContactModal}
|
showContactModal={showContactModal}
|
||||||
|
|
|
@ -44,6 +44,7 @@ const createProps = (overrideProps: Partial<Props>): Props => ({
|
||||||
canAddNewMembers: isBoolean(overrideProps.canAddNewMembers)
|
canAddNewMembers: isBoolean(overrideProps.canAddNewMembers)
|
||||||
? overrideProps.canAddNewMembers
|
? overrideProps.canAddNewMembers
|
||||||
: false,
|
: false,
|
||||||
|
conversationId: '123',
|
||||||
i18n,
|
i18n,
|
||||||
memberships: overrideProps.memberships || [],
|
memberships: overrideProps.memberships || [],
|
||||||
showContactModal: action('showContactModal'),
|
showContactModal: action('showContactModal'),
|
||||||
|
|
|
@ -19,10 +19,11 @@ export type GroupV2Membership = {
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
canAddNewMembers: boolean;
|
canAddNewMembers: boolean;
|
||||||
|
conversationId: string;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
maxShownMemberCount?: number;
|
maxShownMemberCount?: number;
|
||||||
memberships: Array<GroupV2Membership>;
|
memberships: Array<GroupV2Membership>;
|
||||||
showContactModal: (conversationId: string) => void;
|
showContactModal: (contactId: string, conversationId: string) => void;
|
||||||
startAddingNewMembers?: () => void;
|
startAddingNewMembers?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,6 +68,7 @@ function sortMemberships(
|
||||||
|
|
||||||
export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
|
export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
|
||||||
canAddNewMembers,
|
canAddNewMembers,
|
||||||
|
conversationId,
|
||||||
i18n,
|
i18n,
|
||||||
maxShownMemberCount = 5,
|
maxShownMemberCount = 5,
|
||||||
memberships,
|
memberships,
|
||||||
|
@ -101,7 +103,7 @@ export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
|
||||||
{sortedMemberships.slice(0, membersToShow).map(({ isAdmin, member }) => (
|
{sortedMemberships.slice(0, membersToShow).map(({ isAdmin, member }) => (
|
||||||
<PanelRow
|
<PanelRow
|
||||||
key={member.id}
|
key={member.id}
|
||||||
onClick={() => showContactModal(member.id)}
|
onClick={() => showContactModal(member.id, conversationId)}
|
||||||
icon={
|
icon={
|
||||||
<Avatar
|
<Avatar
|
||||||
conversationType="direct"
|
conversationType="direct"
|
||||||
|
|
|
@ -58,6 +58,7 @@ import {
|
||||||
import { AvatarDataType, getDefaultAvatars } from '../../types/Avatar';
|
import { AvatarDataType, getDefaultAvatars } from '../../types/Avatar';
|
||||||
import { getAvatarData } from '../../util/getAvatarData';
|
import { getAvatarData } from '../../util/getAvatarData';
|
||||||
import { isSameAvatarData } from '../../util/isSameAvatarData';
|
import { isSameAvatarData } from '../../util/isSameAvatarData';
|
||||||
|
import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper';
|
||||||
|
|
||||||
import { NoopActionType } from './noop';
|
import { NoopActionType } from './noop';
|
||||||
|
|
||||||
|
@ -780,6 +781,7 @@ export const actions = {
|
||||||
openConversationInternal,
|
openConversationInternal,
|
||||||
removeAllConversations,
|
removeAllConversations,
|
||||||
removeCustomColorOnConversations,
|
removeCustomColorOnConversations,
|
||||||
|
removeMemberFromGroup,
|
||||||
repairNewestMessage,
|
repairNewestMessage,
|
||||||
repairOldestMessage,
|
repairOldestMessage,
|
||||||
replaceAvatar,
|
replaceAvatar,
|
||||||
|
@ -803,11 +805,14 @@ export const actions = {
|
||||||
showArchivedConversations,
|
showArchivedConversations,
|
||||||
showChooseGroupMembers,
|
showChooseGroupMembers,
|
||||||
showInbox,
|
showInbox,
|
||||||
|
showSafetyNumberInConversation,
|
||||||
startComposing,
|
startComposing,
|
||||||
startNewConversationFromPhoneNumber,
|
startNewConversationFromPhoneNumber,
|
||||||
startSettingGroupMetadata,
|
startSettingGroupMetadata,
|
||||||
|
toggleAdmin,
|
||||||
toggleConversationInChooseMembers,
|
toggleConversationInChooseMembers,
|
||||||
toggleComposeEditingAvatar,
|
toggleComposeEditingAvatar,
|
||||||
|
updateConversationModelSharedGroups,
|
||||||
verifyConversationsStoppingMessageSend,
|
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 {
|
function showInbox(): ShowInboxActionType {
|
||||||
return {
|
return {
|
||||||
type: 'SHOW_INBOX',
|
type: 'SHOW_INBOX',
|
||||||
|
|
|
@ -4,16 +4,33 @@
|
||||||
// State
|
// State
|
||||||
|
|
||||||
export type GlobalModalsStateType = {
|
export type GlobalModalsStateType = {
|
||||||
|
readonly contactModalState?: ContactModalStateType;
|
||||||
readonly isProfileEditorVisible: boolean;
|
readonly isProfileEditorVisible: boolean;
|
||||||
readonly profileEditorHasError: boolean;
|
readonly profileEditorHasError: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Actions
|
// 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';
|
const TOGGLE_PROFILE_EDITOR = 'globalModals/TOGGLE_PROFILE_EDITOR';
|
||||||
export const TOGGLE_PROFILE_EDITOR_ERROR =
|
export const TOGGLE_PROFILE_EDITOR_ERROR =
|
||||||
'globalModals/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 ToggleProfileEditorActionType = {
|
||||||
type: typeof TOGGLE_PROFILE_EDITOR;
|
type: typeof TOGGLE_PROFILE_EDITOR;
|
||||||
};
|
};
|
||||||
|
@ -23,16 +40,39 @@ export type ToggleProfileEditorErrorActionType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GlobalModalsActionType =
|
export type GlobalModalsActionType =
|
||||||
|
| HideContactModalActionType
|
||||||
|
| ShowContactModalActionType
|
||||||
| ToggleProfileEditorActionType
|
| ToggleProfileEditorActionType
|
||||||
| ToggleProfileEditorErrorActionType;
|
| ToggleProfileEditorErrorActionType;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
hideContactModal,
|
||||||
|
showContactModal,
|
||||||
toggleProfileEditor,
|
toggleProfileEditor,
|
||||||
toggleProfileEditorHasError,
|
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 {
|
function toggleProfileEditor(): ToggleProfileEditorActionType {
|
||||||
return { type: TOGGLE_PROFILE_EDITOR };
|
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;
|
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 { mapDispatchToProps } from '../actions';
|
||||||
import {
|
import {
|
||||||
ContactModal,
|
ContactModal,
|
||||||
PropsType,
|
PropsDataType,
|
||||||
} from '../../components/conversation/ContactModal';
|
} from '../../components/conversation/ContactModal';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getConversationSelector } from '../selectors/conversations';
|
import { getConversationSelector } from '../selectors/conversations';
|
||||||
|
|
||||||
export type SmartContactModalProps = {
|
const mapStateToProps = (state: StateType): PropsDataType => {
|
||||||
contactId: string;
|
const { contactId, conversationId } =
|
||||||
currentConversationId: string;
|
state.globalModals.contactModalState || {};
|
||||||
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 = (
|
const currentConversation = getConversationSelector(state)(conversationId);
|
||||||
state: StateType,
|
|
||||||
props: SmartContactModalProps
|
|
||||||
): PropsType => {
|
|
||||||
const { contactId, currentConversationId } = props;
|
|
||||||
|
|
||||||
const currentConversation = getConversationSelector(state)(
|
|
||||||
currentConversationId
|
|
||||||
);
|
|
||||||
const contact = getConversationSelector(state)(contactId);
|
const contact = getConversationSelector(state)(contactId);
|
||||||
|
|
||||||
const areWeAdmin =
|
const areWeAdmin =
|
||||||
|
@ -51,9 +36,9 @@ const mapStateToProps = (
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...props,
|
|
||||||
areWeAdmin,
|
areWeAdmin,
|
||||||
contact,
|
contact,
|
||||||
|
conversationId,
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isMember,
|
isMember,
|
||||||
|
|
|
@ -25,7 +25,6 @@ export type SmartConversationDetailsProps = {
|
||||||
loadRecentMediaItems: (limit: number) => void;
|
loadRecentMediaItems: (limit: number) => void;
|
||||||
setDisappearingMessages: (seconds: number) => void;
|
setDisappearingMessages: (seconds: number) => void;
|
||||||
showAllMedia: () => void;
|
showAllMedia: () => void;
|
||||||
showContactModal: (conversationId: string) => void;
|
|
||||||
showGroupChatColorEditor: () => void;
|
showGroupChatColorEditor: () => void;
|
||||||
showGroupLinkManagement: () => void;
|
showGroupLinkManagement: () => void;
|
||||||
showGroupV2Permissions: () => void;
|
showGroupV2Permissions: () => void;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { mapDispatchToProps } from '../actions';
|
||||||
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
|
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
import { SmartProfileEditorModal } from './ProfileEditorModal';
|
import { SmartProfileEditorModal } from './ProfileEditorModal';
|
||||||
|
import { SmartContactModal } from './ContactModal';
|
||||||
|
|
||||||
const FilteredSmartProfileEditorModal = SmartProfileEditorModal;
|
const FilteredSmartProfileEditorModal = SmartProfileEditorModal;
|
||||||
|
|
||||||
|
@ -14,9 +15,14 @@ function renderProfileEditor(): JSX.Element {
|
||||||
return <FilteredSmartProfileEditorModal />;
|
return <FilteredSmartProfileEditorModal />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderContactModal(): JSX.Element {
|
||||||
|
return <SmartContactModal />;
|
||||||
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType) => {
|
const mapStateToProps = (state: StateType) => {
|
||||||
return {
|
return {
|
||||||
...state.globalModals,
|
...state.globalModals,
|
||||||
|
renderContactModal,
|
||||||
renderProfileEditor,
|
renderProfileEditor,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -12717,20 +12717,6 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-07-30T16:57:33.618Z"
|
"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",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/ConversationHeader.js",
|
"path": "ts/components/conversation/ConversationHeader.js",
|
||||||
|
|
|
@ -646,22 +646,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
return this;
|
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 {
|
setMuteExpiration(ms = 0): void {
|
||||||
this.model.setMuteExpiration(
|
this.model.setMuteExpiration(
|
||||||
ms >= Number.MAX_SAFE_INTEGER ? ms : Date.now() + ms
|
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 {
|
showContactModal(contactId: string): void {
|
||||||
if (this.contactModalView) {
|
window.reduxActions.globalModals.showContactModal(contactId, this.model.id);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showGroupLinkManagement(): void {
|
showGroupLinkManagement(): void {
|
||||||
|
|
|
@ -23,6 +23,9 @@ const ConversationStack = Whisper.View.extend({
|
||||||
model: conversation,
|
model: conversation,
|
||||||
});
|
});
|
||||||
this.listenTo(conversation, 'unload', () => this.onUnload(conversation));
|
this.listenTo(conversation, 'unload', () => this.onUnload(conversation));
|
||||||
|
this.listenTo(conversation, 'showSafetyNumber', () =>
|
||||||
|
view.showSafetyNumber()
|
||||||
|
);
|
||||||
view.$el.appendTo(this.el);
|
view.$el.appendTo(this.el);
|
||||||
|
|
||||||
if (this.lastConversation && this.lastConversation !== conversation) {
|
if (this.lastConversation && this.lastConversation !== conversation) {
|
||||||
|
@ -119,6 +122,13 @@ Whisper.InboxView = Whisper.View.extend({
|
||||||
this.focusConversation();
|
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 => {
|
window.Whisper.events.on('loadingProgress', count => {
|
||||||
const view = this.appLoadingScreen;
|
const view = this.appLoadingScreen;
|
||||||
if (view) {
|
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 { createApp } from './state/roots/createApp';
|
||||||
import { createChatColorPicker } from './state/roots/createChatColorPicker';
|
import { createChatColorPicker } from './state/roots/createChatColorPicker';
|
||||||
import { createCompositionArea } from './state/roots/createCompositionArea';
|
import { createCompositionArea } from './state/roots/createCompositionArea';
|
||||||
import { createContactModal } from './state/roots/createContactModal';
|
|
||||||
import { createConversationDetails } from './state/roots/createConversationDetails';
|
import { createConversationDetails } from './state/roots/createConversationDetails';
|
||||||
import { createConversationHeader } from './state/roots/createConversationHeader';
|
import { createConversationHeader } from './state/roots/createConversationHeader';
|
||||||
import { createForwardMessageModal } from './state/roots/createForwardMessageModal';
|
import { createForwardMessageModal } from './state/roots/createForwardMessageModal';
|
||||||
|
@ -424,7 +423,6 @@ declare global {
|
||||||
createApp: typeof createApp;
|
createApp: typeof createApp;
|
||||||
createChatColorPicker: typeof createChatColorPicker;
|
createChatColorPicker: typeof createChatColorPicker;
|
||||||
createCompositionArea: typeof createCompositionArea;
|
createCompositionArea: typeof createCompositionArea;
|
||||||
createContactModal: typeof createContactModal;
|
|
||||||
createConversationDetails: typeof createConversationDetails;
|
createConversationDetails: typeof createConversationDetails;
|
||||||
createConversationHeader: typeof createConversationHeader;
|
createConversationHeader: typeof createConversationHeader;
|
||||||
createForwardMessageModal: typeof createForwardMessageModal;
|
createForwardMessageModal: typeof createForwardMessageModal;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue