{member.name}
{member.userId}
diff --git a/src/lib/components/matrix/MessageInput.svelte b/src/lib/components/matrix/MessageInput.svelte
index 8216cc3..80b6797 100644
--- a/src/lib/components/matrix/MessageInput.svelte
+++ b/src/lib/components/matrix/MessageInput.svelte
@@ -36,14 +36,12 @@
// Render emojis as Twemoji images for preview
function renderEmojiPreview(text: string): string {
- // Escape HTML first
const escaped = text
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/\n/g, "
");
- // Replace emojis with Twemoji images
return escaped.replace(emojiRegex, (emoji) => {
const url = getTwemojiUrl(emoji);
return `

`;
@@ -89,7 +87,6 @@
// Emoji picker state
let showEmojiPicker = $state(false);
- let emojiButtonRef: HTMLButtonElement;
// Emoji autocomplete state
let showEmojiAutocomplete = $state(false);
@@ -125,22 +122,17 @@
function autoResize() {
if (!inputRef) return;
inputRef.style.height = "auto";
- inputRef.style.height = Math.min(inputRef.scrollHeight, 200) + "px";
+ inputRef.style.height = Math.min(inputRef.scrollHeight, 160) + "px";
}
// Handle typing indicator
function handleTyping() {
- // Clear existing timeout
- if (typingTimeout) {
- clearTimeout(typingTimeout);
- }
+ if (typingTimeout) clearTimeout(typingTimeout);
- // Send typing indicator
setTyping(roomId, true).catch((e) =>
log.error("Failed to send typing", { error: e }),
);
- // Stop typing after 3 seconds of no input
typingTimeout = setTimeout(() => {
setTyping(roomId, false).catch((e) =>
log.error("Failed to stop typing", { error: e }),
@@ -151,31 +143,20 @@
// Handle input
function handleInput() {
autoResize();
- if (message.trim()) {
- handleTyping();
- }
-
- // Auto-convert completed emoji shortcodes like :heart: to actual emojis
+ if (message.trim()) handleTyping();
autoConvertShortcodes();
-
- // Check for @ mentions and : emoji shortcodes
checkForMention();
checkForEmoji();
}
- // Auto-convert completed emoji shortcodes (e.g., :heart:) to actual emojis
+ // Auto-convert completed emoji shortcodes
function autoConvertShortcodes() {
if (!inputRef) return;
const cursorPos = inputRef.selectionStart;
-
- // Look for completed shortcodes like :name:
const converted = convertEmojiShortcodes(message);
if (converted !== message) {
- // Calculate cursor offset based on length difference
const lengthDiff = message.length - converted.length;
message = converted;
-
- // Restore cursor position (adjusted for shorter string)
setTimeout(() => {
if (inputRef) {
const newPos = Math.max(0, cursorPos - lengthDiff);
@@ -188,16 +169,12 @@
// Check if user is typing an emoji shortcode
function checkForEmoji() {
if (!inputRef) return;
-
const cursorPos = inputRef.selectionStart;
const textBeforeCursor = message.slice(0, cursorPos);
-
- // Find the last : before cursor
const lastColonIndex = textBeforeCursor.lastIndexOf(":");
if (lastColonIndex >= 0) {
const textAfterColon = textBeforeCursor.slice(lastColonIndex + 1);
- // Check if there's a space before : (or it's at start) and no space after, and query is at least 2 chars
const charBeforeColon =
lastColonIndex > 0 ? message[lastColonIndex - 1] : " ";
@@ -222,31 +199,23 @@
// Handle emoji selection from autocomplete
function handleEmojiSelect(emoji: string) {
- // Replace :query with the emoji
const beforeEmoji = message.slice(0, emojiStartIndex);
const afterEmoji = message.slice(emojiStartIndex + emojiQuery.length + 1);
message = `${beforeEmoji}${emoji}${afterEmoji}`;
-
showEmojiAutocomplete = false;
emojiQuery = "";
-
- // Focus back on textarea
inputRef?.focus();
}
// Check if user is typing a mention
function checkForMention() {
if (!inputRef) return;
-
const cursorPos = inputRef.selectionStart;
const textBeforeCursor = message.slice(0, cursorPos);
-
- // Find the last @ before cursor that's not part of a completed mention
const lastAtIndex = textBeforeCursor.lastIndexOf("@");
if (lastAtIndex >= 0) {
const textAfterAt = textBeforeCursor.slice(lastAtIndex + 1);
- // Check if there's a space before @ (or it's at start) and no space after
const charBeforeAt = lastAtIndex > 0 ? message[lastAtIndex - 1] : " ";
if (
@@ -266,23 +235,19 @@
// Handle mention selection
function handleMentionSelect(member: RoomMember) {
- // Replace @query with userId (userId already has @ prefix)
const beforeMention = message.slice(0, mentionStartIndex);
const afterMention = message.slice(
mentionStartIndex + mentionQuery.length + 1,
);
message = `${beforeMention}${member.userId} ${afterMention}`;
-
showMentions = false;
mentionQuery = "";
-
- // Focus back on textarea
inputRef?.focus();
}
// Handle key press
function handleKeyDown(e: KeyboardEvent) {
- // If mention autocomplete is open, let it handle navigation keys
+ // Mention autocomplete navigation
if (
showMentions &&
["ArrowUp", "ArrowDown", "Tab", "Escape"].includes(e.key)
@@ -290,15 +255,13 @@
autocompleteRef?.handleKeyDown(e);
return;
}
-
- // Enter with mention autocomplete open selects the mention
if (showMentions && e.key === "Enter") {
e.preventDefault();
autocompleteRef?.handleKeyDown(e);
return;
}
- // If emoji autocomplete is open, let it handle navigation keys
+ // Emoji autocomplete navigation
if (
showEmojiAutocomplete &&
["ArrowUp", "ArrowDown", "Tab", "Escape"].includes(e.key)
@@ -306,8 +269,6 @@
emojiAutocompleteRef?.handleKeyDown(e);
return;
}
-
- // Enter with emoji autocomplete open selects the emoji
if (showEmojiAutocomplete && e.key === "Enter") {
e.preventDefault();
emojiAutocompleteRef?.handleKeyDown(e);
@@ -321,70 +282,42 @@
return;
}
- // Auto-continue lists on Shift+Enter or regular Enter with list
+ // Auto-continue lists on Shift+Enter
if (e.key === "Enter" && e.shiftKey) {
const cursorPos = inputRef?.selectionStart || 0;
const textBefore = message.slice(0, cursorPos);
const currentLine = textBefore.split("\n").pop() || "";
- // Check for numbered list (1. 2. etc)
const numberedMatch = currentLine.match(/^(\s*)(\d+)\.\s/);
if (numberedMatch) {
e.preventDefault();
const indent = numberedMatch[1];
const nextNum = parseInt(numberedMatch[2]) + 1;
- const newText =
+ message =
message.slice(0, cursorPos) +
`\n${indent}${nextNum}. ` +
message.slice(cursorPos);
- message = newText;
setTimeout(() => {
- if (inputRef) {
+ if (inputRef)
inputRef.selectionStart = inputRef.selectionEnd =
cursorPos + indent.length + String(nextNum).length + 4;
- }
}, 0);
return;
}
- // Check for bullet list (- or *)
const bulletMatch = currentLine.match(/^(\s*)([-*])\s/);
if (bulletMatch) {
e.preventDefault();
const indent = bulletMatch[1];
const bullet = bulletMatch[2];
- const newText =
+ message =
message.slice(0, cursorPos) +
`\n${indent}${bullet} ` +
message.slice(cursorPos);
- message = newText;
setTimeout(() => {
- if (inputRef) {
+ if (inputRef)
inputRef.selectionStart = inputRef.selectionEnd =
cursorPos + indent.length + 4;
- }
- }, 0);
- return;
- }
-
- // Check for lettered sub-list (a. b. etc)
- const letteredMatch = currentLine.match(/^(\s*)([a-z])\.\s/);
- if (letteredMatch) {
- e.preventDefault();
- const indent = letteredMatch[1];
- const nextLetter = String.fromCharCode(
- letteredMatch[2].charCodeAt(0) + 1,
- );
- const newText =
- message.slice(0, cursorPos) +
- `\n${indent}${nextLetter}. ` +
- message.slice(cursorPos);
- message = newText;
- setTimeout(() => {
- if (inputRef) {
- inputRef.selectionStart = inputRef.selectionEnd =
- cursorPos + indent.length + 5;
- }
}, 0);
return;
}
@@ -396,28 +329,23 @@
const trimmed = message.trim();
if (!trimmed || isSending || disabled) return;
- // Convert emoji shortcodes like :heart: to actual emojis
const processedMessage = convertEmojiShortcodes(trimmed);
// Handle edit mode
if (editingMessage) {
if (processedMessage === editingMessage.content) {
- // No changes, just cancel
onCancelEdit?.();
message = "";
return;
}
onSaveEdit?.(processedMessage);
message = "";
- if (inputRef) {
- inputRef.style.height = "auto";
- }
+ if (inputRef) inputRef.style.height = "auto";
return;
}
isSending = true;
- // Clear typing indicator
if (typingTimeout) {
clearTimeout(typingTimeout);
typingTimeout = null;
@@ -426,10 +354,8 @@
log.error("Failed to stop typing", { error: e }),
);
- // Create a temporary event ID for the pending message
const tempEventId = `pending-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
- // Add pending message immediately (optimistic update)
const pendingMessage: Message = {
eventId: tempEventId,
roomId,
@@ -448,14 +374,9 @@
addPendingMessage(roomId, pendingMessage);
message = "";
-
- // Clear reply
onCancelReply?.();
- // Reset textarea height
- if (inputRef) {
- inputRef.style.height = "auto";
- }
+ if (inputRef) inputRef.style.height = "auto";
try {
const result = await sendMessage(
@@ -463,21 +384,17 @@
processedMessage,
replyTo?.eventId,
);
- // Confirm the pending message with the real event ID
if (result?.event_id) {
confirmPendingMessage(roomId, tempEventId, result.event_id);
} else {
- // If no event ID returned, just mark as not pending
confirmPendingMessage(roomId, tempEventId, tempEventId);
}
} catch (e: unknown) {
log.error("Failed to send message", { error: e });
- // Remove the pending message on failure
removePendingMessage(roomId, tempEventId);
toasts.error(getErrorMessage(e, "Failed to send message"));
} finally {
isSending = false;
- // Refocus after DOM settles from optimistic update
await tick();
inputRef?.focus();
}
@@ -488,11 +405,8 @@
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (!file || disabled) return;
-
- // Reset input
input.value = "";
- // Check file size (50MB limit)
const maxSize = 50 * 1024 * 1024;
if (file.size > maxSize) {
toasts.error(m.toast_error_file_too_large());
@@ -518,35 +432,34 @@
}
-
+
{#if editingMessage}
+
edit
-
Editing message
-
{editingMessage.content}
+
Editing message
+
+ {editingMessage.content}
+
@@ -556,35 +469,47 @@
{#if replyTo && !editingMessage}
+
reply
-
+
Replying to {replyTo.senderName}
-
{replyTo.content}
+
{replyTo.content}
{/if}
-
+
+ {#if showEmojiPicker}
+
+
+ {
+ message += emoji;
+ inputRef?.focus();
+ }}
+ onClose={() => (showEmojiPicker = false)}
+ />
+
+
+ {/if}
+
+
@@ -656,13 +561,13 @@
/>
{/if}
-
+
-
+
{#if message && hasEmoji(message)}
{@html renderEmojiPreview(message)}
@@ -676,96 +581,63 @@
{placeholder}
disabled={disabled || isSending}
rows="1"
- class="w-full pl-4 pr-12 py-3 bg-dark rounded-2xl border border-light/20
- placeholder:text-light/40 resize-none overflow-hidden
- focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary
+ class="w-full pl-3 pr-10 py-2.5 bg-dark/50 rounded-xl border border-light/5 text-[13px]
+ placeholder:text-light/25 resize-none overflow-hidden
+ focus:outline-none focus:border-light/15
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors {message && hasEmoji(message)
? 'text-transparent caret-light'
: 'text-light'}"
- style="min-height: 48px; max-height: 200px;"
+ style="min-height: 40px; max-height: 160px;"
>
-
-
- {#if showEmojiPicker}
-
- {
- message += emoji;
- inputRef?.focus();
- }}
- onClose={() => (showEmojiPicker = false)}
- position={{ x: 0, y: 0 }}
- />
-
- {/if}
-
+
{#if message.length > 1000}
-
- {message.length} / 4000
+
+
+ {message.length} / 4000
+
{/if}
diff --git a/src/lib/components/matrix/MessageList.svelte b/src/lib/components/matrix/MessageList.svelte
index 734be0f..b84e7ef 100644
--- a/src/lib/components/matrix/MessageList.svelte
+++ b/src/lib/components/matrix/MessageList.svelte
@@ -1,7 +1,5 @@
@@ -324,97 +157,23 @@
-
- {#if onLoadMore}
-
-
-
- {/if}
-
-
{#if allVisibleMessages.length === 0}
-
-
No messages yet
-
Be the first to send a message!
-
- {:else if virtualizer && enableVirtualization}
-
-
- {#each virtualItems as virtualRow (virtualRow.key)}
- {@const message = allVisibleMessages[virtualRow.index]}
- {@const previousMessage =
- virtualRow.index > 0
- ? allVisibleMessages[virtualRow.index - 1]
- : null}
- {@const isGrouped = shouldGroup(message, previousMessage)}
- {@const showDateSeparator = needsDateSeparator(
- message,
- previousMessage,
- )}
-
-
-
- {#if showDateSeparator}
-
-
-
- {formatDateSeparator(message.timestamp)}
-
-
-
- {/if}
-
-
onReact?.(message.eventId, emoji)}
- onToggleReaction={(
- emoji: string,
- reactionEventId: string | null,
- ) => onToggleReaction?.(message.eventId, emoji, reactionEventId)}
- onEdit={() => onEdit?.(message)}
- onDelete={() => onDelete?.(message.eventId)}
- onReply={() => onReply?.(message)}
- onScrollToMessage={scrollToMessage}
- replyPreview={message.replyTo
- ? getReplyPreview(message.replyTo)
- : null}
- />
-
- {/each}
+
No messages yet
+
Be the first to send a message!
{:else}
-
-
+
{#each allVisibleMessages as message, i (message.eventId)}
{@const previousMessage = i > 0 ? allVisibleMessages[i - 1] : null}
{@const isGrouped = shouldGroup(message, previousMessage)}
@@ -425,12 +184,12 @@
{#if showDateSeparator}
-
-
-
+
+
+
{formatDateSeparator(message.timestamp)}
-
+
{/if}
@@ -455,24 +214,19 @@
{/if}
-
+
{#if !shouldAutoScroll && allVisibleMessages.length > 0}
{/if}
diff --git a/src/lib/components/matrix/RoomInfoPanel.svelte b/src/lib/components/matrix/RoomInfoPanel.svelte
index 8b75321..d78e1cf 100644
--- a/src/lib/components/matrix/RoomInfoPanel.svelte
+++ b/src/lib/components/matrix/RoomInfoPanel.svelte
@@ -1,5 +1,5 @@
-
+
-
-
Room Info
+
+
Room Info
-
+
-
-
-
{room.memberCount}
-
Members
+
+
+
{room.memberCount}
+
Members
-
-
- {room.isEncrypted ? "🔒" : "🔓"}
-
-
+
+
+ {room.isEncrypted ? "lock" : "lock_open"}
+
+
{room.isEncrypted ? "Encrypted" : "Not Encrypted"}
-
-
+
+
Details
-
-
+
- Room ID
+ Room ID
{room.roomId}
- Type
- Type
+ {room.isDirect ? "Direct Message" : "Room"}
{#if room.lastActivity}
- Last Activity
- {formatDate(room.lastActivity)}
+ Last Activity
+ {formatDate(room.lastActivity)}
{/if}
@@ -194,20 +157,29 @@
{#if admins.length > 0}
-
+
Admins ({admins.length})
-
+
{#each admins as member}
-
-
- {member.name}
- 👑
+
+ {member.name}
+ shield_person
{/each}
@@ -215,41 +187,55 @@
{/if}
{#if moderators.length > 0}
-
+
Moderators ({moderators.length})
-
+
{#each moderators as member}
-
-
- {member.name}
- 🛡️
+
+ {member.name}
+ shield
{/each}
{/if}
-
-
+
+
Members ({regularMembers.length})
-
+
{#each regularMembers.slice(0, 20) as member}
-
-
- {member.name}
+
+ {member.name}
{/each}
{#if regularMembers.length > 20}
- -
+
-
+{regularMembers.length - 20} more members
{/if}
diff --git a/src/lib/components/matrix/RoomSettingsModal.svelte b/src/lib/components/matrix/RoomSettingsModal.svelte
index a0c6bf6..04510d6 100644
--- a/src/lib/components/matrix/RoomSettingsModal.svelte
+++ b/src/lib/components/matrix/RoomSettingsModal.svelte
@@ -1,5 +1,5 @@
{#if userNames.length > 0}
-
-
-
-
-
-
+
+
+
+
+
{formatTypingText(userNames)}
diff --git a/src/lib/components/matrix/UserProfileModal.svelte b/src/lib/components/matrix/UserProfileModal.svelte
index 9091c8b..395cebb 100644
--- a/src/lib/components/matrix/UserProfileModal.svelte
+++ b/src/lib/components/matrix/UserProfileModal.svelte
@@ -1,12 +1,12 @@
(showActions = true)}
onmouseleave={() => (showActions = false)}
@@ -77,32 +77,22 @@
{#if replyPreview && message.replyTo}