A hybrid panel system for React & Backbone

This commit is contained in:
Josh Perez 2022-12-14 13:41:04 -05:00 committed by GitHub
parent 624adca360
commit ebeb6a7a6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 474 additions and 157 deletions

View file

@ -12,6 +12,7 @@ export type PropsType = {
renderCompositionArea: () => JSX.Element;
renderConversationHeader: () => JSX.Element;
renderTimeline: () => JSX.Element;
renderPanel: () => JSX.Element | undefined;
};
export function ConversationView({
@ -20,6 +21,7 @@ export function ConversationView({
renderCompositionArea,
renderConversationHeader,
renderTimeline,
renderPanel,
}: PropsType): JSX.Element {
const onDrop = React.useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
@ -93,6 +95,7 @@ export function ConversationView({
{renderCompositionArea()}
</div>
</div>
{renderPanel()}
</div>
);
}

View file

@ -80,7 +80,7 @@ const createProps = (
setDisappearingMessages: action('setDisappearingMessages'),
showAllMedia: action('showAllMedia'),
showContactModal: action('showContactModal'),
showChatColorEditor: action('showChatColorEditor'),
pushPanelForConversation: action('pushPanelForConversation'),
showGroupLinkManagement: action('showGroupLinkManagement'),
showGroupV2Permissions: action('showGroupV2Permissions'),
showConversationNotificationsSettings: action(

View file

@ -8,6 +8,7 @@ import { Button, ButtonIconType, ButtonVariant } from '../../Button';
import { Tooltip } from '../../Tooltip';
import type {
ConversationType,
PushPanelForConversationActionType,
ShowConversationType,
} from '../../../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges';
@ -50,6 +51,7 @@ import type {
} from '../../../types/Avatar';
import { isConversationMuted } from '../../../util/isConversationMuted';
import { ConversationDetailsGroups } from './ConversationDetailsGroups';
import { PanelType } from '../../../types/Panels';
enum ModalState {
NothingOpen,
@ -80,7 +82,6 @@ export type StateProps = {
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
showAllMedia: () => void;
showChatColorEditor: () => void;
showGroupLinkManagement: () => void;
showGroupV2Permissions: () => void;
showPendingInvites: () => void;
@ -110,6 +111,7 @@ type ActionProps = {
loadRecentMediaItems: (id: string, limit: number) => void;
onOutgoingAudioCallInConversation: (conversationId: string) => unknown;
onOutgoingVideoCallInConversation: (conversationId: string) => unknown;
pushPanelForConversation: PushPanelForConversationActionType;
replaceAvatar: ReplaceAvatarActionType;
saveAvatarToDisk: SaveAvatarToDiskActionType;
searchInConversation: (id: string) => unknown;
@ -149,6 +151,7 @@ export function ConversationDetails({
onOutgoingVideoCallInConversation,
pendingApprovalMemberships,
pendingMemberships,
pushPanelForConversation,
renderChooseGroupMembersModal,
renderConfirmAdditionsModal,
replaceAvatar,
@ -157,7 +160,6 @@ export function ConversationDetails({
setDisappearingMessages,
setMuteExpiration,
showAllMedia,
showChatColorEditor,
showContactModal,
showConversationNotificationsSettings,
showConversation,
@ -426,7 +428,11 @@ export function ConversationDetails({
/>
}
label={i18n('showChatColorEditor')}
onClick={showChatColorEditor}
onClick={() => {
pushPanelForConversation(conversation.id, {
type: PanelType.ChatColorEditor,
});
}}
right={
<div
className={`ConversationDetails__chat-color ConversationDetails__chat-color--${conversation.conversationColor}`}

View file

@ -24,7 +24,6 @@ import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
// State
import { createChatColorPicker } from './state/roots/createChatColorPicker';
import { createConversationDetails } from './state/roots/createConversationDetails';
import { createApp } from './state/roots/createApp';
import { createGroupLinkManagement } from './state/roots/createGroupLinkManagement';
@ -401,7 +400,6 @@ export const setup = (options: {
const Roots = {
createApp,
createChatColorPicker,
createConversationDetails,
createGroupLinkManagement,
createGroupV2JoinModal,

View file

@ -117,6 +117,7 @@ import {
initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2,
} from '../../groups';
import { getMessageById } from '../../messages/getMessageById';
import type { PanelRenderType } from '../../types/Panels';
// State
@ -392,8 +393,7 @@ export type ConversationsStateType = {
selectedMessage: string | undefined;
selectedMessageCounter: number;
selectedMessageSource: SelectedMessageSource | undefined;
selectedConversationTitle?: string;
selectedConversationPanelDepth: number;
selectedConversationPanels: Array<PanelRenderType>;
showArchived: boolean;
composer?: ComposerStateType;
contactSpoofingReview?: ContactSpoofingReviewStateType;
@ -457,7 +457,8 @@ const DISCARD_MESSAGES = 'conversations/DISCARD_MESSAGES';
const REPLACE_AVATARS = 'conversations/REPLACE_AVATARS';
export const SELECTED_CONVERSATION_CHANGED =
'conversations/SELECTED_CONVERSATION_CHANGED';
const PUSH_PANEL = 'conversations/PUSH_PANEL';
const POP_PANEL = 'conversations/POP_PANEL';
export const SET_VOICE_NOTE_PLAYBACK_RATE =
'conversations/SET_VOICE_NOTE_PLAYBACK_RATE';
@ -678,14 +679,6 @@ export type SetIsNearBottomActionType = {
isNearBottom: boolean;
};
};
export type SetConversationHeaderTitleActionType = {
type: 'SET_CONVERSATION_HEADER_TITLE';
payload: { title?: string };
};
export type SetSelectedConversationPanelDepthActionType = {
type: 'SET_SELECTED_CONVERSATION_PANEL_DEPTH';
payload: { panelDepth: number };
};
export type ScrollToMessageActionType = {
type: 'SCROLL_TO_MESSAGE';
payload: {
@ -781,6 +774,14 @@ export type ToggleConversationInChooseMembersActionType = {
maxGroupSize: number;
};
};
type PushPanelActionType = {
type: typeof PUSH_PANEL;
payload: PanelRenderType;
};
type PopPanelActionType = {
type: typeof POP_PANEL;
payload: null;
};
type ReplaceAvatarsActionType = {
type: typeof REPLACE_AVATARS;
@ -822,6 +823,8 @@ export type ConversationActionType =
| MessageSelectedActionType
| MessagesAddedActionType
| MessagesResetActionType
| PopPanelActionType
| PushPanelActionType
| RemoveAllConversationsActionType
| RepairNewestMessageActionType
| RepairOldestMessageActionType
@ -834,13 +837,11 @@ export type ConversationActionType =
| SetComposeGroupExpireTimerActionType
| SetComposeGroupNameActionType
| SetComposeSearchTermActionType
| SetConversationHeaderTitleActionType
| SetIsFetchingUUIDActionType
| SetIsNearBottomActionType
| SetMessageLoadingStateActionType
| SetPreJoinConversationActionType
| SetRecentMediaItemsActionType
| SetSelectedConversationPanelDepthActionType
| ShowArchivedConversationsActionType
| ShowChooseGroupMembersActionType
| ShowInboxActionType
@ -885,14 +886,16 @@ export const actions = {
discardMessages,
doubleCheckMissingQuoteReference,
generateNewGroupLink,
loadRecentMediaItems,
initiateMigrationToGroupV2,
loadRecentMediaItems,
messageChanged,
messageDeleted,
messageExpanded,
messagesAdded,
messagesReset,
myProfileChanged,
popPanelForConversation,
pushPanelForConversation,
removeAllConversations,
removeCustomColorOnConversations,
removeMemberFromGroup,
@ -924,8 +927,6 @@ export const actions = {
setMuteExpiration,
setPinned,
setPreJoinConversation,
setSelectedConversationHeaderTitle,
setSelectedConversationPanelDepth,
setVoiceNotePlaybackRate,
showArchivedConversations,
showChooseGroupMembers,
@ -2064,20 +2065,61 @@ function setIsFetchingUUID(
},
};
}
function setSelectedConversationHeaderTitle(
title?: string
): SetConversationHeaderTitleActionType {
export type PushPanelForConversationActionType = (
conversationId: string,
panel: PanelRenderType
) => unknown;
function pushPanelForConversation(
conversationId: string,
panel: PanelRenderType
): PushPanelActionType {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error(
`addPanelToConversation: No conversation found for conversation ${conversationId}`
);
}
conversation.trigger('pushPanel', panel);
return {
type: 'SET_CONVERSATION_HEADER_TITLE',
payload: { title },
type: PUSH_PANEL,
payload: panel,
};
}
function setSelectedConversationPanelDepth(
panelDepth: number
): SetSelectedConversationPanelDepthActionType {
return {
type: 'SET_SELECTED_CONVERSATION_PANEL_DEPTH',
payload: { panelDepth },
function popPanelForConversation(
conversationId: string
): ThunkAction<void, RootStateType, unknown, PopPanelActionType> {
return (dispatch, getState) => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error(
`addPanelToConversation: No conversation found for conversation ${conversationId}`
);
}
const { conversations } = getState();
const { selectedConversationPanels } = conversations;
if (!selectedConversationPanels.length) {
return;
}
const panel = [...selectedConversationPanels].pop();
if (!panel) {
return;
}
conversation.trigger('popPanel', panel);
dispatch({
type: POP_PANEL,
payload: null,
});
};
}
@ -2869,8 +2911,7 @@ export function getEmptyState(): ConversationsStateType {
selectedMessageCounter: 0,
selectedMessageSource: undefined,
showArchived: false,
selectedConversationTitle: '',
selectedConversationPanelDepth: 0,
selectedConversationPanels: [],
};
}
@ -3375,7 +3416,7 @@ export function reducer(
return {
...omit(state, 'contactSpoofingReview'),
selectedConversationId,
selectedConversationPanelDepth: 0,
selectedConversationPanels: [],
messagesLookup: omit(state.messagesLookup, messageIds),
messagesByConversation: omit(state.messagesByConversation, [id]),
};
@ -3423,12 +3464,6 @@ export function reducer(
},
};
}
if (action.type === 'SET_SELECTED_CONVERSATION_PANEL_DEPTH') {
return {
...state,
selectedConversationPanelDepth: action.payload.panelDepth,
};
}
if (action.type === 'MESSAGE_SELECTED') {
const { messageId, conversationId } = action.payload;
@ -4180,10 +4215,24 @@ export function reducer(
};
}
if (action.type === 'SET_CONVERSATION_HEADER_TITLE') {
if (action.type === PUSH_PANEL) {
return {
...state,
selectedConversationTitle: action.payload.title,
selectedConversationPanels: [
...state.selectedConversationPanels,
action.payload,
],
};
}
if (action.type === POP_PANEL) {
const { selectedConversationPanels } = state;
const nextPanels = [...selectedConversationPanels];
nextPanels.pop();
return {
...state,
selectedConversationPanels: nextPanels,
};
}

View file

@ -1,19 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { Provider } from 'react-redux';
import type { Store } from 'redux';
import type { SmartChatColorPickerProps } from '../smart/ChatColorPicker';
import { SmartChatColorPicker } from '../smart/ChatColorPicker';
export const createChatColorPicker = (
store: Store,
props: SmartChatColorPickerProps
): React.ReactElement => (
<Provider store={store}>
<SmartChatColorPicker {...props} />
</Provider>
);

View file

@ -64,6 +64,9 @@ import * as log from '../../logging/log';
import { TimelineMessageLoadingState } from '../../util/timelineUtil';
import { isSignalConversation } from '../../util/isSignalConversation';
import { reduce } from '../../util/iterables';
import { getConversationTitleForPanelType } from '../../util/getConversationTitleForPanelType';
import type { ReactPanelRenderType, PanelRenderType } from '../../types/Panels';
import { isPanelHandledByReact } from '../../types/Panels';
let placeholderContact: ConversationType;
export const getPlaceholderContact = (): ConversationType => {
@ -1131,3 +1134,34 @@ export const getHideStoryConversationIds = createSelector(
conversationId => conversationLookup[conversationId].hideStory
)
);
const getTopPanel = createSelector(
getConversations,
(conversations): PanelRenderType | undefined =>
conversations.selectedConversationPanels[
conversations.selectedConversationPanels.length - 1
]
);
export const getTopPanelRenderableByReact = createSelector(
getConversations,
(conversations): ReactPanelRenderType | undefined => {
const topPanel =
conversations.selectedConversationPanels[
conversations.selectedConversationPanels.length - 1
];
if (!isPanelHandledByReact(topPanel)) {
return;
}
return topPanel;
}
);
export const getConversationTitle = createSelector(
getIntl,
getTopPanel,
(i18n, panel): string | undefined =>
getConversationTitleForPanelType(i18n, panel?.type)
);

View file

@ -38,7 +38,6 @@ export type SmartConversationDetailsProps = {
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
conversationId: string;
showAllMedia: () => void;
showChatColorEditor: () => void;
showGroupLinkManagement: () => void;
showGroupV2Permissions: () => void;
showConversationNotificationsSettings: () => void;

View file

@ -12,6 +12,7 @@ import {
import { getPreferredBadgeSelector } from '../selectors/badges';
import {
getConversationSelector,
getConversationTitle,
isMissingRequiredProfileSharing,
} from '../selectors/conversations';
import { CallMode } from '../../types/Calling';
@ -108,14 +109,14 @@ const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
'unblurredAvatarPath',
]),
badge: getPreferredBadgeSelector(state)(conversation.badges),
conversationTitle: state.conversations.selectedConversationTitle,
conversationTitle: getConversationTitle(state),
hasStories,
isMissingMandatoryProfileSharing:
isMissingRequiredProfileSharing(conversation),
isSMSOnly: isConversationSMSOnly(conversation),
isSignalConversation: isSignalConversation(conversation),
i18n: getIntl(state),
showBackButton: state.conversations.selectedConversationPanelDepth > 0,
showBackButton: state.conversations.selectedConversationPanels.length > 0,
outgoingCallButtonStyle: getOutgoingCallButtonStyle(conversation, state),
theme: getTheme(state),
};

View file

@ -3,15 +3,19 @@
import React from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { ConversationView } from '../../components/conversation/ConversationView';
import type { StateType } from '../reducer';
import type { CompositionAreaPropsType } from './CompositionArea';
import { SmartCompositionArea } from './CompositionArea';
import type { OwnProps as ConversationHeaderPropsType } from './ConversationHeader';
import { SmartConversationHeader } from './ConversationHeader';
import type { StateType } from '../reducer';
import type { TimelinePropsType } from './Timeline';
import * as log from '../../logging/log';
import { ConversationView } from '../../components/conversation/ConversationView';
import { PanelType } from '../../types/Panels';
import { SmartChatColorPicker } from './ChatColorPicker';
import { SmartCompositionArea } from './CompositionArea';
import { SmartConversationHeader } from './ConversationHeader';
import { SmartTimeline } from './Timeline';
import { getTopPanelRenderableByReact } from '../selectors/conversations';
import { mapDispatchToProps } from '../actions';
export type PropsType = {
conversationId: string;
@ -31,7 +35,7 @@ export type PropsType = {
timelineProps: TimelinePropsType;
};
const mapStateToProps = (_state: StateType, props: PropsType) => {
const mapStateToProps = (state: StateType, props: PropsType) => {
const {
compositionAreaProps,
conversationHeaderProps,
@ -39,6 +43,8 @@ const mapStateToProps = (_state: StateType, props: PropsType) => {
timelineProps,
} = props;
const topPanel = getTopPanelRenderableByReact(state);
return {
conversationId,
renderCompositionArea: () => (
@ -48,6 +54,24 @@ const mapStateToProps = (_state: StateType, props: PropsType) => {
<SmartConversationHeader {...conversationHeaderProps} />
),
renderTimeline: () => <SmartTimeline {...timelineProps} />,
renderPanel: () => {
if (!topPanel) {
return;
}
if (topPanel.type === PanelType.ChatColorEditor) {
return (
<div className="panel">
<SmartChatColorPicker conversationId={conversationId} />
</div>
);
}
const unknownPanelType: never = topPanel.type;
log.warn(`renderPanel: Got unexpected panel type ${unknownPanelType}`);
return undefined;
},
};
};

54
ts/types/Panels.ts Normal file
View file

@ -0,0 +1,54 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { EmbeddedContactType } from './EmbeddedContact';
import type { UUIDStringType } from './UUID';
export enum PanelType {
AllMedia = 'AllMedia',
ChatColorEditor = 'ChatColorEditor',
ContactDetails = 'ContactDetails',
ConversationDetails = 'ConversationDetails',
GroupInvites = 'GroupInvites',
GroupLinkManagement = 'GroupLinkManagement',
GroupPermissions = 'GroupPermissions',
GroupV1Members = 'GroupV1Members',
MessageDetails = 'MessageDetails',
NotificationSettings = 'NotificationSettings',
StickerManager = 'StickerManager',
}
export type ReactPanelRenderType = { type: PanelType.ChatColorEditor };
export type BackbonePanelRenderType =
| { type: PanelType.AllMedia }
| {
type: PanelType.ContactDetails;
args: {
contact: EmbeddedContactType;
signalAccount?: {
phoneNumber: string;
uuid: UUIDStringType;
};
};
}
| { type: PanelType.ConversationDetails }
| { type: PanelType.GroupInvites }
| { type: PanelType.GroupLinkManagement }
| { type: PanelType.GroupPermissions }
| { type: PanelType.GroupV1Members }
| { type: PanelType.MessageDetails; args: { messageId: string } }
| { type: PanelType.NotificationSettings }
| { type: PanelType.StickerManager };
export type PanelRenderType = ReactPanelRenderType | BackbonePanelRenderType;
export function isPanelHandledByReact(
panel: PanelRenderType
): panel is ReactPanelRenderType {
if (!panel) {
return false;
}
return panel.type === PanelType.ChatColorEditor;
}

View file

@ -0,0 +1,59 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { LocalizerType } from '../types/Util';
import * as log from '../logging/log';
import { PanelType } from '../types/Panels';
export function getConversationTitleForPanelType(
i18n: LocalizerType,
panelType: PanelType | undefined
): string | undefined {
if (!panelType) {
return undefined;
}
if (panelType === PanelType.AllMedia) {
return i18n('allMedia');
}
if (panelType === PanelType.ChatColorEditor) {
return i18n('ChatColorPicker__menu-title');
}
if (panelType === PanelType.ConversationDetails) {
return '';
}
if (panelType === PanelType.GroupInvites) {
return i18n('ConversationDetails--requests-and-invites');
}
if (panelType === PanelType.GroupLinkManagement) {
return i18n('ConversationDetails--group-link');
}
if (panelType === PanelType.GroupPermissions) {
return i18n('permissions');
}
if (panelType === PanelType.NotificationSettings) {
return i18n('ConversationDetails--notifications');
}
if (
panelType === PanelType.ContactDetails ||
panelType === PanelType.GroupV1Members ||
panelType === PanelType.MessageDetails ||
panelType === PanelType.StickerManager
) {
return undefined;
}
const unknownType: never = panelType;
log.warn(
`getConversationTitleForPanelType: Got unexpected type ${unknownType}`
);
return undefined;
}

View file

@ -56,16 +56,18 @@ import { SECOND } from '../util/durations';
import { startConversation } from '../util/startConversation';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
import { hasDraftAttachments } from '../util/hasDraftAttachments';
import type { BackbonePanelRenderType, PanelRenderType } from '../types/Panels';
import { PanelType, isPanelHandledByReact } from '../types/Panels';
type AttachmentOptions = {
messageId: string;
attachment: AttachmentType;
};
type PanelType = { view: Backbone.View; headerTitle?: string };
const { Message } = window.Signal.Types;
type BackbonePanelType = { panelType: PanelType; view: Backbone.View };
const { getAbsoluteAttachmentPath, upgradeMessageSchema } =
window.Signal.Migrations;
@ -125,7 +127,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
private stickerPreviewModalView?: Backbone.View;
// Panel support
private panels: Array<PanelType> = [];
private panels: Array<BackbonePanelType> = [];
private previousFocus?: HTMLElement;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -143,7 +145,9 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
// These are triggered by background.ts for keyboard handling
this.listenTo(this.model, 'open-all-media', this.showAllMedia);
this.listenTo(this.model, 'escape-pressed', this.resetPanel);
this.listenTo(this.model, 'escape-pressed', () => {
window.reduxActions.conversations.popPanelForConversation(this.model.id);
});
this.listenTo(this.model, 'show-message-details', this.showMessageDetail);
this.listenTo(this.model, 'delete-message', this.deleteMessage);
this.listenTo(this.model, 'remove-link-review', removeLinkPreview);
@ -157,6 +161,9 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.setupConversationView();
this.updateAttachmentsView();
this.listenTo(this.model, 'pushPanel', this.pushPanel);
this.listenTo(this.model, 'popPanel', this.popPanel);
}
override events(): Record<string, string> {
@ -212,7 +219,9 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.showGV1Members();
},
onGoBack: () => {
this.resetPanel();
window.reduxActions.conversations.popPanelForConversation(
this.model.id
);
},
onArchive: () => {
@ -237,7 +246,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
showToast(ToastConversationUnarchived);
},
};
window.reduxActions.conversations.setSelectedConversationHeaderTitle();
// setupTimeline
@ -544,7 +552,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
const panel = this.panels[i];
panel.view.remove();
}
window.reduxActions.conversations.setSelectedConversationPanelDepth(0);
}
removeLinkPreview();
@ -624,6 +631,12 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}
showAllMedia(): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.AllMedia,
});
}
getAllMedia(): Backbone.View | undefined {
if (document.querySelectorAll('.module-media-gallery').length) {
return;
}
@ -807,19 +820,24 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
unsubscribe();
},
});
const headerTitle = window.i18n('allMedia');
const update = async () => {
const props = await getProps();
view.update(<MediaGallery i18n={window.i18n} {...props} />);
};
this.addPanel({ view, headerTitle });
update();
return view;
}
showGV1Members(): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.GroupV1Members,
});
}
getGV1Members(): Backbone.View {
const { contactCollection, id } = this.model;
const memberships =
@ -855,8 +873,9 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
),
});
this.addPanel({ view });
view.render();
return view;
}
deleteMessage(messageId: string): void {
@ -877,12 +896,20 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
} else {
this.model.decrementMessageCount();
}
this.resetPanel();
window.reduxActions.conversations.popPanelForConversation(
this.model.id
);
},
});
}
showGroupLinkManagement(): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.GroupLinkManagement,
});
}
getGroupLinkManagement(): Backbone.View {
const view = new ReactWrapperView({
className: 'panel',
JSX: window.Signal.State.Roots.createGroupLinkManagement(
@ -892,13 +919,19 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}
),
});
const headerTitle = window.i18n('ConversationDetails--group-link');
this.addPanel({ view, headerTitle });
view.render();
return view;
}
showGroupV2Permissions(): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.GroupPermissions,
});
}
getGroupV2Permissions(): Backbone.View {
const view = new ReactWrapperView({
className: 'panel',
JSX: window.Signal.State.Roots.createGroupV2Permissions(
@ -908,13 +941,19 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}
),
});
const headerTitle = window.i18n('permissions');
this.addPanel({ view, headerTitle });
view.render();
return view;
}
showPendingInvites(): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.GroupInvites,
});
}
getPendingInvites(): Backbone.View {
const view = new ReactWrapperView({
className: 'panel',
JSX: window.Signal.State.Roots.createPendingInvites(window.reduxStore, {
@ -922,15 +961,19 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
}),
});
const headerTitle = window.i18n(
'ConversationDetails--requests-and-invites'
);
this.addPanel({ view, headerTitle });
view.render();
return view;
}
showConversationNotificationsSettings(): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.NotificationSettings,
});
}
getConversationNotificationsSettings(): Backbone.View {
const view = new ReactWrapperView({
className: 'panel',
JSX: window.Signal.State.Roots.createConversationNotificationsSettings(
@ -940,26 +983,19 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}
),
});
const headerTitle = window.i18n('ConversationDetails--notifications');
this.addPanel({ view, headerTitle });
view.render();
}
showChatColorEditor(): void {
const view = new ReactWrapperView({
className: 'panel',
JSX: window.Signal.State.Roots.createChatColorPicker(window.reduxStore, {
conversationId: this.model.get('id'),
}),
});
const headerTitle = window.i18n('ChatColorPicker__menu-title');
this.addPanel({ view, headerTitle });
view.render();
return view;
}
showConversationDetails(): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.ConversationDetails,
});
}
getConversationDetails(): Backbone.View {
// Run a getProfiles in case member's capabilities have changed
// Redux should cover us on the return here so no need to await this.
if (this.model.throttledGetProfiles) {
@ -981,7 +1017,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
addMembers: this.model.addMembersV2.bind(this.model),
conversationId: this.model.get('id'),
showAllMedia: this.showAllMedia.bind(this),
showChatColorEditor: this.showChatColorEditor.bind(this),
showGroupLinkManagement: this.showGroupLinkManagement.bind(this),
showGroupV2Permissions: this.showGroupV2Permissions.bind(this),
showConversationNotificationsSettings:
@ -1000,13 +1035,24 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
props
),
});
const headerTitle = '';
this.addPanel({ view, headerTitle });
view.render();
return view;
}
showMessageDetail(messageId: string): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.MessageDetails,
args: { messageId },
});
}
getMessageDetail({
messageId,
}: {
messageId: string;
}): Backbone.View | undefined {
const message = window.MessageController.getById(messageId);
if (!message) {
throw new Error(`showMessageDetail: Message ${messageId} missing!`);
@ -1025,7 +1071,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
const onClose = () => {
this.stopListening(message, 'change', update);
this.resetPanel();
window.reduxActions.conversations.popPanelForConversation(this.model.id);
};
const view = new ReactWrapperView({
@ -1048,21 +1094,31 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.listenTo(message, 'expired', onClose);
// We could listen to all involved contacts, but we'll call that overkill
this.addPanel({ view });
view.render();
return view;
}
showStickerManager(): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.StickerManager,
});
}
getStickerManager(): Backbone.View {
const view = new ReactWrapperView({
className: ['sticker-manager-wrapper', 'panel'].join(' '),
JSX: window.Signal.State.Roots.createStickerManager(window.reduxStore),
onClose: () => {
this.resetPanel();
window.reduxActions.conversations.popPanelForConversation(
this.model.id
);
},
});
this.addPanel({ view });
view.render();
return view;
}
showContactDetail({
@ -1075,6 +1131,22 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
uuid: UUIDStringType;
};
}): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.ContactDetails,
args: { contact, signalAccount },
});
}
getContactDetail({
contact,
signalAccount,
}: {
contact: EmbeddedContactType;
signalAccount?: {
phoneNumber: string;
uuid: UUIDStringType;
};
}): Backbone.View {
const view = new ReactWrapperView({
className: 'contact-detail-pane panel',
JSX: (
@ -1090,11 +1162,13 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
/>
),
onClose: () => {
this.resetPanel();
window.reduxActions.conversations.popPanelForConversation(
this.model.id
);
},
});
this.addPanel({ view });
return view;
}
async openConversation(
@ -1108,32 +1182,63 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
);
}
addPanel(panel: PanelType): void {
pushPanel(panel: PanelRenderType): void {
if (isPanelHandledByReact(panel)) {
return;
}
this.panels = this.panels || [];
if (this.panels.length === 0) {
this.previousFocus = document.activeElement as HTMLElement;
}
this.panels.unshift(panel);
panel.view.$el.insertAfter(this.$('.panel').last());
panel.view.$el.one('animationend', () => {
panel.view.$el.addClass('panel--static');
});
const { type } = panel as BackbonePanelRenderType;
window.reduxActions.conversations.setSelectedConversationPanelDepth(
this.panels.length
);
window.reduxActions.conversations.setSelectedConversationHeaderTitle(
panel.headerTitle
);
}
resetPanel(): void {
if (!this.panels || !this.panels.length) {
let view: Backbone.View | undefined;
if (type === PanelType.AllMedia) {
view = this.getAllMedia();
} else if (panel.type === PanelType.ContactDetails) {
view = this.getContactDetail(panel.args);
} else if (type === PanelType.ConversationDetails) {
view = this.getConversationDetails();
} else if (type === PanelType.GroupInvites) {
view = this.getPendingInvites();
} else if (type === PanelType.GroupLinkManagement) {
view = this.getGroupLinkManagement();
} else if (type === PanelType.GroupPermissions) {
view = this.getGroupV2Permissions();
} else if (type === PanelType.GroupV1Members) {
view = this.getGV1Members();
} else if (type === PanelType.NotificationSettings) {
view = this.getConversationNotificationsSettings();
} else if (panel.type === PanelType.MessageDetails) {
view = this.getMessageDetail(panel.args);
} else if (type === PanelType.StickerManager) {
view = this.getStickerManager();
}
if (!view) {
return;
}
const panel = this.panels.shift();
this.panels.push({
panelType: type,
view,
});
view.$el.insertAfter(this.$('.panel').last());
view.$el.one('animationend', () => {
if (view) {
view.$el.addClass('panel--static');
}
});
}
popPanel(poppedPanel: PanelRenderType): void {
if (!this.panels || !this.panels.length) {
return;
}
if (
this.panels.length === 0 &&
@ -1144,36 +1249,42 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.previousFocus = undefined;
}
const panel = this.panels[this.panels.length - 1];
if (!panel) {
return;
}
if (isPanelHandledByReact(poppedPanel)) {
return;
}
this.panels.pop();
if (panel.panelType !== poppedPanel.type) {
log.warn('popPanel: last panel was not of same type');
return;
}
if (this.panels.length > 0) {
this.panels[0].view.$el.fadeIn(250);
this.panels[this.panels.length - 1].view.$el.fadeIn(250);
}
if (panel) {
let timeout: ReturnType<typeof setTimeout> | undefined;
const removePanel = () => {
if (!timeout) {
return;
}
let timeout: ReturnType<typeof setTimeout> | undefined;
const removePanel = () => {
if (!timeout) {
return;
}
clearTimeout(timeout);
timeout = undefined;
clearTimeout(timeout);
timeout = undefined;
panel.view.remove();
};
panel.view.$el
.addClass('panel--remove')
.one('transitionend', removePanel);
panel.view.remove();
};
panel.view.$el.addClass('panel--remove').one('transitionend', removePanel);
// Backup, in case things go wrong with the transitionend event
timeout = setTimeout(removePanel, SECOND);
}
window.reduxActions.conversations.setSelectedConversationPanelDepth(
this.panels.length
);
window.reduxActions.conversations.setSelectedConversationHeaderTitle(
this.panels[0]?.headerTitle
);
// Backup, in case things go wrong with the transitionend event
timeout = setTimeout(removePanel, SECOND);
}
async clearAttachments(): Promise<void> {

2
ts/window.d.ts vendored
View file

@ -37,7 +37,6 @@ import type { ConversationController } from './ConversationController';
import type { ReduxActions } from './state/types';
import type { createStore } from './state/createStore';
import type { createApp } from './state/roots/createApp';
import type { createChatColorPicker } from './state/roots/createChatColorPicker';
import type { createConversationDetails } from './state/roots/createConversationDetails';
import type { createGroupLinkManagement } from './state/roots/createGroupLinkManagement';
import type { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
@ -167,7 +166,6 @@ export type SignalCoreType = {
createStore: typeof createStore;
Roots: {
createApp: typeof createApp;
createChatColorPicker: typeof createChatColorPicker;
createConversationDetails: typeof createConversationDetails;
createGroupLinkManagement: typeof createGroupLinkManagement;
createGroupV2JoinModal: typeof createGroupV2JoinModal;