feat(matrix-web): add screen sharing, presence display, and extended emoji picker

Screen sharing:
- Add toggleScreenShare() method to Matrix store
- Add screen share button to CallView with Screencast icon
- Show visual indicator when screen sharing is active

Presence display:
- Add online/offline indicator to RoomHeader for DMs
- Show presence status text (Online, Vor X Min. aktiv, etc.)
- Green dot indicator with tooltip

Extended emoji picker:
- Add 6 emoji categories (Häufig, Smileys, Gesten, Symbole, Tiere, Essen)
- Quick emoji bar for common reactions
- Expandable full picker with category tabs
- Grid layout for easy browsing
This commit is contained in:
Till-JS 2026-01-29 22:14:28 +01:00
parent b5fa0f42b6
commit 0023394074
4 changed files with 171 additions and 25 deletions

View file

@ -6,6 +6,7 @@
MicrophoneSlash,
VideoCamera,
VideoCameraSlash,
Screencast,
User,
} from '@manacore/shared-icons';
import { onDestroy } from 'svelte';
@ -66,6 +67,10 @@
matrixStore.toggleCameraMute();
}
async function handleScreenShare() {
await matrixStore.toggleScreenShare();
}
function handleHangup() {
matrixStore.hangupCall();
onHangup?.();
@ -108,8 +113,14 @@
{/if}
<div>
<p class="font-medium text-white">{call.opponentName || 'Unbekannt'}</p>
<p class="text-sm text-white/70">
{call.type === 'video' ? 'Videoanruf' : 'Sprachanruf'} · {getStateText(call.state)}
<p class="text-sm text-white/70 flex items-center gap-2">
<span>{call.type === 'video' ? 'Videoanruf' : 'Sprachanruf'} · {getStateText(call.state)}</span>
{#if call.isScreenSharing}
<span class="flex items-center gap-1 px-2 py-0.5 bg-violet-500/30 rounded-full text-violet-300 text-xs">
<Screencast class="w-3 h-3" />
Bildschirmfreigabe
</span>
{/if}
</p>
</div>
</div>
@ -189,6 +200,16 @@
<VideoCamera class="w-6 h-6 text-white" />
{/if}
</button>
<!-- Screen share -->
<button
class="w-14 h-14 rounded-full flex items-center justify-center transition-colors
{call.isScreenSharing ? 'bg-violet-500 hover:bg-violet-600' : 'bg-white/20 hover:bg-white/30'}"
onclick={handleScreenShare}
title={call.isScreenSharing ? 'Bildschirmfreigabe beenden' : 'Bildschirm freigeben'}
>
<Screencast class="w-6 h-6 text-white" />
</button>
{/if}
<!-- Hang up -->

View file

@ -48,9 +48,22 @@
let imageLoading = $state(true);
let imageError = $state(false);
// Quick reaction emojis
// Quick reaction emojis (always visible)
const quickEmojis = ['👍', '❤️', '😂', '😮', '😢', '🎉'];
// Extended emoji categories for full picker
const emojiCategories = [
{ name: 'Häufig', emojis: ['👍', '👎', '❤️', '😂', '😮', '😢', '🎉', '🔥', '💯', '✨'] },
{ name: 'Smileys', emojis: ['😀', '😃', '😄', '😁', '😆', '🥹', '😅', '🤣', '😊', '😇', '🙂', '😉', '😌', '😍', '🥰', '😘'] },
{ name: 'Gesten', emojis: ['👏', '🙌', '👐', '🤝', '🙏', '✌️', '🤞', '🤟', '🤘', '👌', '🤌', '👋', '💪', '👀'] },
{ name: 'Symbole', emojis: ['✅', '❌', '⭐', '💫', '🌟', '💡', '🎯', '🚀', '💎', '🏆', '🔑', '📌', '🔔', '💬'] },
{ name: 'Tiere', emojis: ['🐱', '🐶', '🐻', '🦊', '🐼', '🐨', '🦁', '🐸', '🐵', '🦄', '🐝', '🦋'] },
{ name: 'Essen', emojis: ['🍕', '🍔', '🍟', '🌮', '🍜', '🍣', '🍦', '🍩', '🍪', '☕', '🍺', '🍷'] },
];
let showFullPicker = $state(false);
let selectedCategory = $state(0);
async function handleReaction(emoji: string) {
showEmojiPicker = false;
await matrixStore.reactToMessage(message.id, emoji);
@ -539,23 +552,67 @@
<!-- Emoji picker backdrop -->
<button
class="fixed inset-0 z-40"
onclick={() => (showEmojiPicker = false)}
onclick={() => {
showEmojiPicker = false;
showFullPicker = false;
}}
aria-label="Schließen"
></button>
<!-- Emoji picker dropdown -->
<div
class="absolute z-50 flex gap-1 rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 p-2 shadow-xl
class="absolute z-50 rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 shadow-xl
left-0 top-full mt-2 lg:bottom-full lg:top-auto lg:mt-0 lg:mb-2
{message.isOwn ? 'lg:right-0 lg:left-auto' : ''}"
{message.isOwn ? 'lg:right-0 lg:left-auto' : ''}
{showFullPicker ? 'w-72' : ''}"
>
{#each quickEmojis as emoji}
<button
class="text-xl hover:scale-125 transition-transform p-1"
onclick={() => handleReaction(emoji)}
>
{emoji}
</button>
{/each}
{#if showFullPicker}
<!-- Full emoji picker with categories -->
<div class="p-2">
<!-- Category tabs -->
<div class="flex gap-1 mb-2 border-b border-black/10 dark:border-white/10 pb-2 overflow-x-auto">
{#each emojiCategories as category, i}
<button
class="px-2 py-1 text-xs rounded-md whitespace-nowrap transition-colors
{selectedCategory === i ? 'bg-violet-500 text-white' : 'hover:bg-black/5 dark:hover:bg-white/10 text-muted-foreground'}"
onclick={() => (selectedCategory = i)}
>
{category.name}
</button>
{/each}
</div>
<!-- Emoji grid -->
<div class="grid grid-cols-8 gap-1 max-h-40 overflow-y-auto">
{#each emojiCategories[selectedCategory].emojis as emoji}
<button
class="text-xl hover:scale-110 hover:bg-black/5 dark:hover:bg-white/10 rounded p-1 transition-all"
onclick={() => handleReaction(emoji)}
>
{emoji}
</button>
{/each}
</div>
</div>
{:else}
<!-- Quick emoji bar -->
<div class="flex items-center gap-1 p-2">
{#each quickEmojis as emoji}
<button
class="text-xl hover:scale-125 transition-transform p-1"
onclick={() => handleReaction(emoji)}
>
{emoji}
</button>
{/each}
<!-- Expand button -->
<button
class="ml-1 p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
onclick={() => (showFullPicker = true)}
title="Mehr Emojis"
>
<DotsThree class="h-4 w-4 text-muted-foreground" />
</button>
</div>
{/if}
</div>
{/if}
</div>

View file

@ -45,6 +45,23 @@
});
}
});
// Presence for DMs
let isOnline = $derived(room?.isDirect && room?.presence === 'online');
// Format last active time
let presenceText = $derived(() => {
if (!room?.isDirect) return '';
if (room.presence === 'online') return 'Online';
if (!room.lastActiveAgo) return 'Offline';
const minutes = Math.floor(room.lastActiveAgo / 60000);
if (minutes < 1) return 'Gerade aktiv';
if (minutes < 60) return `Vor ${minutes} Min. aktiv`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `Vor ${hours} Std. aktiv`;
const days = Math.floor(hours / 24);
return `Vor ${days} Tag${days > 1 ? 'en' : ''} aktiv`;
});
</script>
{#if room}
@ -59,15 +76,25 @@
<List class="h-5 w-5" />
</button>
<!-- Room avatar -->
<div
class="flex h-10 w-10 items-center justify-center rounded-full shadow-md
bg-gradient-to-br from-violet-500 to-purple-600 text-white"
>
{#if room.avatar}
<img src={room.avatar} alt={room.name} class="h-10 w-10 rounded-full object-cover" />
{:else}
<span class="text-sm font-semibold">{room.name.charAt(0).toUpperCase()}</span>
<!-- Room avatar with online indicator -->
<div class="relative flex-shrink-0">
<div
class="flex h-10 w-10 items-center justify-center rounded-full shadow-md
bg-gradient-to-br from-violet-500 to-purple-600 text-white"
>
{#if room.avatar}
<img src={room.avatar} alt={room.name} class="h-10 w-10 rounded-full object-cover" />
{:else}
<span class="text-sm font-semibold">{room.name.charAt(0).toUpperCase()}</span>
{/if}
</div>
<!-- Online indicator for DMs -->
{#if room.isDirect}
<div
class="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-white dark:border-zinc-900
{isOnline ? 'bg-green-500' : 'bg-zinc-400 dark:bg-zinc-600'}"
title={presenceText()}
></div>
{/if}
</div>
@ -94,11 +121,19 @@
</div>
{/if}
</div>
<p class="flex items-center gap-1 text-sm text-muted-foreground">
<p class="flex items-center gap-1.5 text-sm text-muted-foreground">
{#if room.topic}
<span class="truncate">{room.topic}</span>
{:else if room.isDirect}
<span>Direktnachricht</span>
<span class="flex items-center gap-1.5">
{#if isOnline}
<span class="w-2 h-2 rounded-full bg-green-500"></span>
<span class="text-green-600 dark:text-green-400">Online</span>
{:else}
<span class="w-2 h-2 rounded-full bg-zinc-400"></span>
<span>{presenceText() || 'Offline'}</span>
{/if}
</span>
{:else}
<Users class="h-3 w-3" />
<span>{room.memberCount} Mitglieder</span>

View file

@ -1485,6 +1485,39 @@ class MatrixStore {
}
}
/**
* Toggle screen sharing
*/
async toggleScreenShare(): Promise<boolean> {
if (!this._matrixCall || !this._activeCall) return false;
try {
const isSharing = this._activeCall.isScreenSharing;
if (isSharing) {
// Stop screen sharing - switch back to camera
await this._matrixCall.setScreensharingEnabled(false);
this._activeCall = { ...this._activeCall, isScreenSharing: false };
} else {
// Start screen sharing
const success = await this._matrixCall.setScreensharingEnabled(true, {
audio: true, // Include system audio if available
});
if (success) {
this._activeCall = { ...this._activeCall, isScreenSharing: true };
} else {
console.warn('Screen sharing was denied or failed');
return false;
}
}
return true;
} catch (err) {
console.error('Error toggling screen share:', err);
this._error = 'Bildschirmfreigabe fehlgeschlagen';
return false;
}
}
/**
* Set up call event handlers
*/