Adds transitions to panels

Co-authored-by: Jamie Kyle <jamie@signal.org>
This commit is contained in:
Josh Perez 2023-07-26 18:23:32 -04:00 committed by GitHub
parent bbd43b6e38
commit 4ec94367c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 708 additions and 414 deletions

View file

@ -2291,7 +2291,7 @@ ipc.on('get-config', async event => {
proxyUrl: process.env.HTTPS_PROXY || process.env.https_proxy || undefined,
contentProxyUrl: config.get<string>('contentProxyUrl'),
sfuUrl: config.get('sfuUrl'),
reducedMotionSetting: animationSettings.prefersReducedMotion,
reducedMotionSetting: DISABLE_GPU || animationSettings.prefersReducedMotion,
registrationChallengeUrl: config.get<string>('registrationChallengeUrl'),
serverPublicParams: config.get<string>('serverPublicParams'),
serverTrustRoot: config.get<string>('serverTrustRoot'),

View file

@ -1,94 +0,0 @@
// Copyright 2015 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import './mixins';
@keyframes panel--in--ltr {
from {
// stylelint-disable-next-line declaration-property-value-disallowed-list
transform: translateX(500px);
}
to {
// stylelint-disable-next-line declaration-property-value-disallowed-list
transform: translateX(0);
}
}
@keyframes panel--in--rtl {
from {
// stylelint-disable-next-line declaration-property-value-disallowed-list
transform: translateX(-500px);
}
to {
// stylelint-disable-next-line declaration-property-value-disallowed-list
transform: translateX(0);
}
}
.conversation {
@include light-theme {
background-color: $color-white;
}
@include dark-theme {
background-color: $color-gray-95;
}
.panel {
&:not(.main) {
&:dir(ltr) {
animation: panel--in--ltr 350ms cubic-bezier(0.17, 0.17, 0, 1);
}
&:dir(rtl) {
animation: panel--in--rtl 350ms cubic-bezier(0.17, 0.17, 0, 1);
}
}
&--static {
animation: none;
}
&--remove {
&:dir(ltr) {
// stylelint-disable-next-line declaration-property-value-disallowed-list
transform: translateX(100%);
}
&:dir(rtl) {
// stylelint-disable-next-line declaration-property-value-disallowed-list
transform: translateX(-100%);
}
transition: transform 350ms cubic-bezier(0.17, 0.17, 0, 1);
}
}
}
// Make sure the main panel is hidden when other panels are in the dom
.panel + .main.panel {
display: none;
}
.message-detail-wrapper {
height: calc(100% - 48px);
width: 100%;
overflow-y: auto;
}
.typing-bubble-wrapper {
margin-bottom: 20px;
}
.contact-detail-pane {
overflow-y: scroll;
padding-top: 40px;
padding-bottom: 40px;
}
.permissions-popup,
.debug-log-window {
.modal {
background-color: transparent;
padding: 0;
}
}

View file

@ -163,12 +163,6 @@ a {
}
}
.group-member-list {
.container {
outline: none;
}
}
$loading-height: 16px;
.loading {

View file

@ -1,51 +0,0 @@
// Copyright 2015 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#app-container {
height: 100%;
}
.conversation-stack,
.inbox,
.no-conversation-open {
height: 100%;
overflow: hidden;
position: relative;
}
.scrollable {
height: 100%;
overflow: auto;
}
.no-conversation-open {
display: flex;
flex-direction: column;
text-align: center;
justify-content: center;
align-items: center;
}
.conversation-stack {
flex-grow: 1;
}
.conversation.placeholder {
text-align: center;
user-select: none;
.container {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.content {
display: inline-block;
}
h3 {
font-size: large;
}
}

View file

@ -6,6 +6,16 @@
// CAUTION: these styles are often overridden by other components
// if you make changes to these, you must check EVERY component that uses <Modal.../>
#app-container {
height: 100%;
}
.inbox {
height: 100%;
overflow: hidden;
position: relative;
}
.module-title-bar-drag-area {
-webkit-app-region: drag;
height: var(--title-bar-drag-area-height);

View file

@ -8,7 +8,7 @@
position: absolute;
top: 0;
width: 100%;
z-index: $z-index-base;
z-index: $z-index-above-base;
@include light-theme() {
background-color: $color-white;
@ -18,12 +18,19 @@
background-color: $color-gray-95;
}
&__body {
margin-top: calc(#{$header-height} + var(--title-bar-drag-area-height));
}
&__header {
padding-top: var(--title-bar-drag-area-height);
align-items: center;
display: flex;
flex-direction: row;
align-items: center;
height: calc(#{$header-height} + var(--title-bar-drag-area-height));
padding-top: var(--title-bar-drag-area-height);
position: fixed;
width: 100%;
z-index: $z-index-base;
@include light-theme {
color: $color-gray-90;
@ -96,4 +103,17 @@
}
}
}
&__overlay {
height: 100%;
inset-inline-start: 0;
position: absolute;
top: 0;
width: 100%;
z-index: $z-index-above-base;
}
&__hidden {
display: none;
}
}

View file

@ -5,4 +5,32 @@
display: flex;
flex-direction: row;
height: 100%;
&__conversation-stack {
flex-grow: 1;
height: 100%;
overflow: hidden;
position: relative;
}
&__no-conversation-open {
align-items: center;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
overflow: hidden;
position: relative;
text-align: center;
}
.__conversation {
@include light-theme {
background-color: $color-white;
}
@include dark-theme {
background-color: $color-gray-95;
}
}
}

View file

@ -12,10 +12,6 @@
@import 'progress';
@import 'emoji';
// Old style: main view
@import 'index';
@import 'conversation';
// Old style: modules
@import 'modules';

View file

@ -264,24 +264,24 @@ export function Inbox({
<div className="Inbox">
<div className="module-title-bar-drag-area" />
<div className="left-pane-wrapper">{renderLeftPane()}</div>
<div id="LeftPane">{renderLeftPane()}</div>
<div className="conversation-stack">
<div className="Inbox__conversation-stack">
<div id="toast" />
{selectedConversationId && (
<div
className="conversation"
className="Inbox__conversation"
id={`conversation-${selectedConversationId}`}
>
{renderConversationView()}
</div>
)}
{!prevConversationId && (
<div className="no-conversation-open">
<div className="Inbox__no-conversation-open">
{renderMiniPlayer({ shouldFlow: false })}
<div className="module-splash-screen__logo module-img--128 module-logo-blue" />
<h3>{i18n('icu:welcomeToSignal')}</h3>
<p className="whats-new-placeholder">
<p>
<WhatsNewLink
i18n={i18n}
showWhatsNewModal={showWhatsNewModal}

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import classNames from 'classnames';
import { useEscapeHandling } from '../../hooks/useEscapeHandling';
export type PropsType = {
@ -17,6 +18,7 @@ export type PropsType = {
renderConversationHeader: () => JSX.Element;
renderTimeline: () => JSX.Element;
renderPanel: () => JSX.Element | undefined;
shouldHideConversationView?: boolean;
};
export function ConversationView({
@ -29,6 +31,7 @@ export function ConversationView({
renderConversationHeader,
renderTimeline,
renderPanel,
shouldHideConversationView,
}: PropsType): JSX.Element {
const onDrop = React.useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
@ -92,18 +95,28 @@ export function ConversationView({
);
return (
<div className="ConversationView" onDrop={onDrop} onPaste={onPaste}>
<div className="ConversationView__header">
{renderConversationHeader()}
</div>
<div className="ConversationView__pane main panel">
<div className="ConversationView__timeline--container">
<div aria-live="polite" className="ConversationView__timeline">
{renderTimeline()}
</div>
<div
className="ConversationView ConversationPanel"
onDrop={onDrop}
onPaste={onPaste}
>
<div
className={classNames('ConversationPanel', {
ConversationPanel__hidden: shouldHideConversationView,
})}
>
<div className="ConversationView__header">
{renderConversationHeader()}
</div>
<div className="ConversationView__composition-area">
{renderCompositionArea()}
<div className="ConversationView__pane">
<div className="ConversationView__timeline--container">
<div aria-live="polite" className="ConversationView__timeline">
{renderTimeline()}
</div>
</div>
<div className="ConversationView__composition-area">
{renderCompositionArea()}
</div>
</div>
</div>
{renderPanel()}

View file

@ -5,10 +5,9 @@ import { useCallback, useEffect } from 'react';
import { get } from 'lodash';
import { useSelector } from 'react-redux';
import type { PanelRenderType } from '../types/Panels';
import type { StateType } from '../state/reducer';
import * as KeyboardLayout from '../services/keyboardLayout';
import { getTopPanel } from '../state/selectors/conversations';
import { getHasPanelOpen } from '../state/selectors/conversations';
import { isInFullScreenCall } from '../state/selectors/calling';
import { isShowingAnyModal } from '../state/selectors/globalModals';
import { shouldShowStoriesView } from '../state/selectors/stories';
@ -30,10 +29,7 @@ function isCtrlOrAlt(ev: KeyboardEvent): boolean {
}
function useHasPanels(): boolean {
const topPanel = useSelector<StateType, PanelRenderType | undefined>(
getTopPanel
);
return Boolean(topPanel);
return useSelector(getHasPanelOpen);
}
function useHasGlobalModal(): boolean {

View file

@ -470,7 +470,12 @@ export type ConversationsStateType = Readonly<{
targetedMessage: string | undefined;
targetedMessageCounter: number;
targetedMessageSource: TargetedMessageSource | undefined;
targetedConversationPanels: ReadonlyArray<PanelRenderType>;
targetedConversationPanels: {
isAnimating: boolean;
direction: 'push' | 'pop' | undefined;
stack: ReadonlyArray<PanelRenderType>;
watermark: number;
};
targetedMessageForDetails?: MessageAttributesType;
lastSelectedMessage: MessageTimestamps | undefined;
@ -541,6 +546,8 @@ export const TARGETED_CONVERSATION_CHANGED =
'conversations/TARGETED_CONVERSATION_CHANGED';
const PUSH_PANEL = 'conversations/PUSH_PANEL';
const POP_PANEL = 'conversations/POP_PANEL';
const PANEL_ANIMATION_DONE = 'conversations/PANEL_ANIMATION_DONE';
const PANEL_ANIMATION_STARTED = 'conversations/PANEL_ANIMATION_STARTED';
export const MESSAGE_CHANGED = 'MESSAGE_CHANGED';
export const MESSAGE_DELETED = 'MESSAGE_DELETED';
export const MESSAGE_EXPIRED = 'conversations/MESSAGE_EXPIRED';
@ -904,6 +911,14 @@ type PopPanelActionType = ReadonlyDeep<{
type: typeof POP_PANEL;
payload: null;
}>;
type PanelAnimationDoneActionType = ReadonlyDeep<{
type: typeof PANEL_ANIMATION_DONE;
payload: null;
}>;
type PanelAnimationStartedActionType = ReadonlyDeep<{
type: typeof PANEL_ANIMATION_STARTED;
payload: null;
}>;
type ReplaceAvatarsActionType = ReadonlyDeep<{
type: typeof REPLACE_AVATARS;
@ -947,6 +962,8 @@ export type ConversationActionType =
| MessageTargetedActionType
| MessagesAddedActionType
| MessagesResetActionType
| PanelAnimationStartedActionType
| PanelAnimationDoneActionType
| PopPanelActionType
| PushPanelActionType
| RemoveAllConversationsActionType
@ -1042,6 +1059,8 @@ export const actions = {
openGiftBadge,
popPanelForConversation,
pushPanelForConversation,
panelAnimationDone,
panelAnimationStarted,
removeAllConversations,
removeConversation,
removeCustomColorOnConversations,
@ -2955,10 +2974,9 @@ function popPanelForConversation(): ThunkAction<
> {
return (dispatch, getState) => {
const { conversations } = getState();
const { targetedConversationPanels: selectedConversationPanels } =
conversations;
const { targetedConversationPanels } = conversations;
if (!selectedConversationPanels.length) {
if (!targetedConversationPanels.stack.length) {
return;
}
@ -2969,6 +2987,20 @@ function popPanelForConversation(): ThunkAction<
};
}
function panelAnimationStarted(): PanelAnimationStartedActionType {
return {
type: PANEL_ANIMATION_STARTED,
payload: null,
};
}
function panelAnimationDone(): PanelAnimationDoneActionType {
return {
type: PANEL_ANIMATION_DONE,
payload: null,
};
}
function deleteMessagesForEveryone(
messageIds: ReadonlyArray<string>
): ThunkAction<
@ -4087,7 +4119,12 @@ export function getEmptyState(): ConversationsStateType {
lastSelectedMessage: undefined,
selectedMessageIds: undefined,
showArchived: false,
targetedConversationPanels: [],
targetedConversationPanels: {
isAnimating: false,
direction: undefined,
stack: [],
watermark: -1,
},
};
}
@ -4623,7 +4660,12 @@ export function reducer(
return {
...omit(state, 'contactSpoofingReview'),
selectedConversationId,
targetedConversationPanels: [],
targetedConversationPanels: {
isAnimating: false,
direction: undefined,
stack: [],
watermark: -1,
},
messagesLookup: omit(state.messagesLookup, [...messageIds]),
messagesByConversation: omit(state.messagesByConversation, [
conversationId,
@ -5186,6 +5228,10 @@ export function reducer(
existingConversation.scrollToMessageCounter + 1,
},
},
targetedConversationPanels: {
...state.targetedConversationPanels,
watermark: -1,
},
};
}
if (action.type === MESSAGE_DELETED) {
@ -5539,46 +5585,91 @@ export function reducer(
}
if (action.type === PUSH_PANEL) {
const currentStack = state.targetedConversationPanels.stack;
const watermark = Math.min(
state.targetedConversationPanels.watermark + 1,
currentStack.length
);
const stack = [...currentStack.slice(0, watermark), action.payload];
const targetedConversationPanels = {
isAnimating: false,
direction: 'push' as const,
stack,
watermark,
};
if (action.payload.type === PanelType.MessageDetails) {
return {
...state,
targetedConversationPanels: [
...state.targetedConversationPanels,
action.payload,
],
targetedConversationPanels,
targetedMessageForDetails: action.payload.args.message,
};
}
return {
...state,
targetedConversationPanels: [
...state.targetedConversationPanels,
action.payload,
],
targetedConversationPanels,
};
}
if (action.type === POP_PANEL) {
const { targetedConversationPanels: selectedConversationPanels } = state;
const nextPanels = [...selectedConversationPanels];
const panel = nextPanels.pop();
if (!panel) {
if (state.targetedConversationPanels.watermark === -1) {
return state;
}
if (panel.type === PanelType.MessageDetails) {
const poppedPanel =
state.targetedConversationPanels.stack[
state.targetedConversationPanels.watermark
];
if (!poppedPanel) {
return state;
}
const watermark = Math.max(
state.targetedConversationPanels.watermark - 1,
-1
);
const targetedConversationPanels = {
isAnimating: false,
direction: 'pop' as const,
stack: state.targetedConversationPanels.stack,
watermark,
};
if (poppedPanel.type === PanelType.MessageDetails) {
return {
...state,
targetedConversationPanels: nextPanels,
targetedConversationPanels,
targetedMessageForDetails: undefined,
};
}
return {
...state,
targetedConversationPanels: nextPanels,
targetedConversationPanels,
};
}
if (action.type === PANEL_ANIMATION_STARTED) {
return {
...state,
targetedConversationPanels: {
...state.targetedConversationPanels,
isAnimating: true,
},
};
}
if (action.type === PANEL_ANIMATION_DONE) {
return {
...state,
targetedConversationPanels: {
...state.targetedConversationPanels,
isAnimating: false,
},
};
}

View file

@ -125,10 +125,10 @@ export const getConversationsByGroupId = createSelector(
return state.conversationsByGroupId;
}
);
export const getTargetedConversationsPanelsCount = createSelector(
export const getHasPanelOpen = createSelector(
getConversations,
(state: ConversationsStateType): number => {
return state.targetedConversationPanels.length;
(state: ConversationsStateType): boolean => {
return state.targetedConversationPanels.watermark > 0;
}
);
export const getConversationsByUsername = createSelector(
@ -1133,17 +1133,53 @@ export const getHideStoryConversationIds = createSelector(
)
);
export const getTopPanel = createSelector(
export const getActivePanel = createSelector(
getConversations,
(conversations): PanelRenderType | undefined =>
conversations.targetedConversationPanels[
conversations.targetedConversationPanels.length - 1
conversations.targetedConversationPanels.stack[
conversations.targetedConversationPanels.watermark
]
);
type PanelInformationType = {
currPanel: PanelRenderType | undefined;
direction: 'push' | 'pop';
prevPanel: PanelRenderType | undefined;
};
export const getPanelInformation = createSelector(
getConversations,
getActivePanel,
(conversations, currPanel): PanelInformationType | undefined => {
const { direction, watermark } = conversations.targetedConversationPanels;
if (!direction) {
return;
}
const watermarkDirection =
direction === 'push' ? watermark - 1 : watermark + 1;
const prevPanel =
conversations.targetedConversationPanels.stack[watermarkDirection];
return {
currPanel,
direction,
prevPanel,
};
}
);
export const getIsPanelAnimating = createSelector(
getConversations,
(conversations): boolean => {
return conversations.targetedConversationPanels.isAnimating;
}
);
export const getConversationTitle = createSelector(
getIntl,
getTopPanel,
getActivePanel,
(i18n, panel): string | undefined =>
getConversationTitleForPanelType(i18n, panel?.type)
);

View file

@ -25,9 +25,9 @@ import { getEmojiSkinTone, getTextFormattingEnabled } from '../selectors/items';
import {
getConversationSelector,
getGroupAdminsSelector,
getHasPanelOpen,
getLastEditableMessageId,
getSelectedMessageIds,
getTargetedConversationsPanelsCount,
isMissingRequiredProfileSharing,
} from '../selectors/conversations';
import { getPropsForQuote } from '../selectors/message';
@ -61,7 +61,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { id } = props;
const platform = getPlatform(state);
const shouldHidePopovers = getTargetedConversationsPanelsCount(state) > 0;
const shouldHidePopovers = getHasPanelOpen(state);
const conversationSelector = getConversationSelector(state);
const conversation = conversationSelector(id);

View file

@ -14,6 +14,7 @@ import { getPreferredBadgeSelector } from '../selectors/badges';
import {
getConversationByUuidSelector,
getConversationSelector,
getHasPanelOpen,
isMissingRequiredProfileSharing,
} from '../selectors/conversations';
import { CallMode } from '../../types/Calling';
@ -85,9 +86,7 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
const badgeSelector = useSelector(getPreferredBadgeSelector);
const badge = badgeSelector(conversation.badges);
const i18n = useSelector(getIntl);
const hasPanelShowing = useSelector<StateType, boolean>(
state => state.conversations.targetedConversationPanels.length > 0
);
const hasPanelShowing = useSelector<StateType, boolean>(getHasPanelOpen);
const outgoingCallButtonStyle = useSelector<
StateType,
OutgoingCallButtonStyle

View file

@ -1,11 +1,10 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useMemo, useRef } from 'react';
import classNames from 'classnames';
import type { MutableRefObject } from 'react';
import React, { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import type { PanelRenderType } from '../../types/Panels';
import type { StateType } from '../reducer';
import * as log from '../../logging/log';
import { ContactDetail } from '../../components/conversation/ContactDetail';
import { PanelType } from '../../types/Panels';
@ -19,102 +18,259 @@ import { SmartGroupV2Permissions } from './GroupV2Permissions';
import { SmartMessageDetail } from './MessageDetail';
import { SmartPendingInvites } from './PendingInvites';
import { SmartStickerManager } from './StickerManager';
import { getConversationTitleForPanelType } from '../../util/getConversationTitleForPanelType';
import { getIntl } from '../selectors/user';
import { getConversationTitle, getTopPanel } from '../selectors/conversations';
import { useConversationsActions } from '../ducks/conversations';
import {
getIsPanelAnimating,
getPanelInformation,
} from '../selectors/conversations';
import { focusableSelectors } from '../../util/focusableSelectors';
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({
isRTL,
onAnimationStarted,
onAnimationDone,
overlay,
panel,
}: {
isRTL: boolean;
onAnimationStarted: () => unknown;
onAnimationDone: () => unknown;
overlay: AnimationProps<{ backgroundColor: string }>;
panel: AnimationProps<{ transform: string }>;
}) {
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',
direction: isRTL ? 'reverse' : 'normal',
});
onAnimationStarted();
function onFinish() {
onAnimationDone();
}
animation.addEventListener('finish', onFinish);
return () => {
overlayAnimation?.cancel();
animation.removeEventListener('finish', onFinish);
animation.cancel();
};
}
export function ConversationPanel({
conversationId,
}: {
conversationId: string;
}): JSX.Element | null {
const i18n = useSelector(getIntl);
const { popPanelForConversation, startConversation } =
const panelInformation = useSelector(getPanelInformation);
const { panelAnimationDone, panelAnimationStarted } =
useConversationsActions();
const topPanel = useSelector<StateType, PanelRenderType | undefined>(
getTopPanel
);
const conversationTitle = useSelector(getConversationTitle);
const [shouldRenderPoppedPanel, setShouldRenderPoppedPanel] = useState(true);
const animateRef = useRef<HTMLDivElement | null>(null);
const overlayRef = useRef<HTMLDivElement | null>(null);
const prefersReducedMotion = useReducedMotion();
const selectors = useMemo(() => focusableSelectors.join(','), []);
const panelRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const panelNode = panelRef.current;
if (!panelNode) {
setShouldRenderPoppedPanel(true);
}, [panelInformation?.prevPanel]);
const i18n = useSelector(getIntl);
const isRTL = i18n.getLocaleDirection() === 'rtl';
const isAnimating = useSelector(getIsPanelAnimating);
useEffect(() => {
if (prefersReducedMotion) {
panelAnimationDone();
setShouldRenderPoppedPanel(false);
return;
}
const elements = panelNode.querySelectorAll<HTMLElement>(selectors);
if (panelInformation?.direction === 'pop') {
if (!shouldRenderPoppedPanel) {
return;
}
return doAnimate({
isRTL,
onAnimationDone: () => {
panelAnimationDone();
setShouldRenderPoppedPanel(false);
},
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%)' },
{ transform: 'translateX(100%)' },
],
},
});
}
if (panelInformation?.direction === 'push') {
if (!panelInformation?.currPanel) {
return;
}
return doAnimate({
isRTL,
onAnimationDone: panelAnimationDone,
onAnimationStarted: panelAnimationStarted,
overlay: {
ref: overlayRef,
keyframes: [
{ backgroundColor: 'rgba(0, 0, 0, 0)' },
{ backgroundColor: 'rgba(0, 0, 0, 0.2)' },
],
},
panel: {
ref: animateRef,
keyframes: [
{ transform: 'translateX(100%)' },
{ transform: 'translateX(0%)' },
],
},
});
}
return undefined;
}, [
isRTL,
panelAnimationDone,
panelAnimationStarted,
panelInformation?.currPanel,
panelInformation?.direction,
panelInformation?.prevPanel,
prefersReducedMotion,
shouldRenderPoppedPanel,
]);
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}
/>
)}
{shouldRenderPoppedPanel && (
<div className="ConversationPanel__overlay" ref={overlayRef} />
)}
{shouldRenderPoppedPanel && prevPanel && (
<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;
}
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);
const selectors = useMemo(() => focusableSelectors.join(','), []);
const focusRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!isActive) {
return;
}
const focusNode = focusRef.current;
if (!focusNode) {
return;
}
const elements = focusNode.querySelectorAll<HTMLElement>(selectors);
if (!elements.length) {
return;
}
elements[0]?.focus();
}, [selectors, topPanel]);
if (!topPanel) {
return null;
}
let panelChild: JSX.Element;
let panelClassName = '';
if (topPanel.type === PanelType.AllMedia) {
panelChild = <SmartAllMedia conversationId={conversationId} />;
} else if (topPanel.type === PanelType.ChatColorEditor) {
panelChild = <SmartChatColorPicker conversationId={conversationId} />;
} else if (topPanel.type === PanelType.ContactDetails) {
const { contact, signalAccount } = topPanel.args;
panelChild = (
<ContactDetail
contact={contact}
hasSignalAccount={Boolean(signalAccount)}
i18n={i18n}
onSendMessage={() => {
if (signalAccount) {
startConversation(signalAccount.phoneNumber, signalAccount.uuid);
}
}}
/>
);
} else if (topPanel.type === PanelType.ConversationDetails) {
panelClassName = 'conversation-details-pane';
panelChild = <SmartConversationDetails conversationId={conversationId} />;
} else if (topPanel.type === PanelType.GroupInvites) {
panelChild = (
<SmartPendingInvites
conversationId={conversationId}
ourUuid={window.storage.user.getCheckedUuid().toString()}
/>
);
} else if (topPanel.type === PanelType.GroupLinkManagement) {
panelChild = <SmartGroupLinkManagement conversationId={conversationId} />;
} else if (topPanel.type === PanelType.GroupPermissions) {
panelChild = <SmartGroupV2Permissions conversationId={conversationId} />;
} else if (topPanel.type === PanelType.GroupV1Members) {
panelClassName = 'group-member-list';
panelChild = <SmartGV1Members conversationId={conversationId} />;
} else if (topPanel.type === PanelType.MessageDetails) {
panelClassName = 'message-detail-wrapper';
panelChild = <SmartMessageDetail />;
} else if (topPanel.type === PanelType.NotificationSettings) {
panelChild = (
<SmartConversationNotificationsSettings conversationId={conversationId} />
);
} else if (topPanel.type === PanelType.StickerManager) {
panelClassName = 'sticker-manager-wrapper';
panelChild = <SmartStickerManager />;
} else {
log.warn('renderPanel: Got unexpected panel', topPanel);
return null;
}
}, [isActive, panel, selectors]);
return (
<div
className={classNames('ConversationPanel', 'panel', panelClassName)}
ref={panelRef}
>
<div className="ConversationPanel" ref={ref}>
<div className="ConversationPanel__header">
<button
aria-label={i18n('icu:goBack')}
@ -130,7 +286,84 @@ export function ConversationPanel({
</div>
)}
</div>
{panelChild}
<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} />;
}
if (panel.type === PanelType.ChatColorEditor) {
return <SmartChatColorPicker conversationId={conversationId} />;
}
if (panel.type === PanelType.ContactDetails) {
const { contact, signalAccount } = panel.args;
return (
<ContactDetail
contact={contact}
hasSignalAccount={Boolean(signalAccount)}
i18n={i18n}
onSendMessage={() => {
if (signalAccount) {
startConversation(signalAccount.phoneNumber, signalAccount.uuid);
}
}}
/>
);
}
if (panel.type === PanelType.ConversationDetails) {
return <SmartConversationDetails conversationId={conversationId} />;
}
if (panel.type === PanelType.GroupInvites) {
return (
<SmartPendingInvites
conversationId={conversationId}
ourUuid={window.storage.user.getCheckedUuid().toString()}
/>
);
}
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 (
<SmartConversationNotificationsSettings conversationId={conversationId} />
);
}
if (panel.type === PanelType.StickerManager) {
return <SmartStickerManager />;
}
log.warn(missingCaseError(panel));
return null;
}

View file

@ -10,6 +10,8 @@ import { SmartCompositionArea } from './CompositionArea';
import { SmartConversationHeader } from './ConversationHeader';
import { SmartTimeline } from './Timeline';
import {
getActivePanel,
getIsPanelAnimating,
getSelectedConversationId,
getSelectedMessageIds,
} from '../selectors/conversations';
@ -37,6 +39,12 @@ export function SmartConversationView(): JSX.Element {
);
});
const shouldHideConversationView = useSelector((state: StateType) => {
const activePanel = getActivePanel(state);
const isAnimating = getIsPanelAnimating(state);
return activePanel && !isAnimating;
});
return (
<ConversationView
conversationId={conversationId}
@ -54,6 +62,7 @@ export function SmartConversationView(): JSX.Element {
<SmartTimeline key={conversationId} id={conversationId} />
)}
renderPanel={() => <ConversationPanel conversationId={conversationId} />}
shouldHideConversationView={shouldHideConversationView}
/>
);
}

View file

@ -56,7 +56,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
assert(app);
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
const openConvo = async (contact: PrimaryDevice): Promise<void> => {
debug('opening conversation', contact.profileName);

View file

@ -106,7 +106,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
debug('opening conversation');
{
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
const item = leftPane
.locator(
@ -118,7 +118,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
}
const timeline = window.locator(
'.timeline-wrapper, .conversation .ConversationView'
'.timeline-wrapper, .Inbox__conversation .ConversationView'
);
const deltaList = new Array<number>();

View file

@ -65,7 +65,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
debug('opening conversation');
{
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
const item = leftPane.locator(
`[data-testid="${first.toContact().uuid}"] >> text=${LAST_MESSAGE}`
);
@ -73,7 +73,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
}
const timeline = window.locator(
'.timeline-wrapper, .conversation .ConversationView'
'.timeline-wrapper, .Inbox__conversation .ConversationView'
);
const deltaList = new Array<number>();

View file

@ -47,7 +47,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
const app = await bootstrap.link();
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
const item = leftPane.locator(
`[data-testid="${lastContact?.toContact().uuid}"]`

View file

@ -93,7 +93,7 @@ describe('editing', function needsName() {
}
debug('opening conversation');
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
await leftPane
.locator('.module-conversation-list__item--contact-or-conversation')
.first()
@ -153,7 +153,7 @@ describe('editing', function needsName() {
}
debug('opening conversation');
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
await leftPane
.locator('.module-conversation-list__item--contact-or-conversation')
.first()
@ -223,7 +223,7 @@ describe('editing', function needsName() {
});
debug('opening conversation');
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
await leftPane
.locator('.module-conversation-list__item--contact-or-conversation')
.first()

View file

@ -75,12 +75,12 @@ describe('senderKey', function needsName() {
distributionId,
});
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
debug('Opening group');
await leftPane.locator(`[data-testid="${group.id}"]`).click();
const conversationStack = window.locator('.conversation-stack');
const conversationStack = window.locator('.Inbox__conversation-stack');
debug('Verifying message');
await conversationStack

View file

@ -195,7 +195,7 @@ describe('story/messaging', function unknownContacts() {
{ timestamp: sentAt + 2 }
);
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
debug('Finding both replies');
await leftPane
@ -214,7 +214,7 @@ describe('story/messaging', function unknownContacts() {
debug('waiting for storage service sync to complete');
await app.waitForStorageService();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
debug('Create and send a story to the group');
await leftPane.getByRole('button', { name: 'Stories' }).click();

View file

@ -58,14 +58,14 @@ describe('unknown contacts', function unknownContacts() {
});
debug('opening conversation');
const leftPane = page.locator('.left-pane-wrapper');
const leftPane = page.locator('#LeftPane');
const conversationListItem = leftPane.getByRole('button', {
name: 'Chat with Unknown contact',
});
await conversationListItem.getByText('Message Request').click();
const conversationStack = page.locator('.conversation-stack');
const conversationStack = page.locator('.Inbox__conversation-stack');
await conversationStack.getByText('Missed voice call').waitFor();
debug('accepting message request');

View file

@ -64,7 +64,7 @@ export class App extends EventEmitter {
public async waitForEnabledComposer(): Promise<Locator> {
const window = await this.getWindow();
const composeArea = window.locator(
'.composition-area-wrapper, .conversation .ConversationView'
'.composition-area-wrapper, .Inbox__conversation .ConversationView'
);
const composeContainer = composeArea.locator(
'[data-testid=CompositionInput][data-enabled=true]'

View file

@ -49,7 +49,7 @@ describe('pnp/accept gv2 invite', function needsName() {
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
debug('Opening group');
await leftPane.locator(`[data-testid="${group.id}"]`).click();
@ -67,7 +67,7 @@ describe('pnp/accept gv2 invite', function needsName() {
const window = await app.getWindow();
const conversationStack = window.locator('.conversation-stack');
const conversationStack = window.locator('.Inbox__conversation-stack');
debug('Accepting');
await conversationStack
@ -139,7 +139,7 @@ describe('pnp/accept gv2 invite', function needsName() {
const window = await app.getWindow();
const conversationStack = window.locator('.conversation-stack');
const conversationStack = window.locator('.Inbox__conversation-stack');
debug('Declining');
await conversationStack
@ -189,7 +189,7 @@ describe('pnp/accept gv2 invite', function needsName() {
uuidKind: UUIDKind.ACI,
});
const conversationStack = window.locator('.conversation-stack');
const conversationStack = window.locator('.Inbox__conversation-stack');
debug('Waiting for the ACI invite');
await window
@ -251,7 +251,7 @@ describe('pnp/accept gv2 invite', function needsName() {
uuidKind: UUIDKind.ACI,
});
const conversationStack = window.locator('.conversation-stack');
const conversationStack = window.locator('.Inbox__conversation-stack');
debug('Declining');
await conversationStack
@ -300,7 +300,7 @@ describe('pnp/accept gv2 invite', function needsName() {
});
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
debug('Opening new group');
await leftPane.locator(`[data-testid="${group.id}"]`).click();

View file

@ -35,7 +35,7 @@ describe('pnp/change number', function needsName() {
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
debug('prepare a message for original PNI');
const messageBefore = await first.encryptText(desktop, 'Before', {

View file

@ -109,7 +109,7 @@ describe('pnp/merge', function needsName() {
const { phone } = bootstrap;
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
debug('opening conversation with the aci contact');
await leftPane
@ -246,7 +246,7 @@ describe('pnp/merge', function needsName() {
}
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
debug('opening conversation with the merged contact');
await leftPane
@ -344,7 +344,7 @@ describe('pnp/merge', function needsName() {
}
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
debug('opening conversation with the merged contact');
await leftPane

View file

@ -74,7 +74,7 @@ describe('pnp/PNI Change', function needsName() {
debug('Open conversation with contactA');
{
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
await leftPane
.locator(
@ -174,7 +174,7 @@ describe('pnp/PNI Change', function needsName() {
debug('Open conversation with contactA');
{
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
await leftPane
.locator(
@ -276,7 +276,7 @@ describe('pnp/PNI Change', function needsName() {
debug('Open conversation with contactA');
{
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
await leftPane
.locator(
@ -408,7 +408,7 @@ describe('pnp/PNI Change', function needsName() {
debug('Open conversation with contactA');
{
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
await leftPane
.locator(

View file

@ -99,8 +99,8 @@ describe('pnp/PNI Signature', function needsName() {
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const conversationStack = window.locator('.conversation-stack');
const leftPane = window.locator('#LeftPane');
const conversationStack = window.locator('.Inbox__conversation-stack');
debug('creating a stranger');
const stranger = await server.createPrimaryDevice({
@ -254,7 +254,7 @@ describe('pnp/PNI Signature', function needsName() {
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
debug('opening conversation with the pni contact');
await leftPane

View file

@ -97,8 +97,8 @@ describe('pnp/send gv2 invite', function needsName() {
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const conversationStack = window.locator('.conversation-stack');
const leftPane = window.locator('#LeftPane');
const conversationStack = window.locator('.Inbox__conversation-stack');
debug('clicking compose and "New group" buttons');

View file

@ -86,7 +86,7 @@ describe('pnp/username', function needsName() {
const { phone } = bootstrap;
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
debug('find username in the left pane');
await leftPane
@ -335,7 +335,7 @@ describe('pnp/username', function needsName() {
const window = await app.getWindow();
debug('opening note to self');
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
await leftPane.locator(`[data-testid="${desktop.uuid}"]`).click();
debug('clicking link');

View file

@ -85,8 +85,8 @@ describe('challenge/receipts', function challengeReceiptsTest() {
});
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const conversationStack = window.locator('.conversation-stack');
const leftPane = window.locator('#LeftPane');
const conversationStack = window.locator('.Inbox__conversation-stack');
debug(`Opening conversation with contact (${contact.toContact().uuid})`);
await leftPane

View file

@ -33,8 +33,8 @@ describe('storage service', function needsName() {
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const conversationStack = window.locator('.conversation-stack');
const leftPane = window.locator('#LeftPane');
const conversationStack = window.locator('.Inbox__conversation-stack');
debug('archiving contact');
{

View file

@ -41,7 +41,7 @@ describe('storage service', function needsName() {
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
debug('wait for first contact to be pinned in the left pane');
await leftPane

View file

@ -48,8 +48,8 @@ describe('storage service', function needsName() {
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const conversationStack = window.locator('.conversation-stack');
const leftPane = window.locator('#LeftPane');
const conversationStack = window.locator('.Inbox__conversation-stack');
debug('Opening conversation with a stranger');
debug(stranger.toContact().uuid);
@ -117,7 +117,7 @@ describe('storage service', function needsName() {
debug('Enter message text');
const composeArea = window.locator(
'.composition-area-wrapper, .conversation .ConversationView'
'.composition-area-wrapper, .Inbox__conversation .ConversationView'
);
const input = composeArea.locator('[data-testid=CompositionInput]');

View file

@ -36,8 +36,8 @@ describe('storage service', function needsName() {
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const conversationStack = window.locator('.conversation-stack');
const leftPane = window.locator('#LeftPane');
const conversationStack = window.locator('.Inbox__conversation-stack');
debug('Verifying that the group is pinned on startup');
await leftPane.locator(`[data-testid="${group.id}"]`).waitFor();

View file

@ -110,9 +110,9 @@ describe('storage service', function needsName() {
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
const leftPane = window.locator('#LeftPane');
const conversationView = window.locator(
'.conversation > .ConversationView'
'.Inbox__conversation > .ConversationView'
);
debug('sending two sticker pack links');

View file

@ -2391,13 +2391,6 @@
"updated": "2022-06-14T22:04:43.988Z",
"reasonDetail": "Handling outside click"
},
{
"rule": "React-useRef",
"path": "ts/components/Modal.tsx",
"line": " const modalRef = useRef<HTMLDivElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-08-05T00:22:31.660Z"
},
{
"rule": "React-useRef",
"path": "ts/components/Modal.tsx",
@ -2416,67 +2409,10 @@
},
{
"rule": "React-useRef",
"path": "ts/components/TextAttachment.tsx",
"line": " const ref = useRef<HTMLDivElement>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const sizeRef = useRef<Size | null>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const onSizeChangeRef = useRef<SizeChangeHandler | void>(onSizeChange);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const ref = useRef<any>();",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " * const scrollerRef = useRef()",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " * const scrollerInnerRef = useRef()",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const scrollRef = useRef<Scroll | null>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const onScrollChangeRef = useRef<ScrollChangeHandler>(onScrollChange);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
"path": "ts/components/Modal.tsx",
"line": " const modalRef = useRef<HTMLDivElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-08-05T00:22:31.660Z"
},
{
"rule": "React-useRef",
@ -2616,6 +2552,14 @@
"reasonCategory": "usageTrusted",
"updated": "2022-10-05T18:51:56.411Z"
},
{
"rule": "React-useRef",
"path": "ts/components/TextAttachment.tsx",
"line": " const ref = useRef<HTMLDivElement>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/components/TextAttachment.tsx",
@ -2858,6 +2802,62 @@
"reasonCategory": "usageTrusted",
"updated": "2021-10-22T00:52:39.251Z"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const sizeRef = useRef<Size | null>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const onSizeChangeRef = useRef<SizeChangeHandler | void>(onSizeChange);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const ref = useRef<any>();",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " * const scrollerRef = useRef()",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " * const scrollerInnerRef = useRef()",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const scrollRef = useRef<Scroll | null>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const onScrollChangeRef = useRef<ScrollChangeHandler>(onScrollChange);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/quill/formatting/menu.tsx",
@ -2885,9 +2885,23 @@
{
"rule": "React-useRef",
"path": "ts/state/smart/ConversationPanel.tsx",
"line": " const panelRef = useRef<HTMLDivElement | null>(null);",
"line": " const animateRef = useRef<HTMLDivElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2023-06-15T19:55:51.367Z"
"updated": "2023-07-13T23:34:39.367Z"
},
{
"rule": "React-useRef",
"path": "ts/state/smart/ConversationPanel.tsx",
"line": " const overlayRef = useRef<HTMLDivElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2023-07-13T23:34:39.367Z"
},
{
"rule": "React-useRef",
"path": "ts/state/smart/ConversationPanel.tsx",
"line": " const focusRef = useRef<HTMLDivElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2023-07-13T23:34:39.367Z"
},
{
"rule": "React-useRef",