mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:41:08 +02:00
feat(matrix-web): add online status indicators for DMs
- Add PresenceState and UserPresence types - Track user presence via Matrix SDK events - Display green dot indicator for online users in DM list - Show gray dot for offline users - Add tooltip with last active time Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c4483e2c0b
commit
84fca4036e
3 changed files with 128 additions and 12 deletions
|
|
@ -27,6 +27,22 @@
|
|||
.substring(0, 2)
|
||||
.toUpperCase()
|
||||
);
|
||||
|
||||
// Online status for DMs
|
||||
let isOnline = $derived(room.isDirect && room.presence === 'online');
|
||||
|
||||
// Format last active time
|
||||
let lastActiveText = $derived(() => {
|
||||
if (!room.isDirect || !room.lastActiveAgo) return '';
|
||||
if (room.presence === 'online') return 'Online';
|
||||
const minutes = Math.floor(room.lastActiveAgo / 60000);
|
||||
if (minutes < 1) return 'Gerade aktiv';
|
||||
if (minutes < 60) return `Vor ${minutes} Min.`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `Vor ${hours} Std.`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `Vor ${days} Tag${days > 1 ? 'en' : ''}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<button
|
||||
|
|
@ -36,17 +52,27 @@
|
|||
: 'hover:bg-white/60 dark:hover:bg-white/5 hover:-translate-y-0.5'}"
|
||||
{onclick}
|
||||
>
|
||||
<!-- Avatar -->
|
||||
<div
|
||||
class="w-11 h-11 rounded-full flex items-center justify-center flex-shrink-0 shadow-sm
|
||||
{selected
|
||||
? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white'
|
||||
: 'bg-gradient-to-br from-violet-500 to-purple-600 text-white'}"
|
||||
>
|
||||
{#if room.avatar}
|
||||
<img src={room.avatar} alt={room.name} class="w-11 h-11 rounded-full object-cover" />
|
||||
{:else}
|
||||
<span class="text-sm font-semibold">{initials}</span>
|
||||
<!-- Avatar with online indicator -->
|
||||
<div class="relative flex-shrink-0">
|
||||
<div
|
||||
class="w-11 h-11 rounded-full flex items-center justify-center shadow-sm
|
||||
{selected
|
||||
? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white'
|
||||
: 'bg-gradient-to-br from-violet-500 to-purple-600 text-white'}"
|
||||
>
|
||||
{#if room.avatar}
|
||||
<img src={room.avatar} alt={room.name} class="w-11 h-11 rounded-full object-cover" />
|
||||
{:else}
|
||||
<span class="text-sm font-semibold">{initials}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Online indicator dot -->
|
||||
{#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={lastActiveText()}
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import type {
|
|||
VerificationRequest,
|
||||
CryptoCallbacks,
|
||||
CrossSigningStatus,
|
||||
PresenceState,
|
||||
UserPresence,
|
||||
} from './types';
|
||||
|
||||
const STORAGE_KEY = 'matrix_credentials';
|
||||
|
|
@ -30,6 +32,7 @@ class MatrixStore {
|
|||
private _currentRoomId = $state<string | null>(null);
|
||||
private _timeline = $state<MatrixEvent[]>([]);
|
||||
private _typingUsers = $state<Map<string, string[]>>(new Map());
|
||||
private _userPresence = $state<Map<string, UserPresence>>(new Map());
|
||||
private _error = $state<string | null>(null);
|
||||
private _initialized = $state(false);
|
||||
|
||||
|
|
@ -77,6 +80,21 @@ class MatrixStore {
|
|||
return this._crossSigningReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get presence for a specific user
|
||||
*/
|
||||
getUserPresence(userId: string): UserPresence | undefined {
|
||||
return this._userPresence.get(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is currently online
|
||||
*/
|
||||
isUserOnline(userId: string): boolean {
|
||||
const presence = this._userPresence.get(userId);
|
||||
return presence?.presence === 'online' || presence?.currentlyActive === true;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// Derived State
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
|
@ -260,6 +278,28 @@ class MatrixStore {
|
|||
// Trigger reactivity for room updates
|
||||
this._rooms = this._client!.getRooms();
|
||||
});
|
||||
|
||||
// User presence changes
|
||||
this._client.on(sdk.UserEvent.Presence, (event, user) => {
|
||||
if (!user) return;
|
||||
|
||||
const userId = user.userId;
|
||||
const presence: UserPresence = {
|
||||
userId,
|
||||
presence: (user.presence as PresenceState) || 'offline',
|
||||
lastActiveAgo: user.lastActiveAgo,
|
||||
statusMessage: user.presenceStatusMsg,
|
||||
currentlyActive: user.currentlyActive,
|
||||
};
|
||||
|
||||
// Trigger reactivity by creating new Map
|
||||
const newMap = new Map(this._userPresence);
|
||||
newMap.set(userId, presence);
|
||||
this._userPresence = newMap;
|
||||
|
||||
// Also trigger room list update for DMs
|
||||
this._rooms = this._client!.getRooms();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1199,6 +1239,33 @@ class MatrixStore {
|
|||
}
|
||||
}
|
||||
|
||||
// Get DM user presence info
|
||||
const isDirect = this.isDirectRoom(room);
|
||||
let dmUserId: string | undefined;
|
||||
let presence: SimpleRoom['presence'];
|
||||
let lastActiveAgo: number | undefined;
|
||||
|
||||
if (isDirect && myUserId) {
|
||||
// Find the other user in the DM
|
||||
const members = room.getJoinedMembers();
|
||||
const otherMember = members.find((m) => m.userId !== myUserId);
|
||||
if (otherMember) {
|
||||
dmUserId = otherMember.userId;
|
||||
const userPresence = this._userPresence.get(dmUserId);
|
||||
if (userPresence) {
|
||||
presence = userPresence.presence;
|
||||
lastActiveAgo = userPresence.lastActiveAgo;
|
||||
} else {
|
||||
// Try to get from user object directly
|
||||
const user = this._client?.getUser(dmUserId);
|
||||
if (user) {
|
||||
presence = (user.presence as SimpleRoom['presence']) || 'offline';
|
||||
lastActiveAgo = user.lastActiveAgo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: room.roomId,
|
||||
name: room.name || 'Unnamed Room',
|
||||
|
|
@ -1209,11 +1276,14 @@ class MatrixStore {
|
|||
lastMessageTime: room.getLastActiveTimestamp() || undefined,
|
||||
unreadCount: room.getUnreadNotificationCount('total' as any) || 0,
|
||||
highlightCount: room.getUnreadNotificationCount('highlight' as any) || 0,
|
||||
isDirect: this.isDirectRoom(room),
|
||||
isDirect,
|
||||
isEncrypted: room.hasEncryptionStateEvent(),
|
||||
memberCount: room.getJoinedMemberCount(),
|
||||
membership,
|
||||
inviter,
|
||||
dmUserId,
|
||||
presence,
|
||||
lastActiveAgo,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,22 @@ export type MessageType =
|
|||
*/
|
||||
export type RoomMembership = 'join' | 'invite' | 'leave' | 'ban' | 'knock';
|
||||
|
||||
/**
|
||||
* User presence state
|
||||
*/
|
||||
export type PresenceState = 'online' | 'offline' | 'unavailable';
|
||||
|
||||
/**
|
||||
* User presence info
|
||||
*/
|
||||
export interface UserPresence {
|
||||
userId: string;
|
||||
presence: PresenceState;
|
||||
lastActiveAgo?: number; // milliseconds since last active
|
||||
statusMessage?: string;
|
||||
currentlyActive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified room for UI rendering
|
||||
*/
|
||||
|
|
@ -91,6 +107,10 @@ export interface SimpleRoom {
|
|||
memberCount: number;
|
||||
membership: RoomMembership;
|
||||
inviter?: string; // User who sent the invite
|
||||
// Presence info for DMs
|
||||
dmUserId?: string; // The other user's ID in a DM
|
||||
presence?: PresenceState;
|
||||
lastActiveAgo?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue