mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 02:09:39 +02:00
✨ 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:
parent
b5fa0f42b6
commit
0023394074
4 changed files with 171 additions and 25 deletions
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue