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