mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-09-28 16:09:49 +01:00
Fix more focus behavior
Co-authored-by: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
co-authored by
Jamie
parent
0e1cf8e73d
commit
377378ac91
+10
-20
@@ -353,13 +353,13 @@ $message-padding-horizontal: 12px;
|
||||
}
|
||||
|
||||
.module-message__container--targeted {
|
||||
&.module-message__container--pointer-mode {
|
||||
.module-message--flashing & {
|
||||
animation: module-message__highlight 1.2s cubic-bezier(0.17, 0.17, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.module-message__container--targeted-lighter {
|
||||
&.module-message__container--pointer-mode {
|
||||
.module-message--flashing & {
|
||||
animation: module-message__highlight-lighter 1.2s
|
||||
cubic-bezier(0.17, 0.17, 0, 1);
|
||||
}
|
||||
@@ -437,14 +437,12 @@ $message-padding-horizontal: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-message {
|
||||
@include mixins.focus-visible-within {
|
||||
@include mixins.light-theme {
|
||||
background: variables.$color-selected-message-background-light;
|
||||
}
|
||||
@include mixins.dark-theme {
|
||||
background: variables.$color-selected-message-background-dark;
|
||||
}
|
||||
.module-message:focus-visible {
|
||||
@include mixins.light-theme {
|
||||
background: variables.$color-selected-message-background-light;
|
||||
}
|
||||
@include mixins.dark-theme {
|
||||
background: variables.$color-selected-message-background-dark;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1368,15 +1366,7 @@ $message-padding-horizontal: 12px;
|
||||
}
|
||||
|
||||
.module-Avatar__contents {
|
||||
outline: 3px solid;
|
||||
|
||||
@include mixins.light-theme() {
|
||||
outline-color: variables.$color-white;
|
||||
}
|
||||
|
||||
@include mixins.dark-theme() {
|
||||
outline-color: variables.$color-gray-95;
|
||||
}
|
||||
outline: 3px solid var(--axo-color-surface-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2377,7 +2367,7 @@ button.ConversationDetails__action-button {
|
||||
}
|
||||
|
||||
button.module-image__border-overlay:focus-visible {
|
||||
box-shadow: inset 0px 0px 0px 2px variables.$color-ultramarine;
|
||||
@include mixins.axo-focus-ring-inset;
|
||||
}
|
||||
|
||||
.module-image__border-overlay--dark {
|
||||
|
||||
@@ -257,8 +257,6 @@ export namespace AxoTooltip {
|
||||
keyboardShortcut?: string | null;
|
||||
/** Forces the tooltip to stay open when the trigger is clicked. */
|
||||
keepOpenOnActivation?: boolean;
|
||||
/** Forces the tooltip to stay closed when calling `.focus()` (ex: restoring focus on menu close) */
|
||||
onlyShowOnFocusForUserInputDeviceEvents?: boolean;
|
||||
}>;
|
||||
|
||||
export type RootProps = RootConfigProps &
|
||||
@@ -303,7 +301,6 @@ export namespace AxoTooltip {
|
||||
keyboardShortcut,
|
||||
experimentalTimestamp,
|
||||
keepOpenOnActivation,
|
||||
onlyShowOnFocusForUserInputDeviceEvents,
|
||||
} = props;
|
||||
const direction = useDirection();
|
||||
const collisionBoundary = useContext(CollisionBoundaryContext);
|
||||
@@ -371,19 +368,14 @@ export namespace AxoTooltip {
|
||||
[keepOpenOnActivation]
|
||||
);
|
||||
|
||||
const handleFocus = useCallback(
|
||||
(event: FocusEvent) => {
|
||||
if (onlyShowOnFocusForUserInputDeviceEvents) {
|
||||
// Only show the tooltip if the focus event was fired by an input device, not `.focus()`
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/sourceCapabilities
|
||||
// @ts-expect-error not available in type defs
|
||||
if (event.nativeEvent.sourceCapabilities == null) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
[onlyShowOnFocusForUserInputDeviceEvents]
|
||||
);
|
||||
const handleFocus = useCallback((event: FocusEvent) => {
|
||||
// Only show the tooltip if the focus event was fired by an input device, not `.focus()`
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/sourceCapabilities
|
||||
// @ts-expect-error not available in type defs
|
||||
if (event.nativeEvent.sourceCapabilities == null) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.disabled) {
|
||||
|
||||
@@ -95,7 +95,7 @@ import { AxoTooltip } from '../axo/AxoTooltip.dom.tsx';
|
||||
import { tw } from '../axo/tw.dom.tsx';
|
||||
import type { Emoji } from '../axo/emoji.std.ts';
|
||||
import { RecoveryKeyPasteWarning } from './RecoveryKeyPasteWarning.dom.tsx';
|
||||
import { mergeProps, useFocusRing } from 'react-aria';
|
||||
import { mergeProps, useFocusVisible, useFocusWithin } from 'react-aria';
|
||||
|
||||
const log = createLogger('CompositionInput');
|
||||
|
||||
@@ -115,6 +115,21 @@ Quill.register(
|
||||
true
|
||||
);
|
||||
|
||||
function usePreservedFocusVisibleWithin() {
|
||||
const { isFocusVisible } = useFocusVisible({
|
||||
isTextInput: true,
|
||||
});
|
||||
|
||||
const [isFocusWithin, setFocusWithin] = useState(false);
|
||||
const { focusWithinProps: focusVisibleWithinProps } = useFocusWithin({
|
||||
onFocusWithinChange: setFocusWithin,
|
||||
});
|
||||
|
||||
const isFocusVisibleWithin = isFocusWithin && isFocusVisible;
|
||||
|
||||
return { isFocusVisibleWithin, focusVisibleWithinProps };
|
||||
}
|
||||
|
||||
export type InputApi = {
|
||||
focus: () => void;
|
||||
hasFocus: () => boolean;
|
||||
@@ -237,10 +252,8 @@ export function CompositionInput(props: Props): ReactElement {
|
||||
|
||||
const [isMouseDown, setIsMouseDown] = useState<boolean>(false);
|
||||
|
||||
const { isFocusVisible, focusProps } = useFocusRing({
|
||||
isTextInput: true,
|
||||
within: true,
|
||||
});
|
||||
const { isFocusVisibleWithin, focusVisibleWithinProps } =
|
||||
usePreservedFocusVisibleWithin();
|
||||
|
||||
const generateDelta = (
|
||||
text: string,
|
||||
@@ -1047,8 +1060,8 @@ export function CompositionInput(props: Props): ReactElement {
|
||||
ref={ref}
|
||||
data-testid="CompositionInput"
|
||||
data-enabled={isInputEnabled ? 'true' : 'false'}
|
||||
data-focus-visible={isFocusVisible}
|
||||
{...mergeProps(focusProps, { onMouseDown })}
|
||||
data-focus-visible={isFocusVisibleWithin}
|
||||
{...mergeProps(focusVisibleWithinProps, { onMouseDown })}
|
||||
>
|
||||
{onRecoveryKeyPasteConfirm ? (
|
||||
<RecoveryKeyPasteWarning
|
||||
|
||||
@@ -43,6 +43,9 @@ function getMockMessageProps(): MessagePropsType {
|
||||
isPinned: false,
|
||||
isSignalConversation: false,
|
||||
isSMS: false,
|
||||
isTargeted: false,
|
||||
isTargetedCounter: null,
|
||||
isTargetedSource: null,
|
||||
isVoiceMessagePlayed: false,
|
||||
previews: [],
|
||||
timestamp: 0,
|
||||
|
||||
@@ -700,6 +700,9 @@ function ReplyOrReactionMessage({
|
||||
showContactModal={showContactModal}
|
||||
showSpoiler={showSpoiler}
|
||||
text={reply.body}
|
||||
isTargeted={false}
|
||||
isTargetedCounter={null}
|
||||
isTargetedSource={null}
|
||||
textDirection={TextDirection.Default}
|
||||
timestamp={reply.timestamp}
|
||||
/>
|
||||
|
||||
@@ -42,7 +42,7 @@ const defaultProps: Props = {
|
||||
isSelectMode: false,
|
||||
isSelected: false,
|
||||
renderItem,
|
||||
targetedMessage: undefined,
|
||||
targetedMessage: null,
|
||||
toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'),
|
||||
toggleSelectMessage: action('toggleSelectMessage'),
|
||||
};
|
||||
@@ -83,6 +83,7 @@ export function AutoexpandIfTargeted(): JSX.Element {
|
||||
targetedMessage: {
|
||||
id: 'id1',
|
||||
counter: 1,
|
||||
source: null,
|
||||
},
|
||||
};
|
||||
return <CollapseSetViewer {...props} />;
|
||||
|
||||
@@ -35,7 +35,7 @@ export type Props = CollapseSet & {
|
||||
isSelectMode: boolean;
|
||||
isSelected: boolean;
|
||||
renderItem: (props: RenderItemProps) => JSX.Element;
|
||||
targetedMessage: TargetedMessageType | undefined;
|
||||
targetedMessage: TargetedMessageType | null;
|
||||
toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void;
|
||||
toggleSelectMessage: (
|
||||
conversationId: string,
|
||||
|
||||
@@ -38,7 +38,6 @@ const MAX_GIF_TIME = 8;
|
||||
export type Props = {
|
||||
readonly attachment: AttachmentForUIType;
|
||||
readonly size?: number;
|
||||
readonly tabIndex: number;
|
||||
// test-only, to force reduced motion experience
|
||||
readonly _forceTapToPlay?: boolean;
|
||||
|
||||
@@ -58,7 +57,6 @@ export function GIF(props: Props): JSX.Element {
|
||||
const {
|
||||
attachment,
|
||||
size,
|
||||
tabIndex,
|
||||
_forceTapToPlay,
|
||||
|
||||
i18n,
|
||||
@@ -264,7 +262,6 @@ export function GIF(props: Props): JSX.Element {
|
||||
i18n,
|
||||
cancelDownloadClick,
|
||||
cancelDownloadKeyDown,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
let overlay: JSX.Element | undefined;
|
||||
@@ -285,7 +282,6 @@ export function GIF(props: Props): JSX.Element {
|
||||
aria-label={i18n('icu:GIF--download')}
|
||||
onClick={onOverlayClick}
|
||||
onKeyDown={onOverlayKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<span />
|
||||
</button>
|
||||
@@ -297,7 +293,6 @@ export function GIF(props: Props): JSX.Element {
|
||||
className="module-image__overlay-circle module-image__overlay-circle--undownloadable"
|
||||
aria-label={i18n('icu:mediaNotAvailable')}
|
||||
onClick={undownloadableClick}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<div className="module-image__undownloadable-icon" />
|
||||
</button>
|
||||
|
||||
@@ -47,7 +47,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
onError: action('onError'),
|
||||
overlayText: overrideProps.overlayText || '',
|
||||
playIconOverlay: overrideProps.playIconOverlay || false,
|
||||
tabIndex: overrideProps.tabIndex || 0,
|
||||
theme: overrideProps.theme || ('light' as ThemeType),
|
||||
url: 'url' in overrideProps ? overrideProps.url || '' : pngUrl,
|
||||
width: overrideProps.width || 300,
|
||||
|
||||
@@ -33,7 +33,6 @@ export type Props = {
|
||||
width?: number;
|
||||
cropWidth?: number;
|
||||
cropHeight?: number;
|
||||
tabIndex?: number;
|
||||
|
||||
overlayText?: string;
|
||||
|
||||
@@ -86,7 +85,6 @@ export function Image({
|
||||
onError,
|
||||
overlayText,
|
||||
playIconOverlay,
|
||||
tabIndex,
|
||||
theme,
|
||||
url,
|
||||
width = 0,
|
||||
@@ -194,7 +192,6 @@ export function Image({
|
||||
onClick={startDownloadClick}
|
||||
onKeyDown={startDownloadKeyDown}
|
||||
style={curveStyles}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<span className="module-image__overlay-circle">
|
||||
<span className="module-image__download-icon" />
|
||||
@@ -212,7 +209,6 @@ export function Image({
|
||||
className="module-image__overlay-circle module-image__overlay-circle--undownloadable"
|
||||
aria-label={i18n('icu:mediaNotAvailable')}
|
||||
onClick={undownloadableClick}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<div className="module-image__undownloadable-icon" />
|
||||
</button>
|
||||
@@ -229,7 +225,6 @@ export function Image({
|
||||
i18n,
|
||||
cancelDownloadClick,
|
||||
cancelDownloadKeyDown,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -301,7 +296,6 @@ export function Image({
|
||||
style={curveStyles}
|
||||
onClick={showVisualAttachmentClick}
|
||||
onKeyDown={showVisualAttachmentKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
/>
|
||||
) : null}
|
||||
{closeButton ? (
|
||||
@@ -329,13 +323,11 @@ export function getSpinner({
|
||||
cancelDownloadClick,
|
||||
cancelDownloadKeyDown,
|
||||
i18n,
|
||||
tabIndex,
|
||||
}: {
|
||||
attachment: AttachmentForUIType;
|
||||
cancelDownloadClick: (event: MouseEvent) => void;
|
||||
cancelDownloadKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;
|
||||
i18n: LocalizerType;
|
||||
tabIndex: number | undefined;
|
||||
}): JSX.Element | undefined {
|
||||
if (!attachment.pending) {
|
||||
return undefined;
|
||||
@@ -354,7 +346,6 @@ export function getSpinner({
|
||||
aria-label={i18n('icu:cancelDownload')}
|
||||
onClick={cancelDownloadClick}
|
||||
onKeyDown={cancelDownloadKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<div className="module-image__stop-icon" />
|
||||
<div className="module-image__progress-circle-wrapper">
|
||||
|
||||
@@ -53,7 +53,6 @@ export default {
|
||||
cancelDownload: action('cancelDownload'),
|
||||
onError: action('onError'),
|
||||
stickerSize: 0,
|
||||
tabIndex: 0,
|
||||
withContentAbove: false,
|
||||
withContentBelow: false,
|
||||
},
|
||||
|
||||
@@ -39,7 +39,6 @@ export type Props = {
|
||||
shouldCollapseAbove?: boolean;
|
||||
shouldCollapseBelow?: boolean;
|
||||
stickerSize?: number;
|
||||
tabIndex?: number;
|
||||
withContentAbove?: boolean;
|
||||
withContentBelow?: boolean;
|
||||
|
||||
@@ -125,7 +124,6 @@ export function ImageGrid({
|
||||
startDownload,
|
||||
shouldCollapseAbove,
|
||||
shouldCollapseBelow,
|
||||
tabIndex,
|
||||
theme,
|
||||
withContentAbove,
|
||||
withContentBelow,
|
||||
@@ -229,7 +227,6 @@ export function ImageGrid({
|
||||
height={height}
|
||||
width={width}
|
||||
url={getUrl(attachment) ?? attachment.thumbnailFromBackup?.url}
|
||||
tabIndex={tabIndex}
|
||||
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
|
||||
showVisualAttachment={showAttachmentOrNoLongerAvailableToast(0)}
|
||||
cancelDownload={cancelDownload}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { ReactNode, JSX, MouseEvent, KeyboardEvent } from 'react';
|
||||
import { TargetedMessageSource } from '../../state/ducks/conversationsEnums.std.ts';
|
||||
|
||||
export type Props = {
|
||||
id: string;
|
||||
@@ -12,7 +13,11 @@ export type Props = {
|
||||
isTargeted: boolean;
|
||||
isSelectMode: boolean;
|
||||
isSelected: boolean;
|
||||
targetMessage: (messageId: string, conversationId: string) => unknown;
|
||||
targetMessage: (
|
||||
messageId: string,
|
||||
conversationId: string,
|
||||
targetedMessageSource: TargetedMessageSource
|
||||
) => unknown;
|
||||
toggleSelectMessage: (
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
@@ -44,7 +49,7 @@ export function InlineNotificationWrapper({
|
||||
}, [isTargeted]);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
targetMessage(id, conversationId);
|
||||
targetMessage(id, conversationId, TargetedMessageSource.Focus);
|
||||
}, [id, conversationId, targetMessage]);
|
||||
|
||||
if (isSelectMode) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -143,7 +143,7 @@ export function MessageContextMenu({
|
||||
</AxoMenuBuilder.Item>
|
||||
)}
|
||||
{onMoreInfo && (
|
||||
<AxoMenuBuilder.Item symbol="info" onSelect={onMoreInfo}>
|
||||
<AxoMenuBuilder.Item symbol="info-circle" onSelect={onMoreInfo}>
|
||||
{i18n('icu:MessageContextMenu__info')}
|
||||
</AxoMenuBuilder.Item>
|
||||
)}
|
||||
|
||||
@@ -41,6 +41,9 @@ const defaultMessage: MessageDataPropsType = {
|
||||
isSignalConversation: false,
|
||||
isSMS: false,
|
||||
isSpoilerExpanded: {},
|
||||
isTargeted: false,
|
||||
isTargetedCounter: null,
|
||||
isTargetedSource: null,
|
||||
isVoiceMessagePlayed: false,
|
||||
previews: [],
|
||||
readStatus: ReadStatus.Read,
|
||||
|
||||
@@ -113,6 +113,9 @@ const defaultMessageProps: TimelineMessagesProps = {
|
||||
isSignalConversation: false,
|
||||
isSMS: false,
|
||||
isSpoilerExpanded: {},
|
||||
isTargeted: false,
|
||||
isTargetedCounter: null,
|
||||
isTargetedSource: null,
|
||||
isVoiceMessagePlayed: false,
|
||||
handleDebugMessage: action('debugMessage'),
|
||||
toggleSelectMessage: action('toggleSelectMessage'),
|
||||
|
||||
@@ -64,6 +64,9 @@ function mockMessageTimelineItem(
|
||||
isSignalConversation: false,
|
||||
isSMS: false,
|
||||
isSpoilerExpanded: {},
|
||||
isTargeted: false,
|
||||
isTargetedCounter: null,
|
||||
isTargetedSource: null,
|
||||
isVoiceMessagePlayed: false,
|
||||
previews: [],
|
||||
readStatus: ReadStatus.Read,
|
||||
@@ -385,7 +388,7 @@ const renderItem = ({
|
||||
shouldCollapseBelow={false}
|
||||
shouldHideMetadata={false}
|
||||
shouldRenderDateHeader={false}
|
||||
targetedMessage={undefined}
|
||||
targetedMessage={null}
|
||||
{...actions()}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -46,6 +46,7 @@ import { MessageInteractivity } from './Message.dom.tsx';
|
||||
import type { RenderItemProps } from '../../state/smart/TimelineItem.preload.tsx';
|
||||
import type { CollapseSet } from '../../util/CollapseSet.std.ts';
|
||||
import { tw } from '../../axo/tw.dom.tsx';
|
||||
import { TargetedMessageSource } from '../../state/ducks/conversationsEnums.std.ts';
|
||||
|
||||
const { first, get, isNumber, last, throttle } = lodash;
|
||||
|
||||
@@ -126,7 +127,11 @@ export type PropsActionsType = {
|
||||
) => unknown;
|
||||
markMessageRead: (conversationId: string, messageId: string) => unknown;
|
||||
maybePeekGroupCall: (conversationId: string) => unknown;
|
||||
targetMessage: (messageId: string, conversationId: string) => unknown;
|
||||
targetMessage: (
|
||||
messageId: string,
|
||||
conversationId: string,
|
||||
targetedMessageSource: TargetedMessageSource
|
||||
) => unknown;
|
||||
setCenterMessage: (
|
||||
conversationId: string,
|
||||
messageId: string | undefined
|
||||
@@ -258,7 +263,7 @@ export class Timeline extends Component<PropsType, StateType, SnapshotType> {
|
||||
const lastIndex = items.length - 1;
|
||||
const lastItem = items[lastIndex];
|
||||
strictAssert(lastItem, 'Missing lastItem');
|
||||
targetMessage(lastItem.id, id);
|
||||
targetMessage(lastItem.id, id, TargetedMessageSource.Focus);
|
||||
} else {
|
||||
const containerEl = this.#containerRef.current;
|
||||
if (containerEl) {
|
||||
@@ -319,7 +324,7 @@ export class Timeline extends Component<PropsType, StateType, SnapshotType> {
|
||||
if (setFocus) {
|
||||
const item = items[oldestUnseenIndex];
|
||||
strictAssert(item, 'Missing item at oldestUnseenIndex');
|
||||
targetMessage(item.id, id);
|
||||
targetMessage(item.id, id, TargetedMessageSource.Focus);
|
||||
} else {
|
||||
lastSeenElement.scrollIntoView();
|
||||
}
|
||||
@@ -941,7 +946,11 @@ export class Timeline extends Component<PropsType, StateType, SnapshotType> {
|
||||
targetInnerMessage,
|
||||
'No message at targetIndex in items.messages'
|
||||
);
|
||||
targetMessage(targetInnerMessage.id, id);
|
||||
targetMessage(
|
||||
targetInnerMessage.id,
|
||||
id,
|
||||
TargetedMessageSource.NavigateToMessage
|
||||
);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -958,7 +967,7 @@ export class Timeline extends Component<PropsType, StateType, SnapshotType> {
|
||||
const targetItem = items[targetIndex];
|
||||
strictAssert(targetItem, 'Missing item at targetIndex');
|
||||
if (targetItem.type === 'none') {
|
||||
targetMessage(targetItem.id, id);
|
||||
targetMessage(targetItem.id, id, TargetedMessageSource.Focus);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -971,7 +980,7 @@ export class Timeline extends Component<PropsType, StateType, SnapshotType> {
|
||||
: first(targetItem.messages);
|
||||
|
||||
strictAssert(targetInnerMessage, 'Expect to get first/last of target');
|
||||
targetMessage(targetInnerMessage.id, id);
|
||||
targetMessage(targetInnerMessage.id, id, TargetedMessageSource.Focus);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -1047,7 +1056,7 @@ export class Timeline extends Component<PropsType, StateType, SnapshotType> {
|
||||
|
||||
if (direction === -1) {
|
||||
if (currentTop <= targetTop || index === 0) {
|
||||
targetMessage(currentMessageId, id);
|
||||
targetMessage(currentMessageId, id, TargetedMessageSource.Focus);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -1057,7 +1066,7 @@ export class Timeline extends Component<PropsType, StateType, SnapshotType> {
|
||||
const currentBottom = currentTop + currentRect.height;
|
||||
|
||||
if (currentBottom > targetBottom || index === max - 1) {
|
||||
targetMessage(currentMessageId, id);
|
||||
targetMessage(currentMessageId, id, TargetedMessageSource.Focus);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -1071,7 +1080,7 @@ export class Timeline extends Component<PropsType, StateType, SnapshotType> {
|
||||
if (event.key === 'Home' || (commandOrCtrl && event.key === 'ArrowUp')) {
|
||||
const firstMessageId = first(items);
|
||||
if (firstMessageId) {
|
||||
targetMessage(firstMessageId.id, id);
|
||||
targetMessage(firstMessageId.id, id, TargetedMessageSource.Focus);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ const getDefaultProps = () => ({
|
||||
isGroup: false,
|
||||
interactivity: MessageInteractivity.Normal,
|
||||
interactionMode: 'keyboard' as const,
|
||||
targetedMessage: undefined,
|
||||
targetedMessage: null,
|
||||
theme: ThemeType.light,
|
||||
platform: 'darwin',
|
||||
handleDebugMessage: action('handleDebugMessage'),
|
||||
|
||||
@@ -74,6 +74,7 @@ import type { RenderItemProps } from '../../state/smart/TimelineItem.preload.tsx
|
||||
import type { CollapseSet } from '../../util/CollapseSet.std.ts';
|
||||
import { CollapseSetViewer } from './CollapseSet.dom.tsx';
|
||||
import type { TargetedMessageType } from '../../state/selectors/conversations.dom.ts';
|
||||
import type { TargetedMessageSource } from '../../state/ducks/conversationsEnums.std.ts';
|
||||
|
||||
type CallHistoryType = {
|
||||
type: 'callHistory';
|
||||
@@ -224,7 +225,11 @@ type PropsLocalType = {
|
||||
pollTimestamp: number,
|
||||
conversationId: string
|
||||
) => unknown;
|
||||
targetMessage: (messageId: string, conversationId: string) => unknown;
|
||||
targetMessage: (
|
||||
messageId: string,
|
||||
conversationId: string,
|
||||
targetedMessageSource: TargetedMessageSource
|
||||
) => void;
|
||||
toggleSelectMessage: (
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
@@ -239,7 +244,7 @@ type PropsLocalType = {
|
||||
renderUniversalTimerNotification: () => JSX.Element;
|
||||
renderItem: (props: RenderItemProps) => JSX.Element;
|
||||
i18n: LocalizerType;
|
||||
targetedMessage: TargetedMessageType | undefined;
|
||||
targetedMessage: TargetedMessageType | null;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
|
||||
@@ -304,6 +304,9 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
isTapToView: overrideProps.isTapToView,
|
||||
isTapToViewError: overrideProps.isTapToViewError,
|
||||
isTapToViewExpired: overrideProps.isTapToViewExpired,
|
||||
isTargeted: false,
|
||||
isTargetedCounter: null,
|
||||
isTargetedSource: null,
|
||||
isVoiceMessagePlayed: false,
|
||||
cancelAttachmentDownload: action('cancelAttachmentDownload'),
|
||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||
|
||||
@@ -55,7 +55,7 @@ export type PropsData = {
|
||||
canReply: boolean;
|
||||
canPinMessage: boolean;
|
||||
selectedReaction?: Emoji.Variant;
|
||||
isTargeted?: boolean;
|
||||
isTargeted: boolean;
|
||||
isSignalConversation: boolean;
|
||||
} & Omit<MessagePropsData, 'renderingContext' | 'menu'>;
|
||||
|
||||
|
||||
@@ -232,6 +232,9 @@ export function GroupMemberLabelEditor({
|
||||
isSelectMode={false}
|
||||
isSignalConversation={false}
|
||||
isSMS={false}
|
||||
isTargeted={false}
|
||||
isTargetedCounter={null}
|
||||
isTargetedSource={null}
|
||||
isVoiceMessagePlayed={false}
|
||||
direction="incoming"
|
||||
// oxlint-disable-next-line react/purity
|
||||
|
||||
@@ -239,10 +239,6 @@ function FilterMenu(props: FilterMenuProps): ReactNode {
|
||||
variant="implied-secondary"
|
||||
label={i18n('icu:GroupMembersSearchDialog__FilterMenu__Label')}
|
||||
pressed={props.filter !== GroupMembersSearchDialogFilter.All}
|
||||
tooltip={{
|
||||
label: i18n('icu:GroupMembersSearchDialog__FilterMenu__Label'),
|
||||
onlyShowOnFocusForUserInputDeviceEvents: true,
|
||||
}}
|
||||
/>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
<AxoDropdownMenu.Content>
|
||||
|
||||
@@ -421,7 +421,7 @@ export function addGlobalKeyboardShortcuts(): void {
|
||||
|
||||
window.reduxActions.composer.setQuoteByMessageId(
|
||||
conversation.id,
|
||||
quote ? undefined : targetedMessage
|
||||
quote ? undefined : (targetedMessage ?? undefined)
|
||||
);
|
||||
|
||||
return;
|
||||
|
||||
@@ -634,9 +634,9 @@ export type ConversationsStateType = ReadonlyDeep<{
|
||||
conversationsByGroupId: ConversationLookupType;
|
||||
conversationsByUsername: ConversationLookupType;
|
||||
|
||||
targetedMessage: string | undefined;
|
||||
targetedMessage: string | null;
|
||||
targetedMessageCounter: number;
|
||||
targetedMessageSource: TargetedMessageSource | undefined;
|
||||
targetedMessageSource: TargetedMessageSource | null;
|
||||
|
||||
lastSelectedMessage: MessageTimestamps | undefined;
|
||||
selectedMessageIds: ReadonlyArray<string> | undefined;
|
||||
@@ -831,6 +831,7 @@ export type MessageTargetedActionType = ReadonlyDeep<{
|
||||
type: 'MESSAGE_TARGETED';
|
||||
payload: {
|
||||
messageId: string;
|
||||
targetedMessageSource: TargetedMessageSource;
|
||||
};
|
||||
}>;
|
||||
export type ToggleSelectMessagesActionType = ReadonlyDeep<{
|
||||
@@ -3149,7 +3150,8 @@ function removeAllConversations(): RemoveAllConversationsActionType {
|
||||
|
||||
function targetMessage(
|
||||
messageId: string,
|
||||
conversationId: string
|
||||
conversationId: string,
|
||||
targetedMessageSource: TargetedMessageSource
|
||||
): ThunkAction<void, RootStateType, unknown, MessageTargetedActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
const selectedConversationId = getSelectedConversationId(getState());
|
||||
@@ -3165,6 +3167,7 @@ function targetMessage(
|
||||
type: 'MESSAGE_TARGETED',
|
||||
payload: {
|
||||
messageId,
|
||||
targetedMessageSource,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -4895,7 +4898,7 @@ function showConversation({
|
||||
function onConversationOpened(
|
||||
conversationId: string,
|
||||
messageId: string | undefined,
|
||||
targetedMessageSource: TargetedMessageSource | undefined
|
||||
targetedMessageSource: TargetedMessageSource | null
|
||||
): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
@@ -5267,9 +5270,9 @@ export function getEmptyState(): ConversationsStateType {
|
||||
lastCenterMessageByConversation: {},
|
||||
messagesByConversation: {},
|
||||
messagesLookup: {},
|
||||
targetedMessage: undefined,
|
||||
targetedMessage: null,
|
||||
targetedMessageCounter: 0,
|
||||
targetedMessageSource: undefined,
|
||||
targetedMessageSource: null,
|
||||
lastSelectedMessage: undefined,
|
||||
selectedMessageIds: undefined,
|
||||
showArchived: false,
|
||||
@@ -6067,7 +6070,7 @@ export function reducer(
|
||||
...state,
|
||||
targetedMessage: messageId,
|
||||
targetedMessageCounter: state.targetedMessageCounter + 1,
|
||||
targetedMessageSource: TargetedMessageSource.Focus,
|
||||
targetedMessageSource: action.payload.targetedMessageSource,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6830,9 +6833,9 @@ export function reducer(
|
||||
if (action.type === 'CLEAR_TARGETED_MESSAGE') {
|
||||
return {
|
||||
...state,
|
||||
targetedMessage: undefined,
|
||||
targetedMessage: null,
|
||||
targetedMessageCounter: 0,
|
||||
targetedMessageSource: undefined,
|
||||
targetedMessageSource: null,
|
||||
};
|
||||
}
|
||||
if (action.type === 'CLEAR_UNREAD_METRICS') {
|
||||
@@ -6871,7 +6874,7 @@ export function reducer(
|
||||
? state.preloadData
|
||||
: undefined,
|
||||
hasContactSpoofingReview: false,
|
||||
targetedMessage: messageId,
|
||||
targetedMessage: messageId ?? null,
|
||||
targetedMessageSource: messageId
|
||||
? TargetedMessageSource.NavigateToMessage
|
||||
: TargetedMessageSource.Reset,
|
||||
|
||||
@@ -200,23 +200,25 @@ export const getSafeConversationWithSameTitle = createSelector(
|
||||
export type TargetedMessageType = {
|
||||
id: string;
|
||||
counter: number;
|
||||
source: TargetedMessageSource | null;
|
||||
};
|
||||
export const getTargetedMessage = createSelector(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): TargetedMessageType | undefined => {
|
||||
(state: ConversationsStateType): TargetedMessageType | null => {
|
||||
if (!state.targetedMessage) {
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: state.targetedMessage,
|
||||
counter: state.targetedMessageCounter,
|
||||
source: state.targetedMessageSource,
|
||||
};
|
||||
}
|
||||
);
|
||||
export const getTargetedMessageSource = createSelector(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): TargetedMessageSource | undefined => {
|
||||
(state: ConversationsStateType): TargetedMessageSource | null => {
|
||||
return state.targetedMessageSource;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -185,6 +185,7 @@ import type { PinnedMessageNotificationData } from '../../components/conversatio
|
||||
import type { PollTerminateNotificationDataType } from '../../components/conversation/PollTerminateNotification.dom.tsx';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
import { isDownloadableOrBackfillable } from '../../util/downloadAttachment.preload.ts';
|
||||
import type { TargetedMessageSource } from '../ducks/conversationsEnums.std.ts';
|
||||
|
||||
const { groupBy, isEmpty, isNumber, isObject, map } = lodash;
|
||||
|
||||
@@ -214,8 +215,9 @@ export type GetPropsForBubbleOptions = Readonly<{
|
||||
ourNumber?: string;
|
||||
ourAci: AciString | undefined;
|
||||
ourPni: PniString | undefined;
|
||||
targetedMessageId?: string;
|
||||
targetedMessageCounter?: number;
|
||||
targetedMessageId: string | null;
|
||||
targetedMessageCounter: number | null;
|
||||
targetedMessageSource: TargetedMessageSource | null;
|
||||
pinnedMessagesMessageIds: ReadonlyArray<string> | null;
|
||||
selectedMessageIds: ReadonlyArray<string> | undefined;
|
||||
regionCode?: string;
|
||||
@@ -818,6 +820,7 @@ export type GetPropsForMessageOptions = Pick<
|
||||
| 'ourNumber'
|
||||
| 'targetedMessageId'
|
||||
| 'targetedMessageCounter'
|
||||
| 'targetedMessageSource'
|
||||
| 'pinnedMessagesMessageIds'
|
||||
| 'selectedMessageIds'
|
||||
| 'regionCode'
|
||||
@@ -925,6 +928,7 @@ const getPropsForMessage = (
|
||||
regionCode,
|
||||
targetedMessageId,
|
||||
targetedMessageCounter,
|
||||
targetedMessageSource,
|
||||
pinnedMessagesMessageIds,
|
||||
selectedMessageIds,
|
||||
contactNameColors,
|
||||
@@ -1056,7 +1060,8 @@ const getPropsForMessage = (
|
||||
isSignalConversation: isSignalConversation(author),
|
||||
isSticker: Boolean(sticker),
|
||||
isTargeted,
|
||||
isTargetedCounter: isTargeted ? targetedMessageCounter : undefined,
|
||||
isTargetedCounter: isTargeted ? targetedMessageCounter : null,
|
||||
isTargetedSource: isTargeted ? targetedMessageSource : null,
|
||||
isTapToView: isMessageTapToView,
|
||||
isTapToViewError:
|
||||
isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid,
|
||||
@@ -1121,8 +1126,9 @@ export const getMessagePropsSelector = createSelector(
|
||||
ourAci,
|
||||
ourPni,
|
||||
regionCode,
|
||||
targetedMessageCounter: targetedMessage?.counter,
|
||||
targetedMessageId: targetedMessage?.id,
|
||||
targetedMessageCounter: targetedMessage?.counter ?? null,
|
||||
targetedMessageId: targetedMessage?.id ?? null,
|
||||
targetedMessageSource: targetedMessage?.source ?? null,
|
||||
pinnedMessagesMessageIds,
|
||||
selectedMessageIds,
|
||||
defaultConversationColor,
|
||||
@@ -2992,6 +2998,9 @@ export const getMessageDetailsSelector = createSelector(
|
||||
regionCode,
|
||||
pinnedMessagesMessageIds,
|
||||
selectedMessageIds,
|
||||
targetedMessageId: null,
|
||||
targetedMessageCounter: null,
|
||||
targetedMessageSource: null,
|
||||
defaultConversationColor,
|
||||
hasMediaBackups,
|
||||
getStoryReplyAttachment,
|
||||
|
||||
@@ -95,8 +95,9 @@ const getTimelineItem = (
|
||||
ourAci,
|
||||
ourPni,
|
||||
regionCode,
|
||||
targetedMessageId: targetedMessage?.id,
|
||||
targetedMessageCounter: targetedMessage?.counter,
|
||||
targetedMessageId: targetedMessage?.id ?? null,
|
||||
targetedMessageCounter: targetedMessage?.counter ?? null,
|
||||
targetedMessageSource: targetedMessage?.source ?? null,
|
||||
contactNameColors,
|
||||
callSelector,
|
||||
callHistorySelector,
|
||||
|
||||
@@ -441,7 +441,7 @@ export function getNotificationDataForMessage(
|
||||
callHistorySelector: getCallHistorySelector(state),
|
||||
conversationSelector: getConversationSelector(state),
|
||||
selectedMessageIds: getSelectedMessageIds(state),
|
||||
targetedMessageId: getTargetedMessage(state)?.id,
|
||||
targetedMessageId: getTargetedMessage(state)?.id ?? null,
|
||||
});
|
||||
if (callingNotification) {
|
||||
const text = getCallingNotificationText(callingNotification, i18n);
|
||||
|
||||
Reference in New Issue
Block a user