Fix more focus behavior

Co-authored-by: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
automated-signal
2026-09-12 02:36:09 +00:00
committed by GitHub
co-authored by Jamie
parent 0e1cf8e73d
commit 377378ac91
31 changed files with 330 additions and 329 deletions
+10 -20
View File
@@ -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 {
+8 -16
View File
@@ -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) {
+20 -7
View File
@@ -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,
-5
View File
@@ -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,
-9
View File
@@ -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()}
/>
);
+18 -9
View File
@@ -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;
+13 -10
View File
@@ -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,
+5 -3
View File
@@ -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;
}
);
+14 -5
View File
@@ -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,
+3 -2
View File
@@ -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);