Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
25 changes: 23 additions & 2 deletions packages/core/src/matrix/client/providers/matrix-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export interface EditRoomMessageInput {
message: string;
}

export interface RedactRoomEventInput {
roomId: string;
eventId: string;
}
/**
* Thrown when some attachment events were committed but a later send step failed.
* Callers should restore only `attachments.slice(sentAttachmentCount)` and optionally caption text.
Expand Down Expand Up @@ -179,6 +183,7 @@ interface MatrixContextType {
createRoom: (title: string) => Promise<{ roomId: string }>;
sendMessage: (params: SendMessageInput) => Promise<void>;
editRoomMessage: (params: EditRoomMessageInput) => Promise<void>;
redactRoomEvent: (params: RedactRoomEventInput) => Promise<void>;
toggleReaction: (params: ToggleReactionInput) => Promise<void>;
getRoomMessages: (roomId: string) => Message[] | null;
getPinnedMessageIds: (roomId: string) => string[];
Expand Down Expand Up @@ -684,7 +689,7 @@ export const MatrixProvider: React.FC<MatrixProviderProps> = ({ children }) => {

if (replyToId?.trim()) {
const {
eventId: resolvedTargetId,
eventId: resolvedReplyTargetId,
sender: replyTargetSender,
body: targetBody,
} = await resolveReplyTargetForSend(client, roomId, replyToId);
Expand All @@ -698,7 +703,7 @@ export const MatrixProvider: React.FC<MatrixProviderProps> = ({ children }) => {
...rich,
'm.relates_to': {
'm.in_reply_to': {
event_id: resolvedTargetId,
event_id: resolvedReplyTargetId,
},
},
} as RoomMessageEventContent;
Expand Down Expand Up @@ -726,6 +731,18 @@ export const MatrixProvider: React.FC<MatrixProviderProps> = ({ children }) => {
[client],
);

const redactRoomEvent = React.useCallback(
async ({ roomId, eventId }: RedactRoomEventInput) => {
if (!client) {
throw new Error('Client should be specified');
}
if (!roomId?.trim() || !eventId?.trim()) {
return;
}
await client.redactEvent(roomId, eventId);
},
[client],
);
const getPinnedMessageIds = React.useCallback(
(roomId: string): string[] => {
if (!client) {
Expand Down Expand Up @@ -1079,6 +1096,7 @@ export const MatrixProvider: React.FC<MatrixProviderProps> = ({ children }) => {
createRoom,
sendMessage,
editRoomMessage,
redactRoomEvent,
toggleReaction,
getRoomMessages,
getPinnedMessageIds,
Expand Down Expand Up @@ -1107,6 +1125,9 @@ const noopMatrixContext: MatrixContextType = {
editRoomMessage: async () => {
throw new Error('Matrix unavailable');
},
redactRoomEvent: async () => {
throw new Error('Matrix unavailable');
},
toggleReaction: async () => {
throw new Error('Matrix unavailable');
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
'use client';

import { Fragment, useMemo, useRef, useState } from 'react';
import type { ReactNode } from 'react';
import { useFormatter, useTranslations } from 'next-intl';
import type { TranslationValues } from 'next-intl';
import {
Smile,
SmilePlus,
Pencil,
Reply,
MoreHorizontal,
FileIcon,
ExternalLink,
Image as ImageIcon,
Expand All @@ -20,6 +20,10 @@ import { useMatrix } from '@hypha-platform/core/client';
import { PersonAvatar } from '../../people/components/person-avatar';

import { HumanChatPanelEmojiPicker } from './human-chat-panel-emoji-picker';
import {
HumanChatPanelMessageOverflow,
pushRecentChatReaction,
} from './human-chat-panel-message-overflow';
import { ChatMessageRichText } from './parse-simple-matrix-html';
import {
type ChatPanelAttachmentMedia,
Expand Down Expand Up @@ -490,12 +494,18 @@ type HumanChatPanelMessageBubbleProps = {
onRowPointerLeave?: () => void;
/** Notify when the hover-bar emoji picker opens/closes (parent may lock visibility). */
onHoverReactPickerOpenChange?: (open: boolean) => void;
/** Active Matrix room (for message link + overflow). */
roomId?: string | null;
/** Logged-in Matrix user id (delete permission + recent reactions). */
currentUserId?: string | null;
message: {
id: string;
role: 'user' | 'member';
isSynthetic?: boolean;
parts?: UIMessagePart[];
senderName?: string;
/** Author MXID when known (overflow delete). */
senderMatrixId?: string;
avatarUrl?: string;
timestamp?: Date;
reactions?: Reaction[];
Expand All @@ -520,6 +530,7 @@ type HumanChatPanelMessageBubbleProps = {
onReply?: () => void;
/** When set, Edit is enabled (own text messages only; parent omits otherwise). */
onEdit?: () => void;
onDeleteMessage?: (messageId: string) => void | Promise<void>;
/** When set, user can open react picker (omit for welcome). */
onReact?: (emoji: string) => void | Promise<void>;
};
Expand Down Expand Up @@ -708,8 +719,11 @@ export function HumanChatPanelMessageBubble({
onHoverReactPickerOpenChange,
message,
isStreaming,
roomId,
currentUserId,
onReply,
onEdit,
onDeleteMessage,
onReact,
}: HumanChatPanelMessageBubbleProps) {
const t = useTranslations('HumanChatPanel');
Expand Down Expand Up @@ -799,7 +813,7 @@ export function HumanChatPanelMessageBubble({
reactions.length - MAX_VISIBLE_REACTIONS,
);

return (
const row = (moreSlot: ReactNode | null) => (
<div
data-testid="chat-message"
className={cn(
Expand Down Expand Up @@ -1227,6 +1241,7 @@ export function HumanChatPanelMessageBubble({
open={inlineReactPickerOpen}
onOpenChange={setInlineReactPickerOpen}
onEmojiSelect={(native) => {
pushRecentChatReaction(native);
void onReact(native);
}}
ariaLabel={t('addReactionButton')}
Expand Down Expand Up @@ -1264,7 +1279,10 @@ export function HumanChatPanelMessageBubble({
onHoverReactPickerOpenChange?.(open);
}}
onEmojiSelect={(native) => {
if (onReact) void onReact(native);
if (onReact) {
pushRecentChatReaction(native);
void onReact(native);
}
}}
ariaLabel={t('emojiPickerReactToMessage')}
align="end"
Expand Down Expand Up @@ -1300,16 +1318,34 @@ export function HumanChatPanelMessageBubble({
>
<Reply className="h-3 w-3" strokeWidth={2} />
</button>
<button
type="button"
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-sm p-0 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground [&_svg]:block"
aria-label={t('moreButton')}
disabled
aria-disabled
>
<MoreHorizontal className="h-3 w-3" strokeWidth={2} />
</button>
{moreSlot}
</div>
</div>
);

if (message.isSynthetic) {
return row(null);
}

return (
<HumanChatPanelMessageOverflow
roomId={roomId ?? null}
messageId={message.id}
disabled={false}
canReact={canReact}
onReact={onReact}
onEdit={onEdit}
onReply={onReply}
onDeleteMessage={onDeleteMessage}
currentUserId={currentUserId}
senderMatrixId={message.senderMatrixId}
message={{
parts: message.parts,
media: message.media,
mediaSlots: message.mediaSlots,
}}
>
{row}
</HumanChatPanelMessageOverflow>
);
}
Loading
Loading