Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6cd66e0
fix: tab indexes and reply to failed messages
Aerilym Feb 23, 2026
790928d
fix: add main screen focus state
Aerilym Feb 24, 2026
0483e3b
feat: add keyboard navigation to file attachments
Aerilym Feb 24, 2026
60f9501
fix: emoji reaction list rendering and emoji reaction bar focus
Aerilym Feb 24, 2026
8a87b9c
chore: made all of the message types selectable
Bilb Feb 20, 2026
41c168c
Merge remote-tracking branch 'upstream/dev' into delete-msg-options-u…
Bilb Feb 24, 2026
a813a6e
fix: emoji panel focus trap
Aerilym Feb 26, 2026
b110f97
chore: remove event focusin log
Aerilym Feb 26, 2026
41aa2c6
fix: only show reply in message info when available
Aerilym Feb 26, 2026
68e9739
chore: refactor message architecture as all are selectable
Bilb Feb 26, 2026
de81b32
chore: move message interaction logic to outer message container
Aerilym Feb 27, 2026
780661e
chore: merge delete-msg-options-updated into fix/tab_indexes
Aerilym Feb 27, 2026
a4eb7fd
fix: move popovers to interactable component
Aerilym Feb 27, 2026
1729fb0
fix: split message component into interactable parent
Aerilym Feb 27, 2026
d02b0d7
fix: track message deleted state as int instead of bool
Bilb Feb 27, 2026
b410b42
fix: standardise message deletion logic
Bilb Mar 2, 2026
1463f68
feat: move interactables to message list and flatten messages
Aerilym Mar 3, 2026
9fa5724
fix: multi select mode event propagation and message list padding
Aerilym Mar 3, 2026
3dca2c6
fix: message info view padding
Aerilym Mar 3, 2026
bee80e0
fix: conversation header button spacing
Aerilym Mar 3, 2026
25a92a0
chore: unify message padding for typing indicator
Aerilym Mar 3, 2026
3cd2183
Merge pull request #1872 from session-foundation/chore/flatten_messag…
Bilb Mar 3, 2026
83e8f15
fix: clean up mark as deleted logic & quote
Bilb Mar 3, 2026
fe8d0b0
Merge remote-tracking branch 'upstream/delete-msg-options-updated' in…
Bilb Mar 3, 2026
9b94899
fix: useMessageIsDeleted is not returning a bool anymore
Bilb Mar 3, 2026
ca67f03
Merge remote-tracking branch 'upstream/dev' into delete-msg-options-u…
Bilb Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ts/components/SessionToastContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Slide, ToastContainer, ToastContainerProps } from 'react-toastify';
import styled from 'styled-components';
import { isTestIntegration } from '../shared/env_vars';

// NOTE: https://styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity
const StyledToastContainer = styled(ToastContainer)`
Expand Down Expand Up @@ -40,7 +41,7 @@ export const SessionToastContainer = () => {
return (
<WrappedToastContainer
position="bottom-right"
autoClose={5000}
autoClose={isTestIntegration() ? 1000 : 5000}
hideProgressBar={true}
newestOnTop={true}
closeOnClick={true}
Expand Down
6 changes: 6 additions & 0 deletions ts/components/SessionTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
useRef,
useState,
useEffect,
type Dispatch,
} from 'react';
import styled, { type CSSProperties } from 'styled-components';
import useDebounce from 'react-use/lib/useDebounce';
Expand Down Expand Up @@ -47,6 +48,11 @@ export type PopoverTriggerPosition = {
offsetX?: number;
};

export type WithPopoverPosition = { triggerPosition: PopoverTriggerPosition | null };
export type WithSetPopoverPosition = {
setTriggerPosition: Dispatch<PopoverTriggerPosition | null>;
};

export const defaultTriggerPos: PopoverTriggerPosition = { x: 0, y: 0, width: 0, height: 0 };

export function getTriggerPositionFromBoundingClientRect(rect: DOMRect): PopoverTriggerPosition {
Expand Down
2 changes: 2 additions & 0 deletions ts/components/basic/SessionRadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type SessionRadioItems = Array<{
label: string;
inputDataTestId: SessionDataTestId;
labelDataTestId: SessionDataTestId;
disabled?: boolean;
}>;

interface Props {
Expand Down Expand Up @@ -50,6 +51,7 @@ export const SessionRadioGroup = (props: Props) => {
label={item.label}
active={itemIsActive}
value={item.value}
disabled={item.disabled}
inputDataTestId={item.inputDataTestId}
labelDataTestId={item.labelDataTestId}
inputName={group}
Expand Down
2 changes: 0 additions & 2 deletions ts/components/calling/IncomingCallDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export const IncomingCallDialog = () => {
};
}, [incomingCallFromPubkey]);

// #region input handlers
const handleAcceptIncomingCall = async () => {
if (incomingCallFromPubkey) {
await CallManager.USER_acceptIncomingCallRequest(incomingCallFromPubkey);
Expand All @@ -77,7 +76,6 @@ export const IncomingCallDialog = () => {
if (!hasIncomingCall || !incomingCallFromPubkey) {
return null;
}
// #endregion

if (hasIncomingCall) {
return (
Expand Down
4 changes: 2 additions & 2 deletions ts/components/conversation/SessionConversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,10 @@ export class SessionConversation extends Component<Props, State> {
if (msg.body.replace(/\s/g, '').includes(recoveryPhrase.replace(/\s/g, ''))) {
window.inboxStore?.dispatch(
updateConfirmModal({
title: tr('warning'),
title: { token: 'warning' },
i18nMessage: { token: 'recoveryPasswordWarningSendDescription' },
okTheme: SessionButtonColor.Danger,
okText: tr('send'),
okText: { token: 'send' },
onClickOk: () => {
void sendAndScroll();
},
Expand Down
6 changes: 3 additions & 3 deletions ts/components/conversation/SessionEmojiReactBarPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { MessageReactBar } from './message/message-content/MessageReactBar';
import { THEME_GLOBALS } from '../../themes/globals';
import { SessionEmojiPanelPopover } from './SessionEmojiPanelPopover';
import { closeContextMenus } from '../../util/contextMenu';
import { useMessageInteractions } from '../../hooks/useMessageInteractions';
import { useFocusedMessageId } from '../../state/selectors/conversations';
import { useReactToMessage } from '../../hooks/useMessageInteractions';

export function SessionEmojiReactBarPopover({
messageId,
Expand All @@ -26,7 +26,7 @@ export function SessionEmojiReactBarPopover({
const emojiPanelRef = useRef<HTMLDivElement>(null);
const emojiReactionBarRef = useRef<HTMLDivElement>(null);
const [showEmojiPanel, setShowEmojiPanel] = useState<boolean>(false);
const { reactToMessage } = useMessageInteractions(messageId);
const reactToMessage = useReactToMessage(messageId);
const focusedMessageId = useFocusedMessageId();

const closeEmojiPanel = () => {
Expand All @@ -41,7 +41,7 @@ export function SessionEmojiReactBarPopover({
const onEmojiClick = async (args: any) => {
const emoji = args.native ?? args;
closeEmojiPanel();
await reactToMessage(emoji);
await reactToMessage?.(emoji);
};

useClickAway(emojiPanelRef, () => {
Expand Down
35 changes: 7 additions & 28 deletions ts/components/conversation/SessionMessagesList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useLayoutEffect, useState, type FC } from 'react';
import { useLayoutEffect, useState } from 'react';
import { useSelector } from 'react-redux';

import useKey from 'react-use/lib/useKey';
Expand All @@ -7,43 +7,23 @@ import {
getOldTopMessageId,
getSortedMessagesTypesOfSelectedConversation,
useFocusedMessageId,
type MessagePropsType,
} from '../../state/selectors/conversations';
import { useSelectedConversationKey } from '../../state/selectors/selectedConversation';
import { MessageDateBreak } from './message/message-item/DateBreak';
import { CommunityInvitation } from './message/message-item/CommunityInvitation';
import { GroupUpdateMessage } from './message/message-item/GroupUpdateMessage';
import { Message } from './message/message-item/Message';
import { MessageRequestResponse } from './message/message-item/MessageRequestResponse';
import { CallNotification } from './message/message-item/notification-bubble/CallNotification';

import { IsDetailMessageViewContext } from '../../contexts/isDetailViewContext';
import { SessionLastSeenIndicator } from './SessionLastSeenIndicator';
import { TimerNotification } from './TimerNotification';
import { DataExtractionNotification } from './message/message-item/DataExtractionNotification';
import { InteractionNotification } from './message/message-item/InteractionNotification';
import type { WithMessageId } from '../../session/types/with';
import { useKeyboardShortcut } from '../../hooks/useKeyboardShortcut';
import { KbdShortcut } from '../../util/keyboardShortcuts';
import { useMessageInteractions } from '../../hooks/useMessageInteractions';
import { GenericReadableMessage } from './message/message-item/GenericReadableMessage';
import { useCopyText, useReply } from '../../hooks/useMessageInteractions';

function isNotTextboxEvent(e: KeyboardEvent) {
return (e?.target as any)?.type === undefined;
}

let previousRenderedConvo: string | undefined;

const componentForMessageType: Record<MessagePropsType, FC<WithMessageId>> = {
'group-notification': GroupUpdateMessage,
'group-invitation': CommunityInvitation,
'message-request-response': MessageRequestResponse,
'data-extraction': DataExtractionNotification,
'timer-notification': TimerNotification,
'call-notification': CallNotification,
'interaction-notification': InteractionNotification,
'regular-message': Message,
};

export const SessionMessagesList = (props: {
scrollAfterLoadMore: (
messageIdToScrollTo: string,
Expand All @@ -60,8 +40,9 @@ export const SessionMessagesList = (props: {
const [didScroll, setDidScroll] = useState(false);
const oldTopMessageId = useSelector(getOldTopMessageId);
const oldBottomMessageId = useSelector(getOldBottomMessageId);
const focusedMessageId = useFocusedMessageId();
const { reply, copyText } = useMessageInteractions(focusedMessageId);
const focusedMessageId = useFocusedMessageId() ?? undefined;
const reply = useReply(focusedMessageId);
const copyText = useCopyText(focusedMessageId);

useKeyboardShortcut({ shortcut: KbdShortcut.messageToggleReply, handler: reply, scopeId: 'all' });
useKeyboardShortcut({ shortcut: KbdShortcut.messageCopyText, handler: copyText, scopeId: 'all' });
Expand Down Expand Up @@ -113,8 +94,6 @@ export const SessionMessagesList = (props: {
.map(messageProps => {
const { messageId } = messageProps;

const ComponentToRender = componentForMessageType[messageProps.message.messageType];

const unreadIndicator = messageProps.showUnreadIndicator ? (
<SessionLastSeenIndicator
key={'unread-indicator'}
Expand All @@ -136,7 +115,7 @@ export const SessionMessagesList = (props: {
return [
dateBreak,
unreadIndicator,
<ComponentToRender key={messageId} messageId={messageId} />,
<GenericReadableMessage key={messageId} messageId={messageId} />,
];
})
// TODO: check if we reverse this upstream, we might be reversing twice
Expand Down
16 changes: 9 additions & 7 deletions ts/components/conversation/TimerNotification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Localizer } from '../basic/Localizer';
import { SessionButtonColor } from '../basic/SessionButton';
import { SessionIcon } from '../icon';
import { getTimerNotificationStr } from '../../models/timerNotifications';
import type { WithMessageId } from '../../session/types/with';
import type { WithContextMenuId, WithMessageId } from '../../session/types/with';
import {
useMessageAuthor,
useMessageAuthorIsUs,
Expand All @@ -34,6 +34,7 @@ import {
useMessageExpirationUpdateTimespanText,
} from '../../state/selectors';
import { tr, type TrArgs } from '../../localization/localeTools';
import type { WithPopoverPosition, WithSetPopoverPosition } from '../SessionTooltip';

const FollowSettingButton = styled.button`
color: var(--primary-color);
Expand Down Expand Up @@ -65,13 +66,11 @@ function useFollowSettingsButtonClick({ messageId }: WithMessageId) {
disappearing_messages_type: localizedMode,
};

const okText = tr('confirm');

dispatch(
updateConfirmModal({
title: tr('disappearingMessagesFollowSetting'),
title: { token: 'disappearingMessagesFollowSetting' },
i18nMessage,
okText,
okText: { token: 'confirm' },
okTheme: SessionButtonColor.Danger,
onClickOk: async () => {
if (!selectedConvoKey) {
Expand Down Expand Up @@ -149,7 +148,9 @@ const FollowSettingsButton = ({ messageId }: WithMessageId) => {
);
};

export const TimerNotification = (props: WithMessageId) => {
export const TimerNotification = (
props: WithMessageId & WithPopoverPosition & WithSetPopoverPosition & WithContextMenuId
) => {
const { messageId } = props;
const timespanSeconds = useMessageExpirationUpdateTimespanSeconds(messageId);
const expirationMode = useMessageExpirationUpdateMode(messageId);
Expand Down Expand Up @@ -177,8 +178,9 @@ export const TimerNotification = (props: WithMessageId) => {

return (
<ExpirableReadableMessage
contextMenuId={props.contextMenuId}
setTriggerPosition={props.setTriggerPosition}
messageId={messageId}
isControlMessage={true}
key={`readable-message-${messageId}`}
dataTestId={'disappear-control-message'}
>
Expand Down
6 changes: 3 additions & 3 deletions ts/components/conversation/header/ConversationHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,10 @@ function useShowRecreateModal() {
(name: string, members: Array<PubkeyType>) => {
dispatch(
updateConfirmModal({
title: tr('recreateGroup'),
title: { token: 'recreateGroup' },
i18nMessage: { token: 'legacyGroupChatHistory' },
okText: tr('theContinue'),
cancelText: tr('cancel'),
okText: { token: 'theContinue' },
cancelText: { token: 'cancel' },
okTheme: SessionButtonColor.Danger,
onClickOk: () => {
openCreateGroup(name, members);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ import { useSelector } from 'react-redux';
import useKey from 'react-use/lib/useKey';
import { getAppDispatch } from '../../../state/dispatch';

import { deleteMessagesForX } from '../../../interactions/conversations/unsendingInteractions';
import { resetSelectedMessageIds } from '../../../state/ducks/conversations';
import { getSelectedMessageIds } from '../../../state/selectors/conversations';
import {
useSelectedConversationKey,
useSelectedIsPublic,
} from '../../../state/selectors/selectedConversation';
import { useSelectedConversationKey } from '../../../state/selectors/selectedConversation';
import {
SessionButton,
SessionButtonColor,
Expand All @@ -21,11 +17,12 @@ import { tr } from '../../../localization/localeTools';
import { SessionLucideIconButton } from '../../icon/SessionIconButton';
import { LUCIDE_ICONS_UNICODE } from '../../icon/lucide';
import { isBackspace, isDeleteKey, isEscapeKey } from '../../../util/keyboardShortcuts';
import { useDeleteMessagesCb } from '../../menuAndSettingsHooks/useDeleteMessagesCb';

export const SelectionOverlay = () => {
const selectedMessageIds = useSelector(getSelectedMessageIds);
const selectedConversationKey = useSelectedConversationKey();
const isPublic = useSelectedIsPublic();
const deleteMessagesCb = useDeleteMessagesCb(selectedConversationKey);
const dispatch = getAppDispatch();
const ref = useRef(null);

Expand All @@ -51,8 +48,8 @@ export const SelectionOverlay = () => {
return true;
case 'Backspace':
case 'Delete':
if (selectionMode && selectedConversationKey) {
void deleteMessagesForX(selectedMessageIds, selectedConversationKey, isPublic);
if (selectionMode) {
void deleteMessagesCb?.(selectedMessageIds);
}
return true;
default:
Expand All @@ -61,12 +58,6 @@ export const SelectionOverlay = () => {
}
);

// `enforceDeleteServerSide` should check for message statuses too, but when we have multiple selected,
// some might be sent and some in an error state. We default to trying to delete all of them server side first,
// which might fail. If that fails, the user will need to do a delete for all the ones sent already, and a manual delete
// for each ones which is in an error state.
const enforceDeleteServerSide = isPublic;

return (
<SessionFocusTrap
initialFocus={() => ref.current}
Expand Down Expand Up @@ -96,14 +87,8 @@ export const SelectionOverlay = () => {
buttonShape={SessionButtonShape.Square}
buttonType={SessionButtonType.Solid}
text={tr('delete')}
onClick={async () => {
if (selectedConversationKey) {
await deleteMessagesForX(
selectedMessageIds,
selectedConversationKey,
enforceDeleteServerSide
);
}
onClick={() => {
void deleteMessagesCb?.(selectedMessageIds);
}}
/>
</SessionFocusTrap>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const ClickToTrustSender = (props: { messageId: string }) => {
const convo = ConvoHub.use().get(sender);
window.inboxStore?.dispatch(
updateConfirmModal({
title: tr('attachmentsAutoDownloadModalTitle'),
title: { token: 'attachmentsAutoDownloadModalTitle' },
i18nMessage: {
token: 'attachmentsAutoDownloadModalDescription',
conversation_name: convo.getNicknameOrRealUsernameOrPlaceholder(),
Expand Down Expand Up @@ -114,8 +114,8 @@ export const ClickToTrustSender = (props: { messageId: string }) => {
onClickClose: () => {
window.inboxStore?.dispatch(updateConfirmModal(null));
},
okText: tr('yes'),
cancelText: tr('no'),
okText: { token: 'yes' },
cancelText: { token: 'no' },
})
);
};
Expand Down
Loading