diff --git a/client/src/components/ConversationAvatar.tsx b/client/src/components/ConversationAvatar.tsx index 4dcbcdc419845f25954ba97c11eda79356c38e39..4b48d453e3690e3df333c401a1e11758eae9ce05 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 b0665e5f61cba6eb777b8cb3feda320374888fb8..3c88d42dd9a4591fca616df9d7c213b09079750b 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 99ab4e3aa0c42cc5649e893ff8035a6f35da18f8..348a186b45a113ff9c1558664ff0c712c2ec54ba 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 62d1a0b8d8f5cd11d01d29ca4adcc2e795e0277e..97b236c2a00d4e6a3f9d5cfe19237f423490456e 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 6a43148ce9cef3aa069e155b982fbf50cff7bb94..d0a8fe4f80c2c26ab82de44a949b2e1b9c3f5772 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 d1caa7b703d5d0f9a0520381fb292b959db7c113..509cc23a33bc8a88654fb133c38bd9e0b11228c6 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) +})