(feat): add hotkeys
Some checks failed
/ build-web (push) Successful in 7m53s
/ build-desktop (linux) (push) Successful in 12m24s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled

This commit is contained in:
Alois 2026-08-02 01:10:05 +02:00
commit 85633a1c81
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
27 changed files with 1014 additions and 33 deletions

View file

@ -85,6 +85,8 @@ export default function Screen() {
clearLiveMessages,
userId,
chatSecret,
ownId,
inputBoxRef,
error,
errorDescription,
} = useChat();
@ -103,10 +105,25 @@ export default function Screen() {
const [didInitialScroll, setDidInitialScroll] = useState(false);
const [viewportHeight, setViewportHeight] = useState(0);
const [value, setValue] = useState("");
const [editingMessageId, setEditingMessageId] = useState<number | null>(null);
const previousEditingMessageIdRef = useRef<number | null>(null);
const hasValidChatUser = Number.isSafeInteger(userId) && userId > 0;
const hasChatSecret = chatSecret !== null;
useEffect(() => {
const previousEditingMessageId = previousEditingMessageIdRef.current;
previousEditingMessageIdRef.current = editingMessageId;
if (previousEditingMessageId === null || editingMessageId !== null) return;
const frame = requestAnimationFrame(() => {
inputBoxRef.current
?.querySelector<HTMLElement>(".cm-content")
?.focus({ preventScroll: true });
});
return () => cancelAnimationFrame(frame);
}, [editingMessageId, inputBoxRef]);
const messagesQuery = useInfiniteQuery({
queryKey: ["chat-messages", String(userId), hasChatSecret],
initialPageParam: 0,
@ -141,6 +158,7 @@ export default function Screen() {
isAtBottomRef.current = true;
setDidInitialScroll(false);
setLastLiveMessageCount(0);
setEditingMessageId(null);
}, [clearLiveMessages, userId]);
const historicalMessages = useMemo(() => {
@ -213,6 +231,34 @@ export default function Screen() {
const contentHeight = Math.max(totalSize, viewportHeight);
const verticalOffset = Math.max(0, viewportHeight - totalSize);
const editLastMessage = useCallback(() => {
if (editingMessageId !== null) return;
const message = [...messages]
.reverse()
.find(
(candidate) =>
candidate.SenderId === ownId &&
candidate.Content.length > 0 &&
candidate.MessageState !== "awaiting" &&
!("failed" in candidate && candidate.failed) &&
!("decryptionFailed" in candidate && candidate.decryptionFailed),
);
if (!message) return;
const chunkIndex = messageChunks.findIndex((chunk) =>
chunk.messages.some(
(candidate) => candidate.SendTime === message.SendTime,
),
);
const chunkIsRendered = virtualizer
.getVirtualItems()
.some(({ index }) => index === chunkIndex);
if (chunkIndex >= 0 && !chunkIsRendered) {
virtualizer.scrollToIndex(chunkIndex, { align: "center" });
}
setEditingMessageId(message.SendTime);
}, [editingMessageId, messageChunks, messages, ownId, virtualizer]);
useLayoutEffect(() => {
const element = scrollRef.current;
if (!element || typeof ResizeObserver === "undefined") {
@ -489,8 +535,14 @@ export default function Screen() {
loading={null}
component={(user) => (
<Message
editing={editingMessageId === message.SendTime}
grouped={isGrouped}
message={message}
onSetEditing={(editing) =>
setEditingMessageId(
editing ? message.SendTime : null,
)
}
user={user}
/>
)}
@ -504,7 +556,11 @@ export default function Screen() {
</div>
</div>
<div className="z-10 shrink-0">
<InputComponent setValue={setValue} value={value} />
<InputComponent
onEditLastMessage={editLastMessage}
setValue={setValue}
value={value}
/>
</div>
</>
)}