mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:41:08 +02:00
✨ feat(matrix-web): add @mention autocomplete, message forwarding, and improved typing indicator
@mention autocomplete: - Detect @ symbol while typing and show user picker - Search room members by name or user ID - Arrow key navigation and Enter/Tab to select - Insert display name into message Message forwarding: - Add forward button to message actions - ForwardMessageDialog with room selection - Multi-select support for forwarding to multiple rooms - Search and filter rooms Typing indicator improvements: - Show user avatars (stacked, up to 3) - Improved visual design with rounded pill for dots - Better spacing and alignment Also adds: - sendMessageToRoom() store method for forwarding
This commit is contained in:
parent
60cc0be10b
commit
7b2ac78032
7 changed files with 408 additions and 14 deletions
|
|
@ -0,0 +1,173 @@
|
|||
<script lang="ts">
|
||||
import { matrixStore, type SimpleMessage, type SimpleRoom } from '$lib/matrix';
|
||||
import { X, MagnifyingGlass, PaperPlaneTilt, User, Users } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
message: SimpleMessage | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { open, message, onClose }: Props = $props();
|
||||
|
||||
let search = $state('');
|
||||
let sending = $state(false);
|
||||
let selectedRooms = $state<Set<string>>(new Set());
|
||||
|
||||
// Filter rooms by search
|
||||
let filteredRooms = $derived(
|
||||
matrixStore.rooms
|
||||
.filter(
|
||||
(room) =>
|
||||
room.membership === 'join' &&
|
||||
room.id !== matrixStore.currentRoomId &&
|
||||
room.name.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
.slice(0, 20)
|
||||
);
|
||||
|
||||
function toggleRoom(roomId: string) {
|
||||
const newSet = new Set(selectedRooms);
|
||||
if (newSet.has(roomId)) {
|
||||
newSet.delete(roomId);
|
||||
} else {
|
||||
newSet.add(roomId);
|
||||
}
|
||||
selectedRooms = newSet;
|
||||
}
|
||||
|
||||
async function handleForward() {
|
||||
if (!message || selectedRooms.size === 0) return;
|
||||
|
||||
sending = true;
|
||||
|
||||
// Forward to each selected room
|
||||
for (const roomId of selectedRooms) {
|
||||
// Create forward message with quote
|
||||
const forwardText = `> ${message.senderName}: ${message.body}\n\nWeitergeleitete Nachricht`;
|
||||
await matrixStore.sendMessageToRoom(roomId, forwardText);
|
||||
}
|
||||
|
||||
sending = false;
|
||||
selectedRooms = new Set();
|
||||
search = '';
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
selectedRooms = new Set();
|
||||
search = '';
|
||||
onClose();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open && message}
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<!-- Dialog -->
|
||||
<div
|
||||
class="w-full max-w-md rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-black/10 dark:border-white/10 px-4 py-3">
|
||||
<h2 class="text-lg font-semibold">Nachricht weiterleiten</h2>
|
||||
<button
|
||||
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
onclick={handleClose}
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Message Preview -->
|
||||
<div class="px-4 py-3 bg-black/5 dark:bg-white/5 border-b border-black/5 dark:border-white/5">
|
||||
<p class="text-xs text-muted-foreground mb-1">Von {message.senderName}</p>
|
||||
<p class="text-sm line-clamp-3">{message.body}</p>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="p-4 border-b border-black/5 dark:border-white/5">
|
||||
<div class="relative">
|
||||
<MagnifyingGlass class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
bind:value={search}
|
||||
placeholder="Chat suchen..."
|
||||
class="w-full pl-10 pr-4 py-2.5 rounded-xl bg-black/5 dark:bg-white/10 border border-black/10 dark:border-white/10
|
||||
text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Room List -->
|
||||
<div class="max-h-64 overflow-y-auto">
|
||||
{#if filteredRooms.length === 0}
|
||||
<p class="px-4 py-8 text-center text-muted-foreground">Keine Chats gefunden</p>
|
||||
{:else}
|
||||
{#each filteredRooms as room (room.id)}
|
||||
<button
|
||||
class="flex items-center gap-3 w-full px-4 py-3 transition-colors text-left
|
||||
{selectedRooms.has(room.id) ? 'bg-violet-500/10' : 'hover:bg-black/5 dark:hover:bg-white/5'}"
|
||||
onclick={() => toggleRoom(room.id)}
|
||||
>
|
||||
<!-- Checkbox -->
|
||||
<div
|
||||
class="w-5 h-5 rounded-md border-2 flex items-center justify-center transition-colors
|
||||
{selectedRooms.has(room.id) ? 'bg-violet-500 border-violet-500' : 'border-black/20 dark:border-white/20'}"
|
||||
>
|
||||
{#if selectedRooms.has(room.id)}
|
||||
<svg class="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Avatar -->
|
||||
<div
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0
|
||||
bg-gradient-to-br from-violet-500 to-purple-600 text-white"
|
||||
>
|
||||
{#if room.avatar}
|
||||
<img src={room.avatar} alt={room.name} class="w-10 h-10 rounded-full object-cover" />
|
||||
{:else if room.isDirect}
|
||||
<User class="w-5 h-5" />
|
||||
{:else}
|
||||
<Users class="w-5 h-5" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Room info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium truncate">{room.name}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{room.isDirect ? 'Direktnachricht' : `${room.memberCount} Mitglieder`}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-between border-t border-black/10 dark:border-white/10 px-4 py-3">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{selectedRooms.size} ausgewählt
|
||||
</p>
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-xl bg-violet-500 hover:bg-violet-600 text-white font-medium transition-colors
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={selectedRooms.size === 0 || sending}
|
||||
onclick={handleForward}
|
||||
>
|
||||
<PaperPlaneTilt class="h-4 w-4" weight="bold" />
|
||||
{sending ? 'Sende...' : 'Weiterleiten'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
import { de } from 'date-fns/locale';
|
||||
import {
|
||||
ArrowBendUpLeft,
|
||||
ArrowBendUpRight,
|
||||
PencilSimple,
|
||||
Trash,
|
||||
DotsThree,
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
showEncryptionBadge?: boolean;
|
||||
onReply?: (message: SimpleMessage) => void;
|
||||
onEdit?: (message: SimpleMessage) => void;
|
||||
onForward?: (message: SimpleMessage) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -36,6 +38,7 @@
|
|||
showEncryptionBadge = false,
|
||||
onReply,
|
||||
onEdit,
|
||||
onForward,
|
||||
}: Props = $props();
|
||||
|
||||
// Check if message is a decryption error (body starts with "Unable to decrypt:")
|
||||
|
|
@ -623,6 +626,13 @@
|
|||
>
|
||||
<ArrowBendUpLeft class="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
title="Weiterleiten"
|
||||
onclick={() => onForward?.(message)}
|
||||
>
|
||||
<ArrowBendUpRight class="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
{#if message.isOwn && message.type === 'm.text'}
|
||||
<button
|
||||
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { matrixStore, type SimpleMessage } from '$lib/matrix';
|
||||
import { matrixStore, type SimpleMessage, type RoomMember } from '$lib/matrix';
|
||||
import {
|
||||
PaperPlaneTilt,
|
||||
Paperclip,
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
CircleNotch,
|
||||
Microphone,
|
||||
Stop,
|
||||
User,
|
||||
} from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -37,6 +38,13 @@
|
|||
let audioChunks: Blob[] = [];
|
||||
let recordingInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// @mention autocomplete state
|
||||
let showMentionPicker = $state(false);
|
||||
let mentionQuery = $state('');
|
||||
let mentionStartPos = $state(0);
|
||||
let mentionResults = $state<RoomMember[]>([]);
|
||||
let selectedMentionIndex = $state(0);
|
||||
|
||||
// Set message content when editing
|
||||
$effect(() => {
|
||||
if (editMessage) {
|
||||
|
|
@ -87,6 +95,76 @@
|
|||
// Reset typing timeout
|
||||
clearTimeout(typingTimeout);
|
||||
typingTimeout = setTimeout(stopTyping, 3000);
|
||||
|
||||
// Check for @mention trigger
|
||||
checkForMention();
|
||||
}
|
||||
|
||||
function checkForMention() {
|
||||
if (!textarea) return;
|
||||
|
||||
const cursorPos = textarea.selectionStart;
|
||||
const textBeforeCursor = message.slice(0, cursorPos);
|
||||
|
||||
// Find the last @ before cursor
|
||||
const lastAtIndex = textBeforeCursor.lastIndexOf('@');
|
||||
|
||||
if (lastAtIndex !== -1) {
|
||||
// Check if there's a space before @ (or it's at the start)
|
||||
const charBefore = lastAtIndex > 0 ? textBeforeCursor[lastAtIndex - 1] : ' ';
|
||||
if (charBefore === ' ' || charBefore === '\n' || lastAtIndex === 0) {
|
||||
const query = textBeforeCursor.slice(lastAtIndex + 1);
|
||||
// No space in the query = still typing the mention
|
||||
if (!query.includes(' ') && query.length <= 50) {
|
||||
mentionStartPos = lastAtIndex;
|
||||
mentionQuery = query;
|
||||
showMentionPicker = true;
|
||||
updateMentionResults(query);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close mention picker if conditions not met
|
||||
showMentionPicker = false;
|
||||
mentionQuery = '';
|
||||
}
|
||||
|
||||
function updateMentionResults(query: string) {
|
||||
const members = matrixStore.getRoomMembers();
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
// Filter members by display name or user ID
|
||||
mentionResults = members
|
||||
.filter(
|
||||
(m) =>
|
||||
m.membership === 'join' &&
|
||||
(m.displayName.toLowerCase().includes(lowerQuery) ||
|
||||
m.userId.toLowerCase().includes(lowerQuery))
|
||||
)
|
||||
.slice(0, 6); // Limit to 6 results
|
||||
|
||||
selectedMentionIndex = 0;
|
||||
}
|
||||
|
||||
function insertMention(member: RoomMember) {
|
||||
const beforeMention = message.slice(0, mentionStartPos);
|
||||
const afterMention = message.slice(textarea.selectionStart);
|
||||
|
||||
// Insert pill format: @displayName (the actual Matrix pill is sent as formatted HTML)
|
||||
const mentionText = `@${member.displayName} `;
|
||||
message = beforeMention + mentionText + afterMention;
|
||||
|
||||
// Close picker
|
||||
showMentionPicker = false;
|
||||
mentionQuery = '';
|
||||
|
||||
// Focus and set cursor position
|
||||
setTimeout(() => {
|
||||
textarea.focus();
|
||||
const newPos = mentionStartPos + mentionText.length;
|
||||
textarea.setSelectionRange(newPos, newPos);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function stopTyping() {
|
||||
|
|
@ -98,6 +176,31 @@
|
|||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
// Handle mention picker navigation
|
||||
if (showMentionPicker && mentionResults.length > 0) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
selectedMentionIndex = (selectedMentionIndex + 1) % mentionResults.length;
|
||||
return;
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
selectedMentionIndex =
|
||||
selectedMentionIndex === 0 ? mentionResults.length - 1 : selectedMentionIndex - 1;
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
insertMention(mentionResults[selectedMentionIndex]);
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
showMentionPicker = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send on Enter (without Shift)
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
|
|
@ -308,6 +411,44 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- @Mention Picker -->
|
||||
{#if showMentionPicker && mentionResults.length > 0}
|
||||
<div
|
||||
class="mb-2 rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 shadow-xl overflow-hidden"
|
||||
>
|
||||
<div class="px-3 py-1.5 text-xs text-muted-foreground border-b border-black/5 dark:border-white/5">
|
||||
Erwähne jemanden
|
||||
</div>
|
||||
{#each mentionResults as member, i}
|
||||
<button
|
||||
class="flex items-center gap-3 w-full px-3 py-2 transition-colors text-left
|
||||
{i === selectedMentionIndex ? 'bg-violet-500/10 dark:bg-violet-500/20' : 'hover:bg-black/5 dark:hover:bg-white/5'}"
|
||||
onclick={() => insertMention(member)}
|
||||
>
|
||||
<!-- Avatar -->
|
||||
{#if member.avatarUrl}
|
||||
<img
|
||||
src={member.avatarUrl}
|
||||
alt={member.displayName}
|
||||
class="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center"
|
||||
>
|
||||
<User class="w-4 h-4 text-white" />
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Name and ID -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-sm truncate">{member.displayName}</p>
|
||||
<p class="text-xs text-muted-foreground truncate">{member.userId}</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Input Area -->
|
||||
<div
|
||||
class="flex items-end gap-2 rounded-2xl bg-white/80 dark:bg-white/10 backdrop-blur-xl border border-black/5 dark:border-white/10 p-2 shadow-lg"
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@
|
|||
interface Props {
|
||||
onReply?: (message: SimpleMessage) => void;
|
||||
onEdit?: (message: SimpleMessage) => void;
|
||||
onForward?: (message: SimpleMessage) => void;
|
||||
}
|
||||
|
||||
let { onReply, onEdit }: Props = $props();
|
||||
let { onReply, onEdit, onForward }: Props = $props();
|
||||
|
||||
// Check if current room is encrypted
|
||||
let isRoomEncrypted = $derived(matrixStore.currentSimpleRoom?.isEncrypted ?? false);
|
||||
|
|
@ -105,6 +106,7 @@
|
|||
showEncryptionBadge={isRoomEncrypted}
|
||||
{onReply}
|
||||
{onEdit}
|
||||
{onForward}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-full flex-col items-center justify-center text-base-content/50">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { matrixStore } from '$lib/matrix';
|
||||
import { User } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
users: string[];
|
||||
}
|
||||
|
||||
let { users }: Props = $props();
|
||||
|
||||
// Get full user info from room members
|
||||
let typingUsers = $derived(() => {
|
||||
const members = matrixStore.getRoomMembers();
|
||||
return users.map((name) => {
|
||||
const member = members.find((m) => m.displayName === name);
|
||||
return {
|
||||
name,
|
||||
avatarUrl: member?.avatarUrl,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
let text = $derived(() => {
|
||||
if (users.length === 0) return '';
|
||||
if (users.length === 1) return `${users[0]} tippt...`;
|
||||
|
|
@ -14,16 +29,36 @@
|
|||
</script>
|
||||
|
||||
{#if users.length > 0}
|
||||
<div class="flex items-center gap-2 px-4 py-2 text-sm text-base-content/60">
|
||||
<div class="flex items-center gap-3 px-4 py-2">
|
||||
<!-- User avatars (stacked) -->
|
||||
<div class="flex -space-x-2">
|
||||
{#each typingUsers().slice(0, 3) as user, i}
|
||||
{#if user.avatarUrl}
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
alt={user.name}
|
||||
class="w-6 h-6 rounded-full border-2 border-white dark:border-zinc-900 object-cover"
|
||||
style="z-index: {3 - i}"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="w-6 h-6 rounded-full border-2 border-white dark:border-zinc-900 bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center"
|
||||
style="z-index: {3 - i}"
|
||||
>
|
||||
<User class="w-3 h-3 text-white" />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Animated dots -->
|
||||
<span class="flex gap-1">
|
||||
<span class="h-2 w-2 animate-bounce rounded-full bg-base-content/40 [animation-delay:0ms]"
|
||||
></span>
|
||||
<span class="h-2 w-2 animate-bounce rounded-full bg-base-content/40 [animation-delay:150ms]"
|
||||
></span>
|
||||
<span class="h-2 w-2 animate-bounce rounded-full bg-base-content/40 [animation-delay:300ms]"
|
||||
></span>
|
||||
</span>
|
||||
<span>{text()}</span>
|
||||
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-black/5 dark:bg-white/10">
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:0ms]"></span>
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:150ms]"></span>
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:300ms]"></span>
|
||||
</div>
|
||||
|
||||
<!-- Text -->
|
||||
<span class="text-sm text-muted-foreground">{text()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -569,6 +569,21 @@ class MatrixStore {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a specific room (for forwarding)
|
||||
*/
|
||||
async sendMessageToRoom(roomId: string, body: string): Promise<boolean> {
|
||||
if (!this._client) return false;
|
||||
|
||||
try {
|
||||
await this._client.sendTextMessage(roomId, body);
|
||||
return true;
|
||||
} catch (err) {
|
||||
this._error = err instanceof Error ? err.message : 'Failed to send message';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send typing indicator
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import CreateRoomDialog from '$lib/components/chat/CreateRoomDialog.svelte';
|
||||
import RoomSettingsPanel from '$lib/components/chat/RoomSettingsPanel.svelte';
|
||||
import SearchDialog from '$lib/components/chat/SearchDialog.svelte';
|
||||
import ForwardMessageDialog from '$lib/components/chat/ForwardMessageDialog.svelte';
|
||||
import { CallView, IncomingCallDialog } from '$lib/components/call';
|
||||
import { ChatCircle, Plus, Gear } from '@manacore/shared-icons';
|
||||
import { browser } from '$app/environment';
|
||||
|
|
@ -17,10 +18,12 @@
|
|||
let showCreateRoom = $state(false);
|
||||
let showRoomSettings = $state(false);
|
||||
let showSearch = $state(false);
|
||||
let showForward = $state(false);
|
||||
|
||||
// Reply/Edit state
|
||||
// Reply/Edit/Forward state
|
||||
let replyTo = $state<SimpleMessage | null>(null);
|
||||
let editMessage = $state<SimpleMessage | null>(null);
|
||||
let forwardMessage = $state<SimpleMessage | null>(null);
|
||||
|
||||
// Check if mobile
|
||||
let isMobile = $state(browser ? window.innerWidth < 1024 : false);
|
||||
|
|
@ -71,6 +74,11 @@
|
|||
editMessage = message;
|
||||
}
|
||||
|
||||
function handleForward(message: SimpleMessage) {
|
||||
forwardMessage = message;
|
||||
showForward = true;
|
||||
}
|
||||
|
||||
function handleRoomCreated(roomId: string) {
|
||||
matrixStore.selectRoom(roomId);
|
||||
}
|
||||
|
|
@ -174,7 +182,7 @@
|
|||
/>
|
||||
|
||||
<!-- Timeline -->
|
||||
<Timeline onReply={handleReply} onEdit={handleEdit} />
|
||||
<Timeline onReply={handleReply} onEdit={handleEdit} onForward={handleForward} />
|
||||
|
||||
<!-- Message Input -->
|
||||
<MessageInput
|
||||
|
|
@ -244,3 +252,13 @@
|
|||
{#if incomingCall && !activeCall}
|
||||
<IncomingCallDialog call={incomingCall} onAnswer={handleCallAnswer} onReject={handleCallReject} />
|
||||
{/if}
|
||||
|
||||
<!-- Forward Message Dialog -->
|
||||
<ForwardMessageDialog
|
||||
open={showForward}
|
||||
message={forwardMessage}
|
||||
onClose={() => {
|
||||
showForward = false;
|
||||
forwardMessage = null;
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue