mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
feat(app): add quick reply and edit keybinds (#3032)
This commit is contained in:
1 parent
364084c819
commit
9908518f5b
43 files changed
+3475
-2972
No files matched your search
@@ -40,7 +40,7 @@ import {
|
||||
createChannelStream,
|
||||
getCollapsedMessageGroupKey,
|
||||
} from '@app/features/messaging/utils/MessageGroupingUtils';
|
||||
import {getMessageSelector} from '@app/features/messaging/utils/MessageNodeSelectors';
|
||||
import {findMessageElement, getMessageSelector} from '@app/features/messaging/utils/MessageNodeSelectors';
|
||||
import LocalUserSpamOverride from '@app/features/moderation/state/LocalUserSpamOverride';
|
||||
import SelectedChannel from '@app/features/navigation/state/SelectedChannel';
|
||||
import Permission from '@app/features/permissions/state/Permission';
|
||||
@@ -281,6 +281,25 @@ export const Messages = observer(function Messages({
|
||||
});
|
||||
lastStateSnapshotRef.current = snapshot;
|
||||
}, [channel.id, state]);
|
||||
const revealMessageNode = useCallback(
|
||||
(targetNode: HTMLElement) => {
|
||||
const scrollerNode = scrollManager.ref.current?.getViewportElement();
|
||||
if (!scrollerNode) return;
|
||||
const targetRect = targetNode.getBoundingClientRect();
|
||||
const scrollerRect = scrollerNode.getBoundingClientRect();
|
||||
const isAbove = targetRect.top < scrollerRect.top;
|
||||
const isBelow = targetRect.bottom > scrollerRect.bottom;
|
||||
if (isAbove || isBelow) {
|
||||
scrollManager.ref.current?.revealElement({
|
||||
node: targetNode,
|
||||
padding: 80,
|
||||
animate: false,
|
||||
});
|
||||
scrollManager.scrollHandle();
|
||||
}
|
||||
},
|
||||
[scrollManager],
|
||||
);
|
||||
const onMessageEdit = useCallback(
|
||||
(targetNode: HTMLElement) => {
|
||||
const scrollerNode = scrollManager.ref.current?.getViewportElement();
|
||||
@@ -296,20 +315,9 @@ export const Messages = observer(function Messages({
|
||||
return;
|
||||
}
|
||||
}
|
||||
const targetRect = targetNode.getBoundingClientRect();
|
||||
const scrollerRect = scrollerNode.getBoundingClientRect();
|
||||
const isAbove = targetRect.top < scrollerRect.top;
|
||||
const isBelow = targetRect.bottom > scrollerRect.bottom;
|
||||
if (isAbove || isBelow) {
|
||||
scrollManager.ref.current?.revealElement({
|
||||
node: targetNode,
|
||||
padding: 80,
|
||||
animate: false,
|
||||
});
|
||||
scrollManager.scrollHandle();
|
||||
}
|
||||
revealMessageNode(targetNode);
|
||||
},
|
||||
[scrollManager, channel.id],
|
||||
[scrollManager, channel.id, revealMessageNode],
|
||||
);
|
||||
const onReveal = useCallback(
|
||||
(messageId: string | null) => {
|
||||
@@ -432,6 +440,22 @@ export const Messages = observer(function Messages({
|
||||
dispatchUnsubs.forEach((u) => u());
|
||||
};
|
||||
}, [channel.id, updateFromState, onScrollToPresent, onMessageSent, onEscapePressed, scrollManager]);
|
||||
useEffect(() => {
|
||||
return ComponentBus.subscribe('MESSAGE_REVEAL', (payload?: unknown) => {
|
||||
const data = (payload ?? {}) as {channelId?: string; messageId?: string};
|
||||
if (data.channelId !== channel.id || !data.messageId) return;
|
||||
const messageId = data.messageId;
|
||||
window.requestAnimationFrame(() => {
|
||||
const node = findMessageElement(
|
||||
document,
|
||||
scrollManager.ref.current?.getViewportElement(),
|
||||
channel.id,
|
||||
messageId,
|
||||
);
|
||||
if (node) revealMessageNode(node);
|
||||
});
|
||||
});
|
||||
}, [channel.id, scrollManager, revealMessageNode]);
|
||||
useEffect(() => {
|
||||
const editingMessageId = state.editingMessageId;
|
||||
if (editingMessageId) {
|
||||
|
||||
@@ -17,6 +17,9 @@ import * as ReactionCommands from '@app/features/messaging/commands/ReactionComm
|
||||
import * as SavedMessageCommands from '@app/features/messaging/commands/SavedMessageCommands';
|
||||
import {ForwardModal, type ForwardModalSuccess} from '@app/features/messaging/components/modals/ForwardModal';
|
||||
import type {Message} from '@app/features/messaging/models/MessagingMessage';
|
||||
import MessageEdit from '@app/features/messaging/state/MessageEdit';
|
||||
import MessageReply from '@app/features/messaging/state/MessageReply';
|
||||
import Messages from '@app/features/messaging/state/MessagingMessages';
|
||||
import SavedMessages from '@app/features/messaging/state/SavedMessages';
|
||||
import {buildMessageJumpLink} from '@app/features/messaging/utils/MessageLinkUtils';
|
||||
import {retryFailedMessage} from '@app/features/messaging/utils/MessageRetryUtils';
|
||||
@@ -507,6 +510,67 @@ export function requestMessageReply(message: Message, options?: RequestMessageRe
|
||||
startReply(shouldMention);
|
||||
}
|
||||
|
||||
type MessageStepDirection = -1 | 1;
|
||||
|
||||
function findAdjacentMessage(
|
||||
channelId: string,
|
||||
currentMessageId: string | null,
|
||||
direction: MessageStepDirection,
|
||||
predicate: (message: Message) => boolean,
|
||||
): Message | null {
|
||||
const candidates = Messages.getMessages(channelId).toArray().filter(predicate);
|
||||
const index = currentMessageId ? candidates.findIndex((message) => message.id === currentMessageId) : -1;
|
||||
if (index === -1) return candidates[candidates.length - 1] ?? null;
|
||||
if (direction < 0) return candidates[Math.max(index - 1, 0)];
|
||||
return candidates[index + 1] ?? null;
|
||||
}
|
||||
|
||||
function isReplyCandidate(message: Message): boolean {
|
||||
return (
|
||||
message.state === MessageStates.SENT &&
|
||||
message.isUserMessage() &&
|
||||
!isClientSystemMessage(message) &&
|
||||
!Relationships.isBlocked(message.author.id)
|
||||
);
|
||||
}
|
||||
|
||||
function isEditCandidate(message: Message): boolean {
|
||||
return (
|
||||
message.state === MessageStates.SENT &&
|
||||
message.isUserMessage() &&
|
||||
!isClientSystemMessage(message) &&
|
||||
message.isCurrentUserAuthor() &&
|
||||
!message.messageSnapshots
|
||||
);
|
||||
}
|
||||
|
||||
export function requestAdjacentMessageReply(channelId: string, direction: MessageStepDirection): void {
|
||||
const current = MessageReply.getReplyingMessage(channelId)?.messageId ?? null;
|
||||
const message = findAdjacentMessage(channelId, current, direction, isReplyCandidate);
|
||||
if (!message) {
|
||||
MessageCommands.stopReply(channelId);
|
||||
return;
|
||||
}
|
||||
if (!getMessagePermissions(message)?.canSendMessages) return;
|
||||
requestMessageReply(message);
|
||||
ComponentBus.dispatch('MESSAGE_REVEAL', {channelId, messageId: message.id});
|
||||
}
|
||||
|
||||
export function startAdjacentMessageEdit(channelId: string, direction: MessageStepDirection): void {
|
||||
const current = MessageEdit.getEditingMessageId(channelId);
|
||||
const message = findAdjacentMessage(channelId, current, direction, isEditCandidate);
|
||||
if (!message) {
|
||||
if (current) {
|
||||
MessageCommands.stopEdit(channelId);
|
||||
ComponentBus.dispatch('FOCUS_TEXTAREA', {channelId});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!getMessagePermissions(message)?.canEditMessage) return;
|
||||
MessageCommands.startEdit(channelId, message.id, message.content);
|
||||
ComponentBus.dispatch('MESSAGE_REVEAL', {channelId, messageId: message.id});
|
||||
}
|
||||
|
||||
interface RequestMessageForwardOptions {
|
||||
mediaSelection?: MessageCommands.ForwardMediaSelection;
|
||||
onForwardSuccess?: (result: ForwardModalSuccess) => void;
|
||||
|
||||
Reference in new issue
Block a user