@stream-io/video-client 0.1.11 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
+ import { anyNumber } from 'vitest-mock-extended';
2
3
  import { StreamVideoParticipant, VisibilityState } from '../../types';
3
4
  import { CallState } from '../CallState';
4
5
  import {
@@ -16,90 +17,144 @@ import {
16
17
  import * as TestData from '../../sorting/__tests__/participant-data';
17
18
 
18
19
  describe('CallState', () => {
19
- it('should emit sorted participants', () => {
20
- const state = new CallState();
21
- state.setSortParticipantsBy(noopComparator());
22
- state.setParticipants(TestData.participants());
23
-
24
- // initial sort criteria
25
- const ps = state.participants;
26
- expect(ps.map((p) => p.name)).toEqual(['A', 'B', 'C', 'D', 'E', 'F']);
27
-
28
- // update sort criteria
29
- state.setSortParticipantsBy(
30
- combineComparators(
31
- dominantSpeaker,
32
- publishingAudio,
33
- publishingVideo,
34
- screenSharing,
35
- ),
36
- );
37
-
38
- const ps2 = state.participants;
39
- expect(ps2.map((p) => p.name)).toEqual(['D', 'B', 'A', 'F', 'E', 'C']);
40
- });
20
+ describe('sorting', () => {
21
+ it('should emit sorted participants', () => {
22
+ const state = new CallState();
23
+ state.setSortParticipantsBy(noopComparator());
24
+ state.setParticipants(TestData.participants());
25
+
26
+ // initial sort criteria
27
+ const ps = state.participants;
28
+ expect(ps.map((p) => p.name)).toEqual(['A', 'B', 'C', 'D', 'E', 'F']);
29
+
30
+ // update sort criteria
31
+ state.setSortParticipantsBy(
32
+ combineComparators(
33
+ dominantSpeaker,
34
+ publishingAudio,
35
+ publishingVideo,
36
+ screenSharing,
37
+ ),
38
+ );
39
+
40
+ const ps2 = state.participants;
41
+ expect(ps2.map((p) => p.name)).toEqual(['D', 'B', 'A', 'F', 'E', 'C']);
42
+ });
41
43
 
42
- it('should be able to disable sorting', () => {
43
- const participants = TestData.participants();
44
- const state = new CallState();
45
- state.setParticipants(TestData.participants());
46
- // initial sort criteria
47
- const ps = state.participants;
48
- expect(ps.map((p) => p.name)).toEqual(['F', 'B', 'E', 'A', 'C', 'D']);
49
-
50
- // disable sorting
51
- state.setSortParticipantsBy(noopComparator());
52
-
53
- // update the dominant speaker -> in this case, no sorting should be applied
54
- const [A] = participants;
55
- state.updateParticipant(A.sessionId, {
56
- isDominantSpeaker: true,
44
+ it('should be able to disable sorting', () => {
45
+ const participants = TestData.participants();
46
+ const state = new CallState();
47
+ state.setParticipants(TestData.participants());
48
+ // initial sort criteria
49
+ const ps = state.participants;
50
+ expect(ps.map((p) => p.name)).toEqual(['F', 'B', 'E', 'A', 'C', 'D']);
51
+
52
+ // disable sorting
53
+ state.setSortParticipantsBy(noopComparator());
54
+
55
+ // update the dominant speaker -> in this case, no sorting should be applied
56
+ const [A] = participants;
57
+ state.updateParticipant(A.sessionId, {
58
+ isDominantSpeaker: true,
59
+ });
60
+
61
+ const ps2 = state.participants;
62
+ expect(ps2.map((p) => p.name)).toEqual(['F', 'B', 'E', 'A', 'C', 'D']);
57
63
  });
58
64
 
59
- const ps2 = state.participants;
60
- expect(ps2.map((p) => p.name)).toEqual(['F', 'B', 'E', 'A', 'C', 'D']);
61
- });
65
+ it('should support custom sorting', () => {
66
+ const state = new CallState();
67
+ state.setSortParticipantsBy(descending(name));
62
68
 
63
- it('should support custom sorting', () => {
64
- const state = new CallState();
65
- state.setSortParticipantsBy(descending(name));
69
+ state.setParticipants(TestData.participants());
70
+ const ps = state.participants;
71
+ expect(ps.map((p) => p.name)).toEqual(['F', 'E', 'D', 'C', 'B', 'A']);
72
+ });
66
73
 
67
- state.setParticipants(TestData.participants());
68
- const ps = state.participants;
69
- expect(ps.map((p) => p.name)).toEqual(['F', 'E', 'D', 'C', 'B', 'A']);
74
+ it('should consider participant visibility', () => {
75
+ const [A, B, C, D] = TestData.participants();
76
+
77
+ const state = new CallState();
78
+ state.setSortParticipantsBy(name);
79
+ state.setParticipants([A, B, C, D]);
80
+ expect(state.participants).toEqual([A, B, C, D]);
81
+
82
+ const Z = {
83
+ ...A,
84
+ name: 'Z',
85
+ };
86
+
87
+ // normal mode: Z is pushed to the end
88
+ state.setParticipants([Z, B, C, D]);
89
+ expect(state.participants).toEqual([B, C, D, Z]);
90
+
91
+ const ifInvisibleBy = conditional(
92
+ (a: StreamVideoParticipant, b: StreamVideoParticipant) =>
93
+ a.viewportVisibilityState === VisibilityState.INVISIBLE ||
94
+ b.viewportVisibilityState === VisibilityState.INVISIBLE,
95
+ );
96
+ state.setSortParticipantsBy(ifInvisibleBy(name));
97
+
98
+ // Z is visible, so it is kept in the same position
99
+ state.setParticipants([Z, B, C, D]);
100
+ expect(state.participants).toEqual([Z, B, C, D]);
101
+
102
+ // Z is invisible, so, the normal sorting is applied and Z is pushed to the end
103
+ Z.viewportVisibilityState = VisibilityState.INVISIBLE;
104
+ state.setParticipants([Z, B, C, D]);
105
+ expect(state.participants).toEqual([B, C, D, Z]);
106
+ });
70
107
  });
71
108
 
72
- it('should consider participant visibility', () => {
73
- const [A, B, C, D] = TestData.participants();
74
-
75
- const state = new CallState();
76
- state.setSortParticipantsBy(name);
77
- state.setParticipants([A, B, C, D]);
78
- expect(state.participants).toEqual([A, B, C, D]);
79
-
80
- const Z = {
81
- ...A,
82
- name: 'Z',
83
- };
84
-
85
- // normal mode: Z is pushed to the end
86
- state.setParticipants([Z, B, C, D]);
87
- expect(state.participants).toEqual([B, C, D, Z]);
88
-
89
- const ifInvisibleBy = conditional(
90
- (a: StreamVideoParticipant, b: StreamVideoParticipant) =>
91
- a.viewportVisibilityState === VisibilityState.INVISIBLE ||
92
- b.viewportVisibilityState === VisibilityState.INVISIBLE,
93
- );
94
- state.setSortParticipantsBy(ifInvisibleBy(name));
95
-
96
- // Z is visible, so it is kept in the same position
97
- state.setParticipants([Z, B, C, D]);
98
- expect(state.participants).toEqual([Z, B, C, D]);
99
-
100
- // Z is invisible, so, the normal sorting is applied and Z is pushed to the end
101
- Z.viewportVisibilityState = VisibilityState.INVISIBLE;
102
- state.setParticipants([Z, B, C, D]);
103
- expect(state.participants).toEqual([B, C, D, Z]);
109
+ describe('pinning', () => {
110
+ it('should update the pinned state of participants in the call', () => {
111
+ const state = new CallState();
112
+ state.setSortParticipantsBy(noopComparator());
113
+ // @ts-ignore
114
+ state.setParticipants([{ sessionId: '123' }, { sessionId: '456' }]);
115
+
116
+ state.setServerSidePins([{ sessionId: '123', userId: 'user-id' }]);
117
+
118
+ expect(state.participants).toEqual([
119
+ { sessionId: '123', pin: { isLocalPin: false, pinnedAt: anyNumber() } },
120
+ { sessionId: '456' },
121
+ ]);
122
+ });
123
+
124
+ it('should unpin participants that are no longer pinned', () => {
125
+ const state = new CallState();
126
+ state.setSortParticipantsBy(noopComparator());
127
+ state.setParticipants([
128
+ // @ts-ignore
129
+ { sessionId: '123', pin: { isLocalPin: false, pinnedAt: 1000 } },
130
+ // @ts-ignore
131
+ { sessionId: '456' },
132
+ ]);
133
+
134
+ state.setServerSidePins([]);
135
+
136
+ expect(state.participants).toEqual([
137
+ { sessionId: '123', pin: undefined },
138
+ { sessionId: '456' },
139
+ ]);
140
+ });
141
+
142
+ it('should not unpin participants that are pinned locally', () => {
143
+ const state = new CallState();
144
+ state.setSortParticipantsBy(noopComparator());
145
+ state.setParticipants([
146
+ // @ts-ignore
147
+ { sessionId: '123', pin: { isLocalPin: true, pinnedAt: 1000 } },
148
+ // @ts-ignore
149
+ { sessionId: '456' },
150
+ ]);
151
+
152
+ state.setServerSidePins([]);
153
+
154
+ expect(state.participants).toEqual([
155
+ { sessionId: '123', pin: { isLocalPin: true, pinnedAt: 1000 } },
156
+ { sessionId: '456' },
157
+ ]);
158
+ });
104
159
  });
105
160
  });
package/src/types.ts CHANGED
@@ -69,9 +69,9 @@ export interface StreamVideoParticipant extends Participant {
69
69
  isLocalParticipant?: boolean;
70
70
 
71
71
  /**
72
- * Timestamp of when the participant is pinned
72
+ * The pin state of the participant.
73
73
  */
74
- pinnedAt?: number;
74
+ pin?: ParticipantPin;
75
75
 
76
76
  /**
77
77
  * The last reaction this user has sent to this call.
@@ -106,6 +106,22 @@ export interface StreamVideoLocalParticipant extends StreamVideoParticipant {
106
106
  audioOutputDeviceId?: string;
107
107
  }
108
108
 
109
+ /**
110
+ * Represents a participant's pin state.
111
+ */
112
+ export type ParticipantPin = {
113
+ /**
114
+ * Set to true if the participant is pinned by the local user.
115
+ * False if the participant is pinned server-side, by the call moderator.
116
+ */
117
+ isLocalPin: boolean;
118
+
119
+ /**
120
+ * Timestamp when the participant is pinned.
121
+ */
122
+ pinnedAt: number;
123
+ };
124
+
109
125
  export const isStreamVideoLocalParticipant = (
110
126
  p: StreamVideoParticipant | StreamVideoLocalParticipant,
111
127
  ): p is StreamVideoLocalParticipant => {