2023-06-15 22:26:53 +00:00
|
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2023-07-26 22:23:32 +00:00
|
|
|
import type { MutableRefObject } from 'react';
|
2023-08-21 20:18:22 +00:00
|
|
|
import React, {
|
|
|
|
forwardRef,
|
2024-03-13 20:44:13 +00:00
|
|
|
memo,
|
2023-08-21 20:18:22 +00:00
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
useRef,
|
|
|
|
useState,
|
|
|
|
} from 'react';
|
2023-06-15 22:26:53 +00:00
|
|
|
import { useSelector } from 'react-redux';
|
|
|
|
import type { PanelRenderType } from '../../types/Panels';
|
|
|
|
import * as log from '../../logging/log';
|
|
|
|
import { ContactDetail } from '../../components/conversation/ContactDetail';
|
|
|
|
import { PanelType } from '../../types/Panels';
|
|
|
|
import { SmartAllMedia } from './AllMedia';
|
|
|
|
import { SmartChatColorPicker } from './ChatColorPicker';
|
|
|
|
import { SmartConversationDetails } from './ConversationDetails';
|
|
|
|
import { SmartConversationNotificationsSettings } from './ConversationNotificationsSettings';
|
|
|
|
import { SmartGV1Members } from './GV1Members';
|
|
|
|
import { SmartGroupLinkManagement } from './GroupLinkManagement';
|
|
|
|
import { SmartGroupV2Permissions } from './GroupV2Permissions';
|
|
|
|
import { SmartMessageDetail } from './MessageDetail';
|
|
|
|
import { SmartPendingInvites } from './PendingInvites';
|
|
|
|
import { SmartStickerManager } from './StickerManager';
|
2023-07-26 22:23:32 +00:00
|
|
|
import { getConversationTitleForPanelType } from '../../util/getConversationTitleForPanelType';
|
2023-06-15 22:26:53 +00:00
|
|
|
import { getIntl } from '../selectors/user';
|
2023-07-26 22:23:32 +00:00
|
|
|
import {
|
|
|
|
getIsPanelAnimating,
|
|
|
|
getPanelInformation,
|
2023-08-21 20:18:22 +00:00
|
|
|
getWasPanelAnimated,
|
2023-07-26 22:23:32 +00:00
|
|
|
} from '../selectors/conversations';
|
2024-03-04 20:32:51 +00:00
|
|
|
import { focusableSelector } from '../../util/focusableSelectors';
|
2023-07-26 22:23:32 +00:00
|
|
|
import { missingCaseError } from '../../util/missingCaseError';
|
|
|
|
import { useConversationsActions } from '../ducks/conversations';
|
|
|
|
import { useReducedMotion } from '../../hooks/useReducedMotion';
|
|
|
|
|
|
|
|
const ANIMATION_CONFIG = {
|
|
|
|
duration: 350,
|
|
|
|
easing: 'cubic-bezier(0.17, 0.17, 0, 1)',
|
|
|
|
fill: 'forwards' as const,
|
|
|
|
};
|
|
|
|
|
|
|
|
type AnimationProps<T> = {
|
|
|
|
ref: MutableRefObject<HTMLDivElement | null>;
|
|
|
|
keyframes: Array<T>;
|
|
|
|
};
|
|
|
|
|
|
|
|
function doAnimate({
|
|
|
|
onAnimationStarted,
|
|
|
|
onAnimationDone,
|
|
|
|
overlay,
|
|
|
|
panel,
|
|
|
|
}: {
|
|
|
|
isRTL: boolean;
|
|
|
|
onAnimationStarted: () => unknown;
|
|
|
|
onAnimationDone: () => unknown;
|
|
|
|
overlay: AnimationProps<{ backgroundColor: string }>;
|
2024-02-28 20:37:13 +00:00
|
|
|
panel: AnimationProps<
|
|
|
|
{ transform: string } | { left: string } | { right: string }
|
|
|
|
>;
|
2023-07-26 22:23:32 +00:00
|
|
|
}) {
|
|
|
|
const animateNode = panel.ref.current;
|
|
|
|
if (!animateNode) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const overlayAnimation = overlay.ref.current?.animate(overlay.keyframes, {
|
|
|
|
...ANIMATION_CONFIG,
|
|
|
|
id: 'panel-animation-overlay',
|
|
|
|
});
|
|
|
|
|
|
|
|
const animation = animateNode.animate(panel.keyframes, {
|
|
|
|
...ANIMATION_CONFIG,
|
|
|
|
id: 'panel-animation',
|
|
|
|
});
|
|
|
|
|
|
|
|
onAnimationStarted();
|
|
|
|
|
|
|
|
function onFinish() {
|
|
|
|
onAnimationDone();
|
|
|
|
}
|
|
|
|
|
|
|
|
animation.addEventListener('finish', onFinish);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
overlayAnimation?.cancel();
|
|
|
|
animation.removeEventListener('finish', onFinish);
|
|
|
|
animation.cancel();
|
|
|
|
};
|
|
|
|
}
|
2023-06-15 22:26:53 +00:00
|
|
|
|
2024-03-13 20:44:13 +00:00
|
|
|
export const ConversationPanel = memo(function ConversationPanel({
|
2023-06-15 22:26:53 +00:00
|
|
|
conversationId,
|
|
|
|
}: {
|
|
|
|
conversationId: string;
|
2024-03-13 20:44:13 +00:00
|
|
|
}) {
|
2023-07-26 22:23:32 +00:00
|
|
|
const panelInformation = useSelector(getPanelInformation);
|
|
|
|
const { panelAnimationDone, panelAnimationStarted } =
|
2023-07-10 22:44:32 +00:00
|
|
|
useConversationsActions();
|
2023-08-21 20:18:22 +00:00
|
|
|
|
2023-07-26 22:23:32 +00:00
|
|
|
const animateRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
const overlayRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
const prefersReducedMotion = useReducedMotion();
|
|
|
|
|
|
|
|
const i18n = useSelector(getIntl);
|
|
|
|
const isRTL = i18n.getLocaleDirection() === 'rtl';
|
|
|
|
|
|
|
|
const isAnimating = useSelector(getIsPanelAnimating);
|
2023-08-21 20:18:22 +00:00
|
|
|
const wasAnimated = useSelector(getWasPanelAnimated);
|
|
|
|
|
|
|
|
const [lastPanelDoneAnimating, setLastPanelDoneAnimating] =
|
|
|
|
useState<PanelRenderType | null>(null);
|
|
|
|
|
|
|
|
const wasAnimatedRef = useRef(wasAnimated);
|
|
|
|
useEffect(() => {
|
|
|
|
wasAnimatedRef.current = wasAnimated;
|
|
|
|
}, [wasAnimated]);
|
2023-07-26 22:23:32 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
2023-08-21 20:18:22 +00:00
|
|
|
setLastPanelDoneAnimating(null);
|
|
|
|
}, [panelInformation?.prevPanel]);
|
|
|
|
|
|
|
|
const onAnimationDone = useCallback(
|
|
|
|
(panel: PanelRenderType | null) => {
|
|
|
|
setLastPanelDoneAnimating(panel);
|
2023-07-26 22:23:32 +00:00
|
|
|
panelAnimationDone();
|
2023-08-21 20:18:22 +00:00
|
|
|
},
|
|
|
|
[panelAnimationDone]
|
|
|
|
);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (prefersReducedMotion || wasAnimatedRef.current) {
|
|
|
|
onAnimationDone(panelInformation?.prevPanel ?? null);
|
2023-07-26 22:23:32 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (panelInformation?.direction === 'pop') {
|
|
|
|
return doAnimate({
|
|
|
|
isRTL,
|
|
|
|
onAnimationDone: () => {
|
2023-08-21 20:18:22 +00:00
|
|
|
onAnimationDone(panelInformation?.prevPanel ?? null);
|
2023-07-26 22:23:32 +00:00
|
|
|
},
|
|
|
|
onAnimationStarted: panelAnimationStarted,
|
|
|
|
overlay: {
|
|
|
|
ref: overlayRef,
|
|
|
|
keyframes: [
|
|
|
|
{ backgroundColor: 'rgba(0, 0, 0, 0.2)' },
|
|
|
|
{ backgroundColor: 'rgba(0, 0, 0, 0)' },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
panel: {
|
|
|
|
ref: animateRef,
|
|
|
|
keyframes: [
|
|
|
|
{ transform: 'translateX(0%)' },
|
2023-11-22 16:20:39 +00:00
|
|
|
{ transform: isRTL ? 'translateX(-100%)' : 'translateX(100%)' },
|
2023-07-26 22:23:32 +00:00
|
|
|
],
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (panelInformation?.direction === 'push') {
|
|
|
|
return doAnimate({
|
|
|
|
isRTL,
|
2023-08-21 20:18:22 +00:00
|
|
|
onAnimationDone: () => {
|
|
|
|
onAnimationDone(panelInformation?.prevPanel ?? null);
|
|
|
|
},
|
2023-07-26 22:23:32 +00:00
|
|
|
onAnimationStarted: panelAnimationStarted,
|
|
|
|
overlay: {
|
|
|
|
ref: overlayRef,
|
|
|
|
keyframes: [
|
|
|
|
{ backgroundColor: 'rgba(0, 0, 0, 0)' },
|
|
|
|
{ backgroundColor: 'rgba(0, 0, 0, 0.2)' },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
panel: {
|
|
|
|
ref: animateRef,
|
|
|
|
keyframes: [
|
2024-02-28 20:37:13 +00:00
|
|
|
// Note that we can't use translateX here because it breaks
|
|
|
|
// gradients for the message in message details screen.
|
|
|
|
// See: https://issues.chromium.org/issues/327027598
|
|
|
|
isRTL ? { right: '100%' } : { left: '100%' },
|
|
|
|
isRTL ? { right: '0' } : { left: '0' },
|
2023-07-26 22:23:32 +00:00
|
|
|
],
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}, [
|
|
|
|
isRTL,
|
2023-08-21 20:18:22 +00:00
|
|
|
onAnimationDone,
|
2023-07-26 22:23:32 +00:00
|
|
|
panelAnimationStarted,
|
|
|
|
panelInformation?.currPanel,
|
|
|
|
panelInformation?.direction,
|
|
|
|
panelInformation?.prevPanel,
|
|
|
|
prefersReducedMotion,
|
|
|
|
]);
|
|
|
|
|
|
|
|
if (!panelInformation) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { currPanel: activePanel, direction, prevPanel } = panelInformation;
|
|
|
|
|
|
|
|
if (!direction) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (direction === 'pop') {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{activePanel && (
|
|
|
|
<PanelContainer
|
|
|
|
conversationId={conversationId}
|
|
|
|
isActive
|
|
|
|
panel={activePanel}
|
|
|
|
/>
|
|
|
|
)}
|
2023-08-21 20:18:22 +00:00
|
|
|
{lastPanelDoneAnimating !== prevPanel && (
|
2023-07-26 22:23:32 +00:00
|
|
|
<div className="ConversationPanel__overlay" ref={overlayRef} />
|
|
|
|
)}
|
2023-08-21 20:18:22 +00:00
|
|
|
{prevPanel && lastPanelDoneAnimating !== prevPanel && (
|
2023-07-26 22:23:32 +00:00
|
|
|
<PanelContainer
|
|
|
|
conversationId={conversationId}
|
|
|
|
panel={prevPanel}
|
|
|
|
ref={animateRef}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (direction === 'push' && activePanel) {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{isAnimating && prevPanel && (
|
|
|
|
<PanelContainer conversationId={conversationId} panel={prevPanel} />
|
|
|
|
)}
|
|
|
|
<div className="ConversationPanel__overlay" ref={overlayRef} />
|
|
|
|
<PanelContainer
|
|
|
|
conversationId={conversationId}
|
|
|
|
isActive
|
|
|
|
panel={activePanel}
|
|
|
|
ref={animateRef}
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
2024-03-13 20:44:13 +00:00
|
|
|
});
|
2023-07-26 22:23:32 +00:00
|
|
|
|
|
|
|
type PanelPropsType = {
|
|
|
|
conversationId: string;
|
|
|
|
panel: PanelRenderType;
|
|
|
|
};
|
|
|
|
|
|
|
|
const PanelContainer = forwardRef<
|
|
|
|
HTMLDivElement,
|
|
|
|
PanelPropsType & { isActive?: boolean }
|
|
|
|
>(function PanelContainerInner(
|
|
|
|
{ conversationId, isActive, panel },
|
|
|
|
ref
|
|
|
|
): JSX.Element {
|
|
|
|
const i18n = useSelector(getIntl);
|
|
|
|
const { popPanelForConversation } = useConversationsActions();
|
|
|
|
const conversationTitle = getConversationTitleForPanelType(i18n, panel.type);
|
2023-06-15 22:26:53 +00:00
|
|
|
|
2023-07-26 22:23:32 +00:00
|
|
|
const focusRef = useRef<HTMLDivElement | null>(null);
|
2023-06-15 22:26:53 +00:00
|
|
|
useEffect(() => {
|
2023-07-26 22:23:32 +00:00
|
|
|
if (!isActive) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const focusNode = focusRef.current;
|
|
|
|
if (!focusNode) {
|
2023-06-15 22:26:53 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-03-04 20:32:51 +00:00
|
|
|
const elements = focusNode.querySelectorAll<HTMLElement>(focusableSelector);
|
2023-06-15 22:26:53 +00:00
|
|
|
if (!elements.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
elements[0]?.focus();
|
2024-03-04 20:32:51 +00:00
|
|
|
}, [isActive, panel]);
|
2023-06-15 22:26:53 +00:00
|
|
|
|
2023-07-26 22:23:32 +00:00
|
|
|
return (
|
|
|
|
<div className="ConversationPanel" ref={ref}>
|
|
|
|
<div className="ConversationPanel__header">
|
|
|
|
<button
|
|
|
|
aria-label={i18n('icu:goBack')}
|
|
|
|
className="ConversationPanel__header__back-button"
|
|
|
|
onClick={popPanelForConversation}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
{conversationTitle && (
|
|
|
|
<div className="ConversationPanel__header__info">
|
|
|
|
<div className="ConversationPanel__header__info__title">
|
|
|
|
{conversationTitle}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<div className="ConversationPanel__body" ref={focusRef}>
|
|
|
|
<PanelElement conversationId={conversationId} panel={panel} />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
function PanelElement({
|
|
|
|
conversationId,
|
|
|
|
panel,
|
|
|
|
}: PanelPropsType): JSX.Element | null {
|
|
|
|
const i18n = useSelector(getIntl);
|
|
|
|
const { startConversation } = useConversationsActions();
|
|
|
|
|
|
|
|
if (panel.type === PanelType.AllMedia) {
|
|
|
|
return <SmartAllMedia conversationId={conversationId} />;
|
2023-06-15 22:26:53 +00:00
|
|
|
}
|
|
|
|
|
2023-07-26 22:23:32 +00:00
|
|
|
if (panel.type === PanelType.ChatColorEditor) {
|
|
|
|
return <SmartChatColorPicker conversationId={conversationId} />;
|
|
|
|
}
|
2023-06-15 22:26:53 +00:00
|
|
|
|
2023-07-26 22:23:32 +00:00
|
|
|
if (panel.type === PanelType.ContactDetails) {
|
|
|
|
const { contact, signalAccount } = panel.args;
|
2023-06-15 22:26:53 +00:00
|
|
|
|
2023-07-26 22:23:32 +00:00
|
|
|
return (
|
2023-06-15 22:26:53 +00:00
|
|
|
<ContactDetail
|
|
|
|
contact={contact}
|
|
|
|
hasSignalAccount={Boolean(signalAccount)}
|
|
|
|
i18n={i18n}
|
|
|
|
onSendMessage={() => {
|
|
|
|
if (signalAccount) {
|
2023-08-16 20:54:39 +00:00
|
|
|
startConversation(
|
|
|
|
signalAccount.phoneNumber,
|
|
|
|
signalAccount.serviceId
|
|
|
|
);
|
2023-06-15 22:26:53 +00:00
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
2023-07-26 22:23:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (panel.type === PanelType.ConversationDetails) {
|
|
|
|
return <SmartConversationDetails conversationId={conversationId} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (panel.type === PanelType.GroupInvites) {
|
|
|
|
return (
|
2023-06-15 22:26:53 +00:00
|
|
|
<SmartPendingInvites
|
|
|
|
conversationId={conversationId}
|
2023-08-10 16:43:33 +00:00
|
|
|
ourAci={window.storage.user.getCheckedAci()}
|
2023-06-15 22:26:53 +00:00
|
|
|
/>
|
|
|
|
);
|
2023-07-26 22:23:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (panel.type === PanelType.GroupLinkManagement) {
|
|
|
|
return <SmartGroupLinkManagement conversationId={conversationId} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (panel.type === PanelType.GroupPermissions) {
|
|
|
|
return <SmartGroupV2Permissions conversationId={conversationId} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (panel.type === PanelType.GroupV1Members) {
|
|
|
|
return <SmartGV1Members conversationId={conversationId} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (panel.type === PanelType.MessageDetails) {
|
|
|
|
return <SmartMessageDetail />;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (panel.type === PanelType.NotificationSettings) {
|
|
|
|
return (
|
2023-06-15 22:26:53 +00:00
|
|
|
<SmartConversationNotificationsSettings conversationId={conversationId} />
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-07-26 22:23:32 +00:00
|
|
|
if (panel.type === PanelType.StickerManager) {
|
|
|
|
return <SmartStickerManager />;
|
|
|
|
}
|
|
|
|
|
|
|
|
log.warn(missingCaseError(panel));
|
|
|
|
return null;
|
2023-06-15 22:26:53 +00:00
|
|
|
}
|