diff --git a/client/src/components/ConversationAvatar.tsx b/client/src/components/ConversationAvatar.tsx
index 4b48d453e3690e3df333c401a1e11758eae9ce05..17745fa0da5785ef0fd41f4b3c74291919cb03e6 100644
--- a/client/src/components/ConversationAvatar.tsx
+++ b/client/src/components/ConversationAvatar.tsx
@@ -18,11 +18,12 @@
 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 { ReactNode, useCallback, useEffect, useState } from 'react'
+import { ReactNode, useCallback, useContext, useEffect, useState } from 'react'
 import { useDropzone } from 'react-dropzone'
 import { useTranslation } from 'react-i18next'
 
 import { useAuthContext } from '../contexts/AuthProvider'
+import { PresenceContext } from '../contexts/PresenceProvider'
 import CallPermissionDenied from '../pages/CallPermissionDenied'
 import { useUpdateAvatarMutation } from '../services/accountQueries'
 import { useContactProfilePicture } from '../services/contactQueries'
@@ -61,7 +62,7 @@ export default function ConversationAvatar({ displayName, contactUri, presence,
 
   if (presence) {
     return (
-      <ConversationAvatarPresence>
+      <ConversationAvatarPresence buddy={contactUri}>
         <Avatar
           id={'avatar' + contactUri}
           {...props}
@@ -83,7 +84,21 @@ export default function ConversationAvatar({ displayName, contactUri, presence,
   )
 }
 
-const ConversationAvatarPresence = ({ children }: { children: ReactNode }) => {
+const ConversationAvatarPresence = ({ children, buddy }: { children: ReactNode; buddy: string }) => {
+  const theme = useTheme()
+  const { getPresence, presence } = useContext(PresenceContext)
+  const [precenseState, setPrecenseState] = useState<number>(getPresence(buddy))
+
+  useEffect(() => {
+    setPrecenseState(getPresence(buddy))
+  }, [getPresence, presence, buddy])
+
+  if (!precenseState || precenseState === 0) {
+    return children
+  }
+
+  const presenceIconSize = '12px'
+
   return (
     <Box sx={{ display: 'flex', flexDirection: 'column', width: 'auto', height: 'auto' }}>
       {children}
@@ -99,11 +114,11 @@ const ConversationAvatarPresence = ({ children }: { children: ReactNode }) => {
       >
         <Box
           sx={{
-            width: '10px',
-            height: '10px',
-            backgroundColor: 'red',
+            width: presenceIconSize,
+            height: presenceIconSize,
+            backgroundColor: '#33cc33',
             borderRadius: '50%',
-            border: '2px solid',
+            border: '2px solid ' + theme.ChatInterface.panelColor,
           }}
         />
       </Box>
diff --git a/client/src/components/ConversationPreferences.tsx b/client/src/components/ConversationPreferences.tsx
index a3c48e9b8705a3b2c5b9311498342b7e8abd4444..05268bcbdc75b43cf32472dc6977db22682bfe2f 100644
--- a/client/src/components/ConversationPreferences.tsx
+++ b/client/src/components/ConversationPreferences.tsx
@@ -519,7 +519,7 @@ const MemberInfo = ({ member, key, isAdmin }: MemberInfoProps) => {
           }}
         >
           {' '}
-          <ConversationAvatar contactUri={member.contact.uri} displayName={bestName} />
+          <ConversationAvatar presence={true} contactUri={member.contact.uri} displayName={bestName} />
           <Box maxWidth={getBestWidth} marginLeft="8px">
             {bestName.length > 15 ? <EllipsisMiddle text={bestName} /> : bestName}
           </Box>
diff --git a/client/src/components/ConversationSummaryList.tsx b/client/src/components/ConversationSummaryList.tsx
index 45f762d15983c0330b487ddd93d01a1c27490095..5026d7026bf8287bc5a1f08b68cdb64cbcd087e8 100644
--- a/client/src/components/ConversationSummaryList.tsx
+++ b/client/src/components/ConversationSummaryList.tsx
@@ -137,6 +137,7 @@ const ConversationSummaryListItem = ({ conversationSummary }: ConversationSummar
         onContextMenu={contextMenuHandler.handleAnchorPosition}
         icon={
           <ConversationAvatar
+            presence={true}
             contactUri={
               Number(conversationSummary.mode) === 0 && filteredMembers.length > 0 ? filteredMembers[0].contact.uri : ''
             }
diff --git a/client/src/contexts/PresenceProvider.tsx b/client/src/contexts/PresenceProvider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..38be287ea0ff65c9f412779d8c2c3fc3a22a8466
--- /dev/null
+++ b/client/src/contexts/PresenceProvider.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2022-2025 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+import { NewBuddyNotification, WebSocketMessageType } from 'jami-web-common'
+import { createContext, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
+
+import { useWebSocketContext } from '../contexts/WebSocketProvider'
+
+interface IPresenceContext {
+  getPresence: (buddy: string) => number
+  presence: Map<string, number>
+}
+
+export const PresenceContext = createContext<IPresenceContext>({
+  getPresence: () => 0,
+  presence: new Map(),
+})
+
+interface PresenceProviderProps {
+  children: ReactNode
+}
+
+const PresenceProvider: React.FC<PresenceProviderProps> = ({ children }) => {
+  const [presence, setPresence] = useState<Map<string, number>>(new Map())
+  const webSocket = useWebSocketContext()
+
+  const getPresence = useCallback(
+    (buddy: string) => {
+      return presence.get(buddy) || 0
+    },
+    [presence],
+  )
+
+  useEffect(() => {
+    function handlePresence(data: NewBuddyNotification) {
+      setPresence((prev) => {
+        const newPresence = new Map(prev)
+        newPresence.set(data.buddyUri, data.status)
+        return newPresence
+      })
+    }
+
+    webSocket.bind(WebSocketMessageType.NewBuddyNotification, handlePresence)
+    return () => {
+      webSocket.unbind(WebSocketMessageType.NewBuddyNotification, handlePresence)
+    }
+  }, [webSocket])
+
+  const value = useMemo(() => ({ getPresence, presence }), [getPresence, presence])
+
+  return <PresenceContext.Provider value={value}>{children}</PresenceContext.Provider>
+}
+
+export default PresenceProvider
diff --git a/client/src/router.tsx b/client/src/router.tsx
index 036422f9a090a52d5b3a6e30247904e5e40370ae..b8dfe5a51b6769687f2d4b79307a299b08e9573f 100644
--- a/client/src/router.tsx
+++ b/client/src/router.tsx
@@ -42,6 +42,7 @@ import CallManagerProvider from './contexts/CallManagerProvider'
 import ConversationProvider from './contexts/ConversationProvider'
 import MessengerProvider from './contexts/MessengerProvider'
 import MobileMenuStateProvider from './contexts/MobileMenuStateProvider'
+import PresenceProvider from './contexts/PresenceProvider'
 import UserMediaProvider from './contexts/UserMediaProvider'
 import WebSocketProvider from './contexts/WebSocketProvider'
 import { AdminConfigurationOptions } from './enums/adminConfigurationOptions'
@@ -123,13 +124,15 @@ export const router = createBrowserRouter(
           element={
             <AuthProvider>
               <WebSocketProvider>
-                <AccountDetailsListener>
-                  <UserMediaProvider>
-                    <CallManagerProvider>
-                      <Outlet />
-                    </CallManagerProvider>
-                  </UserMediaProvider>
-                </AccountDetailsListener>
+                <PresenceProvider>
+                  <AccountDetailsListener>
+                    <UserMediaProvider>
+                      <CallManagerProvider>
+                        <Outlet />
+                      </CallManagerProvider>
+                    </UserMediaProvider>
+                  </AccountDetailsListener>
+                </PresenceProvider>
               </WebSocketProvider>
             </AuthProvider>
           }
diff --git a/server/src/services/ConversationService.ts b/server/src/services/ConversationService.ts
index 82ad793b8726ffd357b4559abd604cc234f15e37..de8f9a2a00ece2a4708e0dd9e6003954eddbbe75 100644
--- a/server/src/services/ConversationService.ts
+++ b/server/src/services/ConversationService.ts
@@ -23,8 +23,10 @@ import { ConversationRequestMetadata } from '../jamid/conversation-request-metad
 import { Jamid } from '../jamid/jamid.js'
 import { WebSocketServer } from '../websocket/websocket-server.js'
 import { ContactService } from './ContactService.js'
+import { PresenceService } from './PresenceService.js'
 
 const jamid = Container.get(Jamid)
+const presenceService = Container.get(PresenceService)
 const webSocketServer = Container.get(WebSocketServer)
 const contactService = Container.get(ContactService)
 
@@ -102,6 +104,9 @@ export class ConversationService {
       if (member.uri === accountUri) {
         return
       }
+
+      presenceService.subscribe(accountId, member.uri)
+
       // Add usernames for conversation members
       try {
         const data = await jamid.lookupAddress(member.uri, '', accountId)
diff --git a/server/src/services/PresenceService.ts b/server/src/services/PresenceService.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e5934329eb11886ee8af5a71c119d5b45d9ff47b
--- /dev/null
+++ b/server/src/services/PresenceService.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022-2025 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+import { Container, Service } from 'typedi'
+
+import { Jamid } from '../jamid/jamid.js'
+
+@Service()
+export class PresenceService {
+  private activeSubscribers: Map<string, Set<string>> = new Map()
+  private jamid: Jamid | undefined = undefined
+
+  getJamid() {
+    if (!this.jamid) {
+      this.jamid = Container.get(Jamid)
+    }
+    return this.jamid
+  }
+
+  subscribe(accountId: string, buddyUri: string) {
+    this.getJamid().subscribeBuddy(accountId, buddyUri, true)
+    const set = this.activeSubscribers.get(accountId)
+    if (!set) {
+      this.activeSubscribers.set(accountId, new Set([buddyUri]))
+      return
+    }
+    this.activeSubscribers.set(accountId, set.add(buddyUri))
+  }
+
+  unsubscribeAll(accountId: string) {
+    const set = this.activeSubscribers.get(accountId)
+    if (!set) {
+      return
+    }
+    for (const uri of set) {
+      this.getJamid().subscribeBuddy(accountId, uri, false)
+    }
+  }
+
+  unsubscribe(accountId: string, buddyUri: string) {
+    this.getJamid().subscribeBuddy(accountId, buddyUri, false)
+    const set = this.activeSubscribers.get(accountId)
+    if (!set) {
+      return
+    }
+    set.delete(buddyUri)
+  }
+}
diff --git a/server/src/websocket/websocket-server.ts b/server/src/websocket/websocket-server.ts
index ba100b9351e6d69c1775a098fc6a0623a021bbdd..5fb961b721f91544d0ff1ec5e21a66f4cf23765a 100644
--- a/server/src/websocket/websocket-server.ts
+++ b/server/src/websocket/websocket-server.ts
@@ -20,12 +20,15 @@ import { Duplex } from 'node:stream'
 
 import { WebSocketMessage, WebSocketMessageTable, WebSocketMessageType } from 'jami-web-common'
 import log from 'loglevel'
-import { Service } from 'typedi'
+import { Container, Service } from 'typedi'
 import { URL } from 'whatwg-url'
 import * as WebSocket from 'ws'
 
+import { PresenceService } from '../services/PresenceService.js'
 import { verifyJwt } from '../utils/jwt.js'
 
+const presenceService = Container.get(PresenceService)
+
 type WebSocketCallback<T extends WebSocketMessageType> = (accountId: string, data: WebSocketMessageTable[T]) => void
 
 type WebSocketCallbacks = {
@@ -89,6 +92,8 @@ export class WebSocketServer {
         if (index !== -1) {
           accountSockets.splice(index, 1)
           if (accountSockets.length === 0) {
+            // Unsubscribe from all presence updates when the last socket is closed
+            presenceService.unsubscribeAll(accountId)
             this.sockets.delete(accountId)
           }
         }