Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
387e045
feat(chat): combine captions with media and fix redacted timeline rows
cursoragent Apr 14, 2026
a700b1a
feat(chat): add drag-and-drop file attach to human chat composer
cursoragent Apr 14, 2026
82967f6
fix(chat): preserve human chat scroll when history shrinks
cursoragent Apr 14, 2026
6e81095
fix: apply CodeRabbit auto-fixes
cursoragent Apr 14, 2026
bb86193
fix(matrix): satisfy RoomMessageEventContent format literal in sendMe…
cursoragent Apr 14, 2026
d08a24f
fix(matrix): allow format on Hypha media event content for captions
cursoragent Apr 15, 2026
6134e39
fix(chat): keep add-reaction emoji picker open inside overflow menus
cursoragent Apr 15, 2026
d6c9d5b
style(chat): compact composer selection format toolbar
cursoragent Apr 15, 2026
66ee428
feat(matrix): send voice as m.audio with duration metadata
cursoragent Apr 15, 2026
ea8f461
feat(chat): mic menu for dictation vs voice, Telegram-style audio pla…
cursoragent Apr 15, 2026
c275c80
feat(i18n): add HumanChatPanel strings for dictation and voice UI
cursoragent Apr 15, 2026
e48dd95
fix(chat): YouTube-style inline video playback and draft play overlay
cursoragent Apr 15, 2026
fbd4285
feat(matrix): edit media messages via m.replace with slots
cursoragent Apr 15, 2026
417ba6b
feat(chat): edit captions and remove attachments on media messages
cursoragent Apr 15, 2026
ce431d3
feat(ui): add squircle shape and chat avatar sizes
cursoragent Apr 15, 2026
4b0aa41
feat(chat): resolve Matrix member avatars for timeline and replies
cursoragent Apr 15, 2026
c8dfd7e
feat(chat): add Discord-style reply header and curved connector
cursoragent Apr 15, 2026
9b4ac28
fix(ci): restore check-types for matrix media edit and chat overflow
cursoragent Apr 15, 2026
3cd63a0
fix(chat): circular avatars and reply row alignment
cursoragent Apr 15, 2026
15ed069
fix: apply CodeRabbit auto-fixes
cursoragent Apr 15, 2026
d328110
feat(chat): improve voice composer stop control and dictation icon
cursoragent Apr 15, 2026
969c571
fix(chat): reply header avatars and top-aligned main avatar
cursoragent Apr 15, 2026
967e083
fix(chat): balance composer vertical padding around textarea and toolbar
cursoragent Apr 15, 2026
2491aa9
fix(matrix): count online members from presence status
cursoragent Apr 15, 2026
5eaa6d6
fix: apply CodeRabbit auto-fixes
cursoragent Apr 15, 2026
0fcafca
fix(chat): strip dictation ZWSP without polynomial regex (CodeQL)
cursoragent Apr 15, 2026
b5d6973
fix(chat): align main avatar top with sender when replying
cursoragent Apr 15, 2026
269048e
fix(human-chat): send on Enter when attach menu is open
cursoragent Apr 15, 2026
e90f203
fix(human-chat): open sibling toolbar popovers in one click
cursoragent Apr 15, 2026
3a4987c
fix(human-chat): align reply connector to quoted avatar
cursoragent Apr 15, 2026
9efbf0a
fix: apply CodeRabbit auto-fixes
cursoragent Apr 15, 2026
c20b13c
fix(human-chat): measure reply thread connector between avatars
cursoragent Apr 15, 2026
c79ca79
fix(human-chat): send on Enter when focus is on draft attachments
cursoragent Apr 15, 2026
b6626d3
fix(human-chat): stream dictation text into composer in real time
cursoragent Apr 15, 2026
7757725
docs(test): link media-edit reply coverage to matrix-provider
cursoragent Apr 15, 2026
4cee0dd
fix(human-chat): resolve member display names from Hypha profile
cursoragent Apr 18, 2026
f5ec335
fix(human-chat): tune Discord-style reply connector geometry
cursoragent Apr 18, 2026
ec3e416
fix(human-chat): Enter sends when toolbar focused or after voice flows
cursoragent Apr 18, 2026
c51f49e
feat(human-chat): Telegram-style voice row in timeline and composer
cursoragent Apr 18, 2026
9476bd4
fix(human-chat): show mic and dictate buttons in composer bar
cursoragent Apr 18, 2026
02787ac
fix(human-chat): spoiler preview and reveal for voice attachments
cursoragent Apr 18, 2026
ea14f57
feat(human-chat): cancel pending uploads and black voice play button
cursoragent Apr 18, 2026
f40dbe9
fix(human-chat): show format toolbar after selection drag ends
cursoragent Apr 18, 2026
f1ff261
fix(human-chat): copy Hypha chat deep links instead of matrix.to
cursoragent Apr 18, 2026
b565a3a
feat(human-chat): unread markers, daily separators, and mark read
cursoragent Apr 18, 2026
b5039c1
fix(human-chat): polish voice/dictate stop button affordance
cursoragent Apr 18, 2026
069d00b
fix(human-chat): close overflow when using quick reaction emojis
cursoragent Apr 18, 2026
00c0a81
feat(human-chat): highlight pasted URLs in composer like Discord
cursoragent Apr 18, 2026
5cf93ab
fix(ci): unblock check-types for Matrix read markers and human chat
cursoragent Apr 18, 2026
c1c938b
fix(human-chat): avoid ReDoS in composer URL punctuation trim
cursoragent Apr 18, 2026
73585d1
feat(human-chat): animate voice waveform bars while audio plays
cursoragent Apr 18, 2026
0a5b0d0
fix(human-chat): swap voice toolbar icons and per-slot stop controls
cursoragent Apr 18, 2026
e4f26fc
feat(human-chat): use microphone icon for idle voice playback control
cursoragent Apr 18, 2026
34fc11f
style(human-chat): match video play overlay to black circle like voic…
cursoragent Apr 18, 2026
006859c
feat(human-chat): REC on-air blink for recording/dictate stop slots
cursoragent Apr 18, 2026
ee9307d
fix(human-chat): anchor format toolbar above selection
cursoragent Apr 18, 2026
b00fbfe
test(core): tighten media edit empty-caption reply regression tests
cursoragent Apr 18, 2026
d601230
fix(human-chat): show reply connector above avatars
cursoragent Apr 18, 2026
e602423
fix(human-chat): redraw reply connector with reliable L path
cursoragent Apr 18, 2026
6129e95
fix(human-chat): resolve prod_privy_* Matrix display names via Hypha …
cursoragent Apr 18, 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
574 changes: 429 additions & 145 deletions packages/core/src/matrix/client/providers/matrix-provider.tsx

Large diffs are not rendered by default.

39 changes: 34 additions & 5 deletions packages/core/src/matrix/rich-reply.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Matrix rich reply plaintext helpers (Client-Server API — rich replies). */

import { MatrixEventEvent, RelationType } from 'matrix-js-sdk';
import { EventType, MatrixEventEvent, RelationType } from 'matrix-js-sdk';
import type * as MatrixSdk from 'matrix-js-sdk';

import type { Message, MessageMediaBundleItem } from './types';
Expand All @@ -18,7 +18,7 @@ export const HYPHA_SPOILER_FIELD = 'org.hypha.spoiler';
export const HYPHA_MEDIA_BUNDLE_FIELD = 'org.hypha.media_bundle';

export type HyphaMediaBundleItemWire = {
msgtype: 'm.file' | 'm.image';
msgtype: 'm.file' | 'm.image' | 'm.audio';
url: string;
body?: string;
filename?: string;
Expand Down Expand Up @@ -257,6 +257,18 @@ function parseReplyFallbackFirstLine(body: string): {
/**
* Map a timeline `m.room.message` event to Hypha `Message` (reply metadata + display body).
*/
/**
* Redacted `m.room.message` events still exist on the timeline; skip them so the UI
* does not render empty rows (avatar + “You” with no body).
*/
export function isRedactedRoomMessageEvent(
event: MatrixSdk.MatrixEvent,
): boolean {
return (
event.getType() === EventType.RoomMessage && event.isRedacted() === true
);
}

export function messageFromRoomMessageEvent(
client: MatrixSdk.MatrixClient,
roomId: string,
Expand All @@ -279,7 +291,10 @@ export function messageFromRoomMessageEvent(
[key: string]: unknown;
};
const msgtypeRaw = content.msgtype;
const isMedia = msgtypeRaw === 'm.file' || msgtypeRaw === 'm.image';
const isMedia =
msgtypeRaw === 'm.file' ||
msgtypeRaw === 'm.image' ||
msgtypeRaw === 'm.audio';
const rawBody = content.body ?? '';
const replyToId = event.getWireContent()?.['m.relates_to']?.['m.in_reply_to']
?.event_id as string | undefined;
Expand Down Expand Up @@ -318,13 +333,24 @@ export function messageFromRoomMessageEvent(
content.formatted_body,
);
}
} else if (
content.format === MATRIX_CUSTOM_HTML_FORMAT &&
typeof content.formatted_body === 'string'
) {
formattedContentHtml = extractReplyFormattedHtml(content.formatted_body);
}
} else if (
!isMedia &&
content.format === MATRIX_CUSTOM_HTML_FORMAT &&
typeof content.formatted_body === 'string'
) {
formattedContentHtml = content.formatted_body;
} else if (
isMedia &&
content.format === MATRIX_CUSTOM_HTML_FORMAT &&
typeof content.formatted_body === 'string'
) {
formattedContentHtml = content.formatted_body;
}

const id = event.getId();
Expand Down Expand Up @@ -352,13 +378,16 @@ export function messageFromRoomMessageEvent(
size: typeof o.size === 'number' ? o.size : undefined,
w: typeof o.w === 'number' ? o.w : undefined,
h: typeof o.h === 'number' ? o.h : undefined,
duration: typeof o.duration === 'number' ? o.duration : undefined,
};
};

const parseBundleItem = (wire: unknown): MessageMediaBundleItem => {
const w = wire as HyphaMediaBundleItemWire;
const mt =
w.msgtype === 'm.file' || w.msgtype === 'm.image'
w.msgtype === 'm.file' ||
w.msgtype === 'm.image' ||
w.msgtype === 'm.audio'
? w.msgtype
: 'm.file';
const url =
Expand All @@ -383,7 +412,7 @@ export function messageFromRoomMessageEvent(
let mediaBundle: Message['mediaBundle'];
if (Array.isArray(bundleRaw) && bundleRaw.length > 0) {
const first = {
msgtype: msgtypeRaw as 'm.file' | 'm.image',
msgtype: msgtypeRaw as 'm.file' | 'm.image' | 'm.audio',
mxcUrl,
filename:
typeof content.filename === 'string'
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/matrix/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@ export type MessageMediaInfo = {
size?: number;
w?: number;
h?: number;
/** Matrix `m.audio` / MSC1767: duration in milliseconds */
duration?: number;
};

/** One slot in a multi-attachment `org.hypha.media_bundle` message. */
export type MessageMediaBundleItem = {
msgtype: 'm.file' | 'm.image';
msgtype: 'm.file' | 'm.image' | 'm.audio';
mxcUrl?: string;
filename?: string;
mediaInfo?: MessageMediaInfo;
Expand All @@ -69,8 +71,10 @@ export type MessageMediaBundleItem = {
export interface Message {
id: string;
sender: string;
/** Synthetic row from a redaction handler: remove this timeline id from UI. */
redacted?: boolean;
/** Matrix msgtype for timeline rendering (`m.text` is implicit when omitted). */
msgtype?: 'm.text' | 'm.file' | 'm.image';
msgtype?: 'm.text' | 'm.file' | 'm.image' | 'm.audio';
/** Visible message text (reply fallback stripped when applicable). */
content: string;
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,30 @@
* Matrix timeline attachment slice shared by chat list, bubbles, and HumanRightPanel.
*/
export type ChatPanelAttachmentMedia = {
msgtype: 'm.file' | 'm.image';
msgtype: 'm.file' | 'm.image' | 'm.audio';
mxcUrl?: string;
filename?: string;
mediaInfo?: {
mimetype?: string;
size?: number;
w?: number;
h?: number;
duration?: number;
};
spoiler?: boolean;
};

const AUDIO_FILE_EXTENSIONS = new Set([
'ogg',
'oga',
'opus',
'mp3',
'm4a',
'aac',
'flac',
'wav',
]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const VIDEO_FILE_EXTENSIONS = new Set([
'mov',
'mp4',
Expand All @@ -33,6 +45,19 @@ function extensionFromFileNameHint(name: string): string {
return base.split('.').pop()?.toLowerCase() ?? '';
}

/** Local file draft: treat as voice/audio attachment (not video). */
export function looksLikeAudioMimeOrName(
mimetype?: string,
filename?: string,
): boolean {
const mt = mimetype?.toLowerCase() ?? '';
if (mt.startsWith('audio/')) return true;
const ext = extensionFromFileNameHint(filename ?? '');
if (!ext) return false;
if (ext === 'webm') return false;
return AUDIO_FILE_EXTENSIONS.has(ext);
}

/** Local file or Matrix `info.mimetype` + name (composer drafts, timeline). */
export function looksLikeVideoMimeOrName(
mimetype?: string,
Expand All @@ -54,3 +79,15 @@ export function isChatPanelVideoFile(
media.filename ?? '',
);
}

/** Matrix `m.audio` or audio-like `m.file` (voice clips often use m.file + audio/*). */
export function isChatPanelAudioFile(
media: Pick<ChatPanelAttachmentMedia, 'msgtype' | 'mediaInfo' | 'filename'>,
): boolean {
if (media.msgtype === 'm.audio') return true;
if (media.msgtype !== 'm.file') return false;
const mt = media.mediaInfo?.mimetype?.toLowerCase() ?? '';
if (mt.startsWith('audio/')) return true;
const ext = extensionFromFileNameHint(media.filename ?? '');
return AUDIO_FILE_EXTENSIONS.has(ext);
}
Loading
Loading