From 08b8ceae475fc0afff60fdcf5dd26689a9bcab19 Mon Sep 17 00:00:00 2001 From: leo <leopold@lchappuis.fr> Date: Wed, 12 Feb 2025 13:28:24 -0500 Subject: [PATCH] presence: add WebSocket and UI component Add a WebSocket and UI component. While the WebSocket is not yet fully implemented, it can receive information from the server. The frontend logic still needs to be developed. Change-Id: Icaca360565e2ab19fd3b030c4d7250b71ba531ad --- client/src/components/ConversationAvatar.tsx | 46 ++++++++++++++++++- common/src/enums/websocket-message-type.ts | 3 ++ common/src/interfaces/websocket-interfaces.ts | 7 +++ common/src/interfaces/websocket-message.ts | 4 ++ server/src/jamid/jamid.ts | 2 + server/src/routers/account-router.ts | 9 ++++ 6 files changed, 69 insertions(+), 2 deletions(-) diff --git a/client/src/components/ConversationAvatar.tsx b/client/src/components/ConversationAvatar.tsx index 4dcbcdc4..4b48d453 100644 --- a/client/src/components/ConversationAvatar.tsx +++ b/client/src/components/ConversationAvatar.tsx @@ -18,7 +18,7 @@ import Check from '@mui/icons-material/Check' import RefreshOutlined from '@mui/icons-material/RefreshOutlined' import { Avatar, AvatarProps, Box, Dialog, Stack, Typography, useTheme } from '@mui/material' -import { useCallback, useEffect, useState } from 'react' +import { ReactNode, useCallback, useEffect, useState } from 'react' import { useDropzone } from 'react-dropzone' import { useTranslation } from 'react-i18next' @@ -44,9 +44,10 @@ import { CircleMaskOverlay, FileDragOverlay } from './Overlay' type ConversationAvatarProps = AvatarProps & { displayName: string contactUri: string + presence?: boolean } -export default function ConversationAvatar({ displayName, contactUri, ...props }: ConversationAvatarProps) { +export default function ConversationAvatar({ displayName, contactUri, presence, ...props }: ConversationAvatarProps) { const profilePictureQuery = useContactProfilePicture(contactUri) const url = profilePictureQuery.data const src = props.src || url || '/broken' @@ -58,6 +59,19 @@ export default function ConversationAvatar({ displayName, contactUri, ...props } document.getElementById('avatar' + contactUri)?.setAttribute('src', updatedSrc) }, [contactUri, props.src, url]) + if (presence) { + return ( + <ConversationAvatarPresence> + <Avatar + id={'avatar' + contactUri} + {...props} + alt={displayName?.toUpperCase()} + src={src} + style={{ backgroundColor: color, color: '#E5E5E5' }} + /> + </ConversationAvatarPresence> + ) + } return ( <Avatar id={'avatar' + contactUri} @@ -69,6 +83,34 @@ export default function ConversationAvatar({ displayName, contactUri, ...props } ) } +const ConversationAvatarPresence = ({ children }: { children: ReactNode }) => { + return ( + <Box sx={{ display: 'flex', flexDirection: 'column', width: 'auto', height: 'auto' }}> + {children} + <Box + sx={{ + display: 'flex', + width: '100%', + justifyContent: 'flex-end', + marginTop: '-7px', + marginLeft: '-3px', + zIndex: '100', + }} + > + <Box + sx={{ + width: '10px', + height: '10px', + backgroundColor: 'red', + borderRadius: '50%', + border: '2px solid', + }} + /> + </Box> + </Box> + ) +} + export const AvatarEditor = () => { const { t } = useTranslation() const { account } = useAuthContext() diff --git a/common/src/enums/websocket-message-type.ts b/common/src/enums/websocket-message-type.ts index b0665e5f..3c88d42d 100644 --- a/common/src/enums/websocket-message-type.ts +++ b/common/src/enums/websocket-message-type.ts @@ -43,4 +43,7 @@ export enum WebSocketMessageType { onWebRtcDescription = 'onWebRtcDescription', sendWebRtcIceCandidate = 'sendWebRtcIceCandidate', onWebRtcIceCandidate = 'onWebRtcIceCandidate', + + // Presence + NewBuddyNotification = 'new-buddy-notification', } diff --git a/common/src/interfaces/websocket-interfaces.ts b/common/src/interfaces/websocket-interfaces.ts index 99ab4e3a..348a186b 100644 --- a/common/src/interfaces/websocket-interfaces.ts +++ b/common/src/interfaces/websocket-interfaces.ts @@ -135,3 +135,10 @@ export interface WebRtcSdp extends CallAction { export interface WebRtcIceCandidate extends CallAction { candidate: RTCIceCandidate } + +export interface NewBuddyNotification { + accountId: string + buddyUri: string + status: number + lineStatus: string +} diff --git a/common/src/interfaces/websocket-message.ts b/common/src/interfaces/websocket-message.ts index 62d1a0b8..97b236c2 100644 --- a/common/src/interfaces/websocket-message.ts +++ b/common/src/interfaces/websocket-message.ts @@ -31,6 +31,7 @@ import { IKnownDevicesChanged, LoadMoreMessages, LoadSwarmUntil, + NewBuddyNotification, ProfileReceived, ReactionAdded, ReactionRemoved, @@ -68,6 +69,9 @@ export interface WebSocketMessageTable { [WebSocketMessageType.onWebRtcDescription]: WebRtcSdp & WithSender [WebSocketMessageType.sendWebRtcIceCandidate]: WebRtcIceCandidate & WithReceiver [WebSocketMessageType.onWebRtcIceCandidate]: WebRtcIceCandidate & WithSender + + // Presence + [WebSocketMessageType.NewBuddyNotification]: NewBuddyNotification } export interface WebSocketMessage<T extends WebSocketMessageType> { diff --git a/server/src/jamid/jamid.ts b/server/src/jamid/jamid.ts index 6a43148c..d0a8fe4f 100644 --- a/server/src/jamid/jamid.ts +++ b/server/src/jamid/jamid.ts @@ -772,6 +772,7 @@ export class Jamid { this.jamiSwig.answerServerRequest(uri, flag) } + // false to stop tracking, true to start tracking subscribeBuddy(accountId: string, uri: string, flag: boolean): void { this.jamiSwig.subscribeBuddy(accountId, uri, flag) } @@ -795,6 +796,7 @@ export class Jamid { this.events.onNewBuddyNotification.subscribe((signal) => { log.debug('Received NewBuddyNotification:', JSON.stringify(signal)) + this.webSocketServer.send(signal.accountId, WebSocketMessageType.NewBuddyNotification, signal) }) this.events.onNewServerSubscriptionRequest.subscribe((signal) => { diff --git a/server/src/routers/account-router.ts b/server/src/routers/account-router.ts index d1caa7b7..509cc23a 100644 --- a/server/src/routers/account-router.ts +++ b/server/src/routers/account-router.ts @@ -163,3 +163,12 @@ accountRouter.post('/displayName', (req, res) => { jamid.updateProfile(accountId, displayName, '', '', 10) res.sendStatus(HttpStatusCode.NoContent) }) + +accountRouter.post('/subscribe', (req, res) => { + const accountId = res.locals.accountId + const subscribe = req.body.subscribe + const enable = req.body.enable + + jamid.subscribeBuddy(accountId, subscribe, enable) + res.sendStatus(HttpStatusCode.Ok) +}) -- GitLab