feat: add user color customization and metadata handling
- Implemented `useUserColor` hook to manage user profile color stored in avatar PNG metadata. - Added `useOtherUserColor` hook to fetch and cache colors from other users' avatars. - Created utility functions for reading and writing PNG metadata, enabling color embedding and extraction. - Refactored `SearchResultGroup`, `RoomInput`, `Message`, `RoomPinMenu`, and `Notifications` components to utilize user color for display. - Introduced `UserColorPicker` component in settings for users to select and save their profile color. - Enhanced notification and message rendering to prioritize custom user colors over legacy colors.
This commit is contained in:
@@ -31,6 +31,16 @@ type CallEventListener = (event: CallEvent) => void;
|
||||
/** The Matrix state event type for call membership (MSC3401) */
|
||||
const CALL_MEMBER_EVENT_TYPE = 'org.matrix.msc3401.call.member';
|
||||
|
||||
/**
|
||||
* Service for managing Matrix RTC calls via LiveKit
|
||||
* Handles LiveKit connections, media streams, call state, and Matrix room signaling
|
||||
*/
|
||||
/** How often to refresh call membership to prevent expiry (45 minutes) */
|
||||
const MEMBERSHIP_REFRESH_INTERVAL_MS = 45 * 60 * 1000;
|
||||
|
||||
/** Call membership expiry time (1 hour) */
|
||||
const MEMBERSHIP_EXPIRY_MS = 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Service for managing Matrix RTC calls via LiveKit
|
||||
* Handles LiveKit connections, media streams, call state, and Matrix room signaling
|
||||
@@ -48,6 +58,12 @@ export class CallService {
|
||||
|
||||
private livekitJwt: LiveKitJWTResponse | null = null;
|
||||
|
||||
private membershipRefreshInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
private currentCallId: string | null = null;
|
||||
|
||||
private wasMutedBeforeDeafen = false;
|
||||
|
||||
/**
|
||||
* Creates a new CallService instance
|
||||
* @param config - Service configuration including homeserver and LiveKit URLs
|
||||
@@ -137,7 +153,7 @@ export class CallService {
|
||||
{
|
||||
device_id: this.config.deviceId,
|
||||
session_id: callId,
|
||||
expires_ts: Date.now() + 1000 * 60 * 60, // 1 hour expiry
|
||||
expires_ts: Date.now() + MEMBERSHIP_EXPIRY_MS,
|
||||
feeds: [
|
||||
{ purpose: 'usermedia' },
|
||||
],
|
||||
@@ -170,6 +186,44 @@ export class CallService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts periodic refresh of call membership to prevent expiry
|
||||
* Refreshes every 45 minutes to ensure the 1 hour expiry is never reached
|
||||
* @param roomId - The Matrix room ID
|
||||
* @param callId - The unique call ID
|
||||
*/
|
||||
private startMembershipRefresh(roomId: string, callId: string): void {
|
||||
this.stopMembershipRefresh();
|
||||
|
||||
this.membershipRefreshInterval = setInterval(async () => {
|
||||
if (!this.activeCall || this.activeCall.roomId !== roomId) {
|
||||
this.stopMembershipRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Refreshing call membership to prevent expiry');
|
||||
await this.sendCallMemberEvent(roomId, callId, true);
|
||||
console.log('Call membership refreshed successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh call membership:', error);
|
||||
}
|
||||
}, MEMBERSHIP_REFRESH_INTERVAL_MS);
|
||||
|
||||
console.log('Started call membership refresh interval (every 45 minutes)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the membership refresh interval
|
||||
*/
|
||||
private stopMembershipRefresh(): void {
|
||||
if (this.membershipRefreshInterval) {
|
||||
clearInterval(this.membershipRefreshInterval);
|
||||
this.membershipRefreshInterval = null;
|
||||
console.log('Stopped call membership refresh interval');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a call in the specified room
|
||||
* @param roomId - The Matrix room ID to start the call in
|
||||
@@ -271,6 +325,10 @@ export class CallService {
|
||||
// Step 5: Send Matrix state event to announce participation
|
||||
// This state event will be rendered in the timeline as a "joined the call" notification
|
||||
await this.sendCallMemberEvent(roomId, callId, true);
|
||||
this.currentCallId = callId;
|
||||
|
||||
// Step 6: Start periodic membership refresh to prevent expiry after 1 hour
|
||||
this.startMembershipRefresh(roomId, callId);
|
||||
|
||||
console.log('Announced call participation to Matrix room');
|
||||
|
||||
@@ -651,6 +709,9 @@ export class CallService {
|
||||
const { roomId } = this.activeCall;
|
||||
this.setState(CallState.Disconnecting);
|
||||
|
||||
// Stop the membership refresh interval
|
||||
this.stopMembershipRefresh();
|
||||
|
||||
// Stop any dialing loop and play departure sound
|
||||
getCallSounds().stopDialingLoop();
|
||||
getCallSounds().playSound(CallSoundType.CallDepart);
|
||||
@@ -674,6 +735,7 @@ export class CallService {
|
||||
|
||||
this.activeCall = null;
|
||||
this.livekitJwt = null;
|
||||
this.currentCallId = null;
|
||||
|
||||
this.emit({
|
||||
type: CallEventType.StateChanged,
|
||||
@@ -712,6 +774,9 @@ export class CallService {
|
||||
this.livekitRoom.localParticipant.setMetadata(metadata).catch((err) => {
|
||||
console.warn('Failed to set participant metadata:', err);
|
||||
});
|
||||
|
||||
// Play undeafen sound
|
||||
getCallSounds().playSound(CallSoundType.Undeafen);
|
||||
}
|
||||
|
||||
this.livekitRoom.localParticipant.setMicrophoneEnabled(!newMuteState);
|
||||
@@ -749,16 +814,23 @@ export class CallService {
|
||||
});
|
||||
});
|
||||
|
||||
// Also mute/unmute our mic when deafening (silently - no mute sound)
|
||||
// Only change mic state if it doesn't match deafen state
|
||||
if (newDeafenState && !this.activeCall.isMuted) {
|
||||
// Deafening - also mute mic
|
||||
this.livekitRoom.localParticipant.setMicrophoneEnabled(false);
|
||||
this.activeCall.isMuted = true;
|
||||
} else if (!newDeafenState && this.activeCall.isMuted) {
|
||||
// Undeafening - also unmute mic
|
||||
this.livekitRoom.localParticipant.setMicrophoneEnabled(true);
|
||||
this.activeCall.isMuted = false;
|
||||
// Handle mic muting when deafening
|
||||
if (newDeafenState) {
|
||||
// Remember if user was muted before deafening
|
||||
this.wasMutedBeforeDeafen = this.activeCall.isMuted;
|
||||
|
||||
// Deafening - always mute mic
|
||||
if (!this.activeCall.isMuted) {
|
||||
this.livekitRoom.localParticipant.setMicrophoneEnabled(false);
|
||||
this.activeCall.isMuted = true;
|
||||
}
|
||||
} else {
|
||||
// Undeafening - only unmute if user wasn't muted before deafening
|
||||
if (!this.wasMutedBeforeDeafen && this.activeCall.isMuted) {
|
||||
this.livekitRoom.localParticipant.setMicrophoneEnabled(true);
|
||||
this.activeCall.isMuted = false;
|
||||
}
|
||||
this.wasMutedBeforeDeafen = false;
|
||||
}
|
||||
|
||||
// Broadcast deafen state to other participants via metadata
|
||||
@@ -943,6 +1015,7 @@ export class CallService {
|
||||
* Cleans up resources
|
||||
*/
|
||||
dispose(): void {
|
||||
this.stopMembershipRefresh();
|
||||
this.endCall();
|
||||
this.listeners.clear();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user