From ab2fb7ed3a95b0107116c45f281635c496b2fc4a Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Mon, 22 Jun 2026 21:58:13 -0300 Subject: [PATCH 1/2] fix: dispatch avatar etag --- app/lib/services/connect.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/lib/services/connect.ts b/app/lib/services/connect.ts index 045b589942a..dce7f343396 100644 --- a/app/lib/services/connect.ts +++ b/app/lib/services/connect.ts @@ -247,6 +247,15 @@ function connect({ server, logoutOnError = false }: { server: string; logoutOnEr } } else if (/updateAvatar/.test(eventName)) { const { username, etag } = ddpMessage.fields.args[0]; + + // If it's the logged user, push the new etag through setUser so the + // servers-DB logged-user record (observed by useAvatarETag) updates, + // refreshing the avatar in ProfileView, SidebarView, etc. + const { user: loggedUser } = store.getState().login; + if (loggedUser?.username === username) { + store.dispatch(setUser({ avatarETag: etag })); + } + const db = database.active; const userCollection = db.get('users'); try { From 51012ead836fcf673c74a9ea91d7859991020749 Mon Sep 17 00:00:00 2001 From: OtavioStasiak Date: Tue, 23 Jun 2026 14:21:27 -0300 Subject: [PATCH 2/2] unit test --- app/lib/services/connect.test.ts | 90 ++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/app/lib/services/connect.test.ts b/app/lib/services/connect.test.ts index 9d20bbf30ca..d3ac06b24ed 100644 --- a/app/lib/services/connect.test.ts +++ b/app/lib/services/connect.test.ts @@ -2,6 +2,8 @@ import { connect, determineAuthType, disconnect } from './connect'; import { mediaSessionInstance } from './voip/MediaSessionInstance'; import { pendingHangups } from './voip/pendingHangups'; import { unsubscribeRooms } from '../methods/subscribeRooms'; +import { setUser } from '../../actions/login'; +import database from '../database'; jest.mock('./voip/MediaSessionInstance', () => ({ mediaSessionInstance: { reset: jest.fn(), drainPendingHangups: jest.fn() } @@ -512,4 +514,92 @@ describe('connect — rooms subscription guard reset on close', () => { }); }); +describe('connect — stream-notify-logged updateAvatar', () => { + const mockUserUpdate = jest.fn, [(u: { avatarETag: string }) => void]>(fn => { + fn({ avatarETag: '' }); + return Promise.resolve(); + }); + const mockUserRecord = { update: (fn: (u: { avatarETag: string }) => void) => mockUserUpdate(fn) }; + const mockFetch = jest.fn, []>(() => Promise.resolve([mockUserRecord])); + const mockDbWrite = jest.fn, [() => Promise]>(async fn => { + await fn(); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + mockOnStreamDataStops.length = 0; + mockStoreGetState.mockReturnValue({ + meteor: { connected: false }, + login: { user: null, isAuthenticated: false }, + settings: {} + }); + + // Wire up database.active so the WatermelonDB section of the handler runs. + (database.active as any).get = jest.fn(() => ({ + query: () => ({ fetch: () => mockFetch() }) + })); + (database.active as any).write = (fn: () => Promise) => mockDbWrite(fn); + + await connect({ server: 'https://example.com' }); + }); + + const getUpdateAvatarHandler = () => getHandlersByEvent('stream-notify-logged')[0]; + + const fireUpdateAvatar = async (args: { username: string; etag: string }) => { + const handler = getUpdateAvatarHandler(); + handler({ fields: { eventName: 'updateAvatar', args: [args] } }); + await flushMicrotasks(); + }; + + it('dispatches setUser with the new etag when the avatar belongs to the logged user', async () => { + mockStoreGetState.mockReturnValue({ + meteor: { connected: true }, + login: { user: { id: 'u1', username: 'rocket.cat' }, isAuthenticated: true }, + settings: {} + }); + + await fireUpdateAvatar({ username: 'rocket.cat', etag: 'newEtag' }); + + expect(mockStoreDispatch).toHaveBeenCalledWith(setUser({ avatarETag: 'newEtag' })); + }); + + it('does not dispatch setUser when the avatar belongs to another user', async () => { + mockStoreGetState.mockReturnValue({ + meteor: { connected: true }, + login: { user: { id: 'u1', username: 'rocket.cat' }, isAuthenticated: true }, + settings: {} + }); + + await fireUpdateAvatar({ username: 'someone.else', etag: 'newEtag' }); + + expect(mockStoreDispatch).not.toHaveBeenCalledWith(setUser({ avatarETag: 'newEtag' })); + }); + + it('does not dispatch setUser when there is no logged user', async () => { + mockStoreGetState.mockReturnValue({ + meteor: { connected: true }, + login: { user: null, isAuthenticated: false }, + settings: {} + }); + + await fireUpdateAvatar({ username: 'rocket.cat', etag: 'newEtag' }); + + expect(mockStoreDispatch).not.toHaveBeenCalledWith(setUser({ avatarETag: 'newEtag' })); + }); + + it('still updates the users-DB record with the new etag for the logged user', async () => { + mockStoreGetState.mockReturnValue({ + meteor: { connected: true }, + login: { user: { id: 'u1', username: 'rocket.cat' }, isAuthenticated: true }, + settings: {} + }); + + await fireUpdateAvatar({ username: 'rocket.cat', etag: 'newEtag' }); + + const updated = { avatarETag: '' }; + await mockUserUpdate.mock.calls[0][0](updated); + expect(updated.avatarETag).toBe('newEtag'); + }); +}); + // Note: Apple authentication when isIOS is true is tested in connect.ios.test.ts