Keep reaction poppers visible at all times
This commit is contained in:
parent
f11c366f53
commit
70d059beeb
12 changed files with 94 additions and 14 deletions
|
@ -84,6 +84,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
checkForAccount: action('checkForAccount'),
|
checkForAccount: action('checkForAccount'),
|
||||||
clearSelectedMessage: action('clearSelectedMessage'),
|
clearSelectedMessage: action('clearSelectedMessage'),
|
||||||
collapseMetadata: overrideProps.collapseMetadata,
|
collapseMetadata: overrideProps.collapseMetadata,
|
||||||
|
containerElementRef: React.createRef<HTMLElement>(),
|
||||||
conversationColor:
|
conversationColor:
|
||||||
overrideProps.conversationColor ||
|
overrideProps.conversationColor ||
|
||||||
select('conversationColor', ConversationColors, ConversationColors[0]),
|
select('conversationColor', ConversationColors, ConversationColors[0]),
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
// Copyright 2018-2021 Signal Messenger, LLC
|
// Copyright 2018-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React, { RefObject } from 'react';
|
||||||
import ReactDOM, { createPortal } from 'react-dom';
|
import ReactDOM, { createPortal } from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { drop, groupBy, orderBy, take, unescape } from 'lodash';
|
import { drop, groupBy, orderBy, take, unescape } from 'lodash';
|
||||||
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
|
import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preventOverflow';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
|
@ -188,6 +189,7 @@ export type PropsData = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsHousekeeping = {
|
export type PropsHousekeeping = {
|
||||||
|
containerElementRef: RefObject<HTMLElement>;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
interactionMode: InteractionModeType;
|
interactionMode: InteractionModeType;
|
||||||
theme?: ThemeType;
|
theme?: ThemeType;
|
||||||
|
@ -1443,7 +1445,13 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
{reactionPickerRoot &&
|
{reactionPickerRoot &&
|
||||||
createPortal(
|
createPortal(
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
<Popper placement="top" modifiers={[offsetDistanceModifier(4)]}>
|
<Popper
|
||||||
|
placement="top"
|
||||||
|
modifiers={[
|
||||||
|
offsetDistanceModifier(4),
|
||||||
|
this.popperPreventOverflowModifier(),
|
||||||
|
]}
|
||||||
|
>
|
||||||
{({ ref, style }) => (
|
{({ ref, style }) => (
|
||||||
<SmartReactionPicker
|
<SmartReactionPicker
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -1801,6 +1809,23 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private popperPreventOverflowModifier(): Partial<PreventOverflowModifier> {
|
||||||
|
const { containerElementRef } = this.props;
|
||||||
|
return {
|
||||||
|
name: 'preventOverflow',
|
||||||
|
options: {
|
||||||
|
altAxis: true,
|
||||||
|
boundary: containerElementRef.current || undefined,
|
||||||
|
padding: {
|
||||||
|
bottom: 16,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
top: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public toggleReactionViewer = (onlyRemove = false): void => {
|
public toggleReactionViewer = (onlyRemove = false): void => {
|
||||||
this.setState(({ reactionViewerRoot }) => {
|
this.setState(({ reactionViewerRoot }) => {
|
||||||
if (reactionViewerRoot) {
|
if (reactionViewerRoot) {
|
||||||
|
@ -2022,7 +2047,11 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
</Reference>
|
</Reference>
|
||||||
{reactionViewerRoot &&
|
{reactionViewerRoot &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<Popper placement={popperPlacement}>
|
<Popper
|
||||||
|
placement={popperPlacement}
|
||||||
|
strategy="fixed"
|
||||||
|
modifiers={[this.popperPreventOverflowModifier()]}
|
||||||
|
>
|
||||||
{({ ref, style }) => (
|
{({ ref, style }) => (
|
||||||
<ReactionViewer
|
<ReactionViewer
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
@ -94,6 +94,8 @@ const _keyForError = (error: Error): string => {
|
||||||
export class MessageDetail extends React.Component<Props> {
|
export class MessageDetail extends React.Component<Props> {
|
||||||
private readonly focusRef = React.createRef<HTMLDivElement>();
|
private readonly focusRef = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
private readonly messageContainerRef = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
// When this component is created, it's initially not part of the DOM, and then it's
|
// When this component is created, it's initially not part of the DOM, and then it's
|
||||||
// added off-screen and animated in. This ensures that the focus takes.
|
// added off-screen and animated in. This ensures that the focus takes.
|
||||||
|
@ -289,13 +291,17 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||||
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
|
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
|
||||||
<div className="module-message-detail__message-container">
|
<div
|
||||||
|
className="module-message-detail__message-container"
|
||||||
|
ref={this.messageContainerRef}
|
||||||
|
>
|
||||||
<Message
|
<Message
|
||||||
{...message}
|
{...message}
|
||||||
renderingContext="conversation/MessageDetail"
|
renderingContext="conversation/MessageDetail"
|
||||||
checkForAccount={checkForAccount}
|
checkForAccount={checkForAccount}
|
||||||
clearSelectedMessage={clearSelectedMessage}
|
clearSelectedMessage={clearSelectedMessage}
|
||||||
contactNameColor={contactNameColor}
|
contactNameColor={contactNameColor}
|
||||||
|
containerElementRef={this.messageContainerRef}
|
||||||
deleteMessage={deleteMessage}
|
deleteMessage={deleteMessage}
|
||||||
deleteMessageForEveryone={deleteMessageForEveryone}
|
deleteMessageForEveryone={deleteMessageForEveryone}
|
||||||
disableMenu
|
disableMenu
|
||||||
|
|
|
@ -38,6 +38,7 @@ const defaultMessageProps: MessagesProps = {
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
checkForAccount: action('checkForAccount'),
|
checkForAccount: action('checkForAccount'),
|
||||||
clearSelectedMessage: action('default--clearSelectedMessage'),
|
clearSelectedMessage: action('default--clearSelectedMessage'),
|
||||||
|
containerElementRef: React.createRef<HTMLElement>(),
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversationId',
|
conversationId: 'conversationId',
|
||||||
conversationType: 'direct', // override
|
conversationType: 'direct', // override
|
||||||
|
|
|
@ -376,7 +376,13 @@ const actions = () => ({
|
||||||
unblurAvatar: action('unblurAvatar'),
|
unblurAvatar: action('unblurAvatar'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderItem = (id: string) => (
|
const renderItem = (
|
||||||
|
id: string,
|
||||||
|
_conversationId: unknown,
|
||||||
|
_onHeightChange: unknown,
|
||||||
|
_actionProps: unknown,
|
||||||
|
containerElementRef: React.RefObject<HTMLElement>
|
||||||
|
) => (
|
||||||
<TimelineItem
|
<TimelineItem
|
||||||
id=""
|
id=""
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
|
@ -384,6 +390,7 @@ const renderItem = (id: string) => (
|
||||||
item={items[id]}
|
item={items[id]}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
interactionMode="keyboard"
|
interactionMode="keyboard"
|
||||||
|
containerElementRef={containerElementRef}
|
||||||
conversationId=""
|
conversationId=""
|
||||||
renderContact={() => '*ContactName*'}
|
renderContact={() => '*ContactName*'}
|
||||||
renderUniversalTimerNotification={() => (
|
renderUniversalTimerNotification={() => (
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import { debounce, get, isNumber, pick, identity } from 'lodash';
|
import { debounce, get, isNumber, pick, identity } from 'lodash';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { CSSProperties, ReactChild, ReactNode } from 'react';
|
import React, { CSSProperties, ReactChild, ReactNode, RefObject } from 'react';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import {
|
import {
|
||||||
AutoSizer,
|
AutoSizer,
|
||||||
|
@ -107,7 +107,8 @@ type PropsHousekeepingType = {
|
||||||
id: string,
|
id: string,
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
onHeightChange: (messageId: string) => unknown,
|
onHeightChange: (messageId: string) => unknown,
|
||||||
actions: PropsActionsType
|
actions: PropsActionsType,
|
||||||
|
containerElementRef: RefObject<HTMLElement>
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
renderLastSeenIndicator: (id: string) => JSX.Element;
|
renderLastSeenIndicator: (id: string) => JSX.Element;
|
||||||
renderHeroRow: (
|
renderHeroRow: (
|
||||||
|
@ -297,7 +298,9 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
|
|
||||||
public resizeFlag = false;
|
public resizeFlag = false;
|
||||||
|
|
||||||
public listRef = React.createRef<List>();
|
private readonly containerRef = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
private readonly listRef = React.createRef<List>();
|
||||||
|
|
||||||
public visibleRows: VisibleRowsType | undefined;
|
public visibleRows: VisibleRowsType | undefined;
|
||||||
|
|
||||||
|
@ -808,7 +811,13 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
role="row"
|
role="row"
|
||||||
>
|
>
|
||||||
<ErrorBoundary i18n={i18n} showDebugLog={() => window.showDebugLog()}>
|
<ErrorBoundary i18n={i18n} showDebugLog={() => window.showDebugLog()}>
|
||||||
{renderItem(messageId, id, this.resizeMessage, actions)}
|
{renderItem(
|
||||||
|
messageId,
|
||||||
|
id,
|
||||||
|
this.resizeMessage,
|
||||||
|
actions,
|
||||||
|
this.containerRef
|
||||||
|
)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1502,6 +1511,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
|
ref={this.containerRef}
|
||||||
>
|
>
|
||||||
{timelineWarning}
|
{timelineWarning}
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ const renderUniversalTimerNotification = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const getDefaultProps = () => ({
|
const getDefaultProps = () => ({
|
||||||
|
containerElementRef: React.createRef<HTMLElement>(),
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
id: 'asdf',
|
id: 'asdf',
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2019-2021 Signal Messenger, LLC
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React, { RefObject } from 'react';
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
|
|
||||||
import { LocalizerType, ThemeType } from '../../types/Util';
|
import { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
|
@ -153,6 +153,7 @@ export type TimelineItemType =
|
||||||
| VerificationNotificationType;
|
| VerificationNotificationType;
|
||||||
|
|
||||||
type PropsLocalType = {
|
type PropsLocalType = {
|
||||||
|
containerElementRef: RefObject<HTMLElement>;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
item?: TimelineItemType;
|
item?: TimelineItemType;
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -179,6 +180,7 @@ export type PropsType = PropsLocalType &
|
||||||
export class TimelineItem extends React.PureComponent<PropsType> {
|
export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
public render(): JSX.Element | null {
|
public render(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
|
containerElementRef,
|
||||||
conversationId,
|
conversationId,
|
||||||
id,
|
id,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
@ -204,6 +206,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
<Message
|
<Message
|
||||||
{...omit(this.props, ['item'])}
|
{...omit(this.props, ['item'])}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
|
containerElementRef={containerElementRef}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
renderingContext="conversation/TimelineItem"
|
renderingContext="conversation/TimelineItem"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { isEmpty, mapValues, pick } from 'lodash';
|
import { isEmpty, mapValues, pick } from 'lodash';
|
||||||
import React from 'react';
|
import React, { RefObject } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import memoizee from 'memoizee';
|
import memoizee from 'memoizee';
|
||||||
|
|
||||||
|
@ -66,11 +66,13 @@ function renderItem(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
onHeightChange: (messageId: string) => unknown,
|
onHeightChange: (messageId: string) => unknown,
|
||||||
actionProps: TimelineActionsType
|
actionProps: TimelineActionsType,
|
||||||
|
containerElementRef: RefObject<HTMLElement>
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<SmartTimelineItem
|
<SmartTimelineItem
|
||||||
{...actionProps}
|
{...actionProps}
|
||||||
|
containerElementRef={containerElementRef}
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
id={messageId}
|
id={messageId}
|
||||||
onHeightChange={createBoundOnHeightChange(onHeightChange, messageId)}
|
onHeightChange={createBoundOnHeightChange(onHeightChange, messageId)}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2019-2021 Signal Messenger, LLC
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React, { RefObject } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
@ -21,6 +21,7 @@ import { SmartUniversalTimerNotification } from './UniversalTimerNotification';
|
||||||
type ExternalProps = {
|
type ExternalProps = {
|
||||||
id: string;
|
id: string;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
containerElementRef: RefObject<HTMLElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Workaround: A react component's required properties are filtering up through connect()
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
|
@ -38,7 +39,7 @@ function renderUniversalTimerNotification(): JSX.Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
const { id, conversationId } = props;
|
const { id, conversationId, containerElementRef } = props;
|
||||||
|
|
||||||
const messageSelector = getMessageSelector(state);
|
const messageSelector = getMessageSelector(state);
|
||||||
const item = messageSelector(id);
|
const item = messageSelector(id);
|
||||||
|
@ -51,6 +52,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
return {
|
return {
|
||||||
item,
|
item,
|
||||||
id,
|
id,
|
||||||
|
containerElementRef,
|
||||||
conversationId,
|
conversationId,
|
||||||
conversationColor: conversation?.conversationColor,
|
conversationColor: conversation?.conversationColor,
|
||||||
customColor: conversation?.customColor,
|
customColor: conversation?.customColor,
|
||||||
|
|
|
@ -13845,6 +13845,14 @@
|
||||||
"updated": "2019-11-01T22:46:33.013Z",
|
"updated": "2019-11-01T22:46:33.013Z",
|
||||||
"reasonDetail": "Used for setting focus only"
|
"reasonDetail": "Used for setting focus only"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-createRef",
|
||||||
|
"path": "ts/components/conversation/MessageDetail.js",
|
||||||
|
"line": " this.messageContainerRef = react_1.default.createRef();",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-08-20T16:48:00.885Z",
|
||||||
|
"reasonDetail": "Needed to confine Poppers. We don't actually manipulate this DOM reference."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/conversation/Quote.js",
|
"path": "ts/components/conversation/Quote.js",
|
||||||
|
@ -13869,6 +13877,14 @@
|
||||||
"updated": "2019-07-31T00:19:18.696Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Timeline needs to interact with its child List directly"
|
"reasonDetail": "Timeline needs to interact with its child List directly"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-createRef",
|
||||||
|
"path": "ts/components/conversation/Timeline.js",
|
||||||
|
"line": " this.containerRef = react_1.default.createRef();",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-08-20T16:48:00.885Z",
|
||||||
|
"reasonDetail": "Needed to confine Poppers. We don't actually manipulate this DOM reference."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.js",
|
"path": "ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.js",
|
||||||
|
|
|
@ -26,6 +26,8 @@ const excludedFilesRegexps = [
|
||||||
|
|
||||||
// Non-distributed files
|
// Non-distributed files
|
||||||
'\\.d\\.ts$',
|
'\\.d\\.ts$',
|
||||||
|
'.+\\.stories\\.js',
|
||||||
|
'.+\\.stories\\.tsx',
|
||||||
|
|
||||||
// High-traffic files in our project
|
// High-traffic files in our project
|
||||||
'^app/.+(ts|js)',
|
'^app/.+(ts|js)',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue