diff --git a/packages/client/src/events/__tests__/participant.test.ts b/packages/client/src/events/__tests__/participant.test.ts index 59340b9d7c..7b4d5a8c70 100644 --- a/packages/client/src/events/__tests__/participant.test.ts +++ b/packages/client/src/events/__tests__/participant.test.ts @@ -74,6 +74,47 @@ describe('Participant events', () => { expect(state.participants).toEqual([]); }); + + it('sets a server-side pin when isPinned is true', () => { + const state = new CallState(); + state.setSortParticipantsBy(noopComparator()); + + const onParticipantJoined = watchParticipantJoined(state); + const now = Date.now(); + + onParticipantJoined({ + // @ts-expect-error incomplete data + participant: { + userId: 'user-id', + sessionId: 'session-id', + }, + isPinned: true, + }); + + const participant = state.findParticipantBySessionId('session-id'); + expect(participant?.pin).toBeDefined(); + expect(participant?.pin?.isLocalPin).toBe(false); + expect(participant?.pin?.pinnedAt).toBeGreaterThanOrEqual(now); + }); + + it('does not set a pin when isPinned is false', () => { + const state = new CallState(); + state.setSortParticipantsBy(noopComparator()); + + const onParticipantJoined = watchParticipantJoined(state); + + onParticipantJoined({ + // @ts-expect-error incomplete data + participant: { + userId: 'user-id', + sessionId: 'session-id', + }, + isPinned: false, + }); + + const participant = state.findParticipantBySessionId('session-id'); + expect(participant?.pin).toBeUndefined(); + }); }); describe('orphaned tracks reconciliation', () => { diff --git a/packages/client/src/events/participant.ts b/packages/client/src/events/participant.ts index a9725deca3..b927446fec 100644 --- a/packages/client/src/events/participant.ts +++ b/packages/client/src/events/participant.ts @@ -38,6 +38,7 @@ export const watchParticipantJoined = (state: CallState) => { StreamVideoParticipantPatch | undefined, Partial >(participant, orphanedTracks, { + ...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }), viewportVisibilityState: { videoTrack: VisibilityState.UNKNOWN, screenShareTrack: VisibilityState.UNKNOWN, diff --git a/packages/client/src/gen/video/sfu/event/events.ts b/packages/client/src/gen/video/sfu/event/events.ts index 12d102d5cd..4e45616d88 100644 --- a/packages/client/src/gen/video/sfu/event/events.ts +++ b/packages/client/src/gen/video/sfu/event/events.ts @@ -625,6 +625,10 @@ export interface ParticipantJoined { * @generated from protobuf field: stream.video.sfu.models.Participant participant = 2; */ participant?: Participant; + /** + * @generated from protobuf field: bool is_pinned = 3; + */ + isPinned: boolean; } /** * ParticipantJoined is fired when a user leaves a call @@ -1557,6 +1561,7 @@ class ParticipantJoined$Type extends MessageType { super('stream.video.sfu.event.ParticipantJoined', [ { no: 1, name: 'call_cid', kind: 'scalar', T: 9 /*ScalarType.STRING*/ }, { no: 2, name: 'participant', kind: 'message', T: () => Participant }, + { no: 3, name: 'is_pinned', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ }, ]); } }