Skip to content

Commit cd08b76

Browse files
authored
feat(ui, llc): add backwards compatible partial state (#983)
* feat: add backwards compatible partial state * feat: add automatic dart fixes * fix all warnings * update dogfooding * Improve deprecations * Add tests for fixes * Add participant data in new callbacks * Add some extra docs * rename builder methods * improve partial state mapper * add unit test for `partialCallStateStream` * Also compare list equality in partialCallStateStream * remove local participant from `AddReactionOption` * Add changelogs * fix typo
1 parent 1b9d80e commit cd08b76

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1690
-350
lines changed

.github/workflows/stream_video_flutter_workflow.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ jobs:
7272
with:
7373
token: ${{secrets.CODECOV_TOKEN}}
7474
files: packages/*/coverage/lcov.info
75+
- name: "Test dart fixes"
76+
run: melos run test:fixes
7577

7678
build:
7779
timeout-minutes: 30

dogfooding/lib/screens/call_screen.dart

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -130,28 +130,25 @@ class _CallScreenState extends State<CallScreen> {
130130
showFeedbackDialog(context, call: widget.call);
131131
}
132132
},
133-
callContentBuilder: (
133+
callContentWidgetBuilder: (
134134
BuildContext context,
135135
Call call,
136-
CallState callState,
137136
) {
138137
return StreamCallContent(
139138
call: call,
140-
callState: callState,
141139
layoutMode: _currentLayoutMode,
142140
pictureInPictureConfiguration:
143141
const PictureInPictureConfiguration(
144142
enablePictureInPicture: true,
145143
),
146-
callParticipantsBuilder: (context, call, callState) {
144+
callParticipantsWidgetBuilder: (context, call) {
147145
return Stack(
148146
children: [
149147
Column(
150148
children: [
151149
Expanded(
152150
child: StreamCallParticipants(
153151
call: call,
154-
participants: callState.callParticipants,
155152
layoutMode: _currentLayoutMode,
156153
),
157154
),
@@ -194,19 +191,23 @@ class _CallScreenState extends State<CallScreen> {
194191
),
195192
),
196193
],
197-
if (!_moreMenuVisible &&
198-
(call.state.valueOrNull?.otherParticipants.isEmpty ??
199-
false))
194+
if (!_moreMenuVisible)
200195
Positioned(
201196
bottom: 0,
202197
left: 0,
203198
right: 0,
204-
child: ShareCallWelcomeCard(callId: call.id),
199+
child: PartialCallStateBuilder(
200+
call: call,
201+
selector: (state) => state.otherParticipants.isEmpty,
202+
builder: (context, isEmpty) => isEmpty
203+
? ShareCallWelcomeCard(callId: call.id)
204+
: const SizedBox.shrink(),
205+
),
205206
)
206207
],
207208
);
208209
},
209-
callAppBarBuilder: (context, call, callState) {
210+
callAppBarWidgetBuilder: (context, call) {
210211
return CallAppBar(
211212
call: call,
212213
leadingWidth: 120,
@@ -219,22 +220,25 @@ class _CallScreenState extends State<CallScreen> {
219220
});
220221
},
221222
),
222-
if (call.state.valueOrNull?.localParticipant != null)
223-
FlipCameraOption(
224-
call: call,
225-
localParticipant: call.state.value.localParticipant!,
226-
),
223+
PartialCallStateBuilder(
224+
call: call,
225+
selector: (state) => state.localParticipant != null,
226+
builder: (context, hasLocalParticipant) =>
227+
hasLocalParticipant
228+
? FlipCameraOption(
229+
call: call,
230+
)
231+
: const SizedBox.shrink(),
232+
),
227233
],
228234
),
229235
title: CallDurationTitle(call: call),
230236
);
231237
},
232-
callControlsBuilder: (
238+
callControlsWidgetBuilder: (
233239
BuildContext context,
234240
Call call,
235-
CallState callState,
236241
) {
237-
final localParticipant = callState.localParticipant!;
238242
return Container(
239243
padding: const EdgeInsets.only(
240244
top: 16,
@@ -254,7 +258,6 @@ class _CallScreenState extends State<CallScreen> {
254258
}),
255259
ToggleScreenShareOption(
256260
call: call,
257-
localParticipant: localParticipant,
258261
screenShareConstraints: const ScreenShareConstraints(
259262
useiOSBroadcastExtension: true,
260263
),
@@ -268,24 +271,28 @@ class _CallScreenState extends State<CallScreen> {
268271
),
269272
ToggleMicrophoneOption(
270273
call: call,
271-
localParticipant: localParticipant,
272274
disabledMicrophoneBackgroundColor:
273275
AppColorPalette.appRed,
274276
),
275277
ToggleCameraOption(
276278
call: call,
277-
localParticipant: localParticipant,
278279
disabledCameraBackgroundColor: AppColorPalette.appRed,
279280
),
280281
const Spacer(),
281-
BadgedCallOption(
282-
callControlOption: CallControlOption(
283-
icon: const Icon(Icons.people),
284-
onPressed: _channel != null //
285-
? () => showParticipants(context)
286-
: null,
287-
),
288-
badgeCount: callState.callParticipants.length,
282+
PartialCallStateBuilder(
283+
call: call,
284+
selector: (state) => state.callParticipants.length,
285+
builder: (context, length) {
286+
return BadgedCallOption(
287+
callControlOption: CallControlOption(
288+
icon: const Icon(Icons.people),
289+
onPressed: _channel != null //
290+
? () => showParticipants(context)
291+
: null,
292+
),
293+
badgeCount: length,
294+
);
295+
},
289296
),
290297
_ShowChatButton(channel: _channel),
291298
]),

dogfooding/lib/screens/home_screen.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,6 @@ class _JoinForm extends StatelessWidget {
441441
}
442442

443443
if (environment == Environment.livestream) {
444-
// TODO: handle livestream join
445444
// Example: https://livestream-react-demo.vercel.app/?id=6G9bxsMaFbMiGvLWWP85d&type=livestream
446445
final callId = uri.queryParameters['id'];
447446
if (callId != null) {

melos.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ scripts:
121121
flutter: true
122122
dirExists: test
123123

124+
test:fixes:
125+
run: melos exec -c 4 --fail-fast -- "cd test_fixes && dart fix --compare-to-golden"
126+
description: Verify dart fixes for a specific package in this project.
127+
packageFilters:
128+
flutter: true
129+
dirExists: test_fixes
130+
124131
update:goldens:
125132
run: melos exec -c 1 --depends-on="alchemist" -- "flutter test --tags golden --update-goldens"
126133
description: Update golden files for all packages in this project.

packages/stream_video/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## Unreleased
2+
3+
✅ Added
4+
* Added `call.partialState` for more specific and efficient state updates.
5+
16
## 0.9.6
27

38
✅ Added

packages/stream_video/lib/src/call/call.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ typedef SetActiveCall = Future<void> Function(Call?);
7474
typedef SetOutgoingCall = Future<void> Function(Call?);
7575
typedef GetActiveCall = Call? Function();
7676
typedef GetOutgoingCall = Call? Function();
77+
typedef CallStateSelector<T> = T Function(CallState state);
7778

7879
const _idState = 1;
7980
const _idUserId = 2;
@@ -294,6 +295,10 @@ class Call {
294295
StateEmitter<CallState> get state => _stateManager.callStateStream;
295296
Stream<Duration> get callDurationStream => _stateManager.durationStream;
296297

298+
Stream<T> partialState<T>(CallStateSelector<T> selector) {
299+
return _stateManager.partialCallStateStream(selector);
300+
}
301+
297302
SharedEmitter<({CallStats publisherStats, CallStats subscriberStats})>
298303
get stats => _stats;
299304
late final _stats = MutableSharedEmitterImpl<

packages/stream_video/lib/src/call/state/call_state_notifier.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:async';
22

3+
import 'package:collection/collection.dart';
34
import 'package:state_notifier/state_notifier.dart';
45

56
import '../../call_state.dart';
@@ -33,6 +34,20 @@ class CallStateNotifier extends StateNotifier<CallState>
3334
late final MutableStateEmitterImpl<CallState> callStateStream;
3435
CallState get callState => callStateStream.value;
3536

37+
Stream<T> partialCallStateStream<T>(T Function(CallState state) selector) {
38+
return callStateStream.valueStream
39+
.map(selector)
40+
.distinct(
41+
(previous, current) =>
42+
identical(previous, current) ||
43+
previous == current ||
44+
(previous is List &&
45+
current is List &&
46+
const ListEquality<dynamic>().equals(previous, current)),
47+
)
48+
.asBroadcastStream();
49+
}
50+
3651
Stream<Duration> get durationStream =>
3752
_durationTimerController.stream.distinct();
3853

packages/stream_video/lib/stream_video.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export 'src/sfu/data/models/sfu_goaway_reason.dart';
2828
export 'src/sfu/data/models/sfu_track_type.dart';
2929
export 'src/sorting/call_participant_sorting_presets.dart';
3030
export 'src/sorting/call_participant_state_sorting.dart';
31+
export 'src/state_emitter.dart' show MutableStateEmitter, StateEmitter;
3132
export 'src/stream_video.dart';
3233
export 'src/token/token.dart';
3334
export 'src/types/other.dart';
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:stream_video/src/call/state/call_state_notifier.dart';
3+
import 'package:stream_video/stream_video.dart';
4+
5+
void main() {
6+
test('partialCallStateStream distinct filter', () async {
7+
var callState = CallState(
8+
callCid: StreamCallCid.from(
9+
type: StreamCallType.defaultType(),
10+
id: 'id',
11+
),
12+
currentUserId: 'userId',
13+
preferences: DefaultCallPreferences(),
14+
);
15+
final notifier = CallStateNotifier(callState);
16+
var updates = 0;
17+
18+
// We only listen to status updates
19+
final subscription = notifier
20+
.partialCallStateStream((state) => state.status)
21+
.listen((status) {
22+
updates++;
23+
});
24+
25+
// Initial state should be received from the partialCallStateStream
26+
await Future<void>.delayed(Duration.zero);
27+
expect(updates, 1);
28+
29+
// Updating status should triger partialCallStateStream
30+
callState = callState.copyWith(status: CallStatus.incoming());
31+
notifier.state = callState;
32+
await Future<void>.delayed(Duration.zero);
33+
expect(updates, 2);
34+
35+
// anything else should not trigger partialCallStateStream
36+
callState = callState.copyWith(isRecording: true);
37+
notifier.state = callState;
38+
await Future<void>.delayed(Duration.zero);
39+
expect(updates, 2);
40+
41+
await subscription.cancel();
42+
});
43+
44+
test('partialCallStateStream mapped list', () async {
45+
var callState = CallState(
46+
callCid: StreamCallCid.from(
47+
type: StreamCallType.defaultType(),
48+
id: 'id',
49+
),
50+
currentUserId: 'userId',
51+
preferences: DefaultCallPreferences(),
52+
).copyWith(
53+
callParticipants: [
54+
CallParticipantState(
55+
userId: 'userId',
56+
roles: const ['participant'],
57+
name: 'name',
58+
custom: const {},
59+
sessionId: 'sessionId',
60+
trackIdPrefix: 'trackIdPrefix',
61+
),
62+
CallParticipantState(
63+
userId: 'userId2',
64+
roles: const ['participant'],
65+
name: 'name2',
66+
custom: const {},
67+
sessionId: 'sessionId2',
68+
trackIdPrefix: 'trackIdPrefix2',
69+
),
70+
],
71+
);
72+
73+
final notifier = CallStateNotifier(callState);
74+
var updates = 0;
75+
76+
// We only listen to status updates
77+
final subscription = notifier
78+
.partialCallStateStream(
79+
(state) => state.callParticipants.map((e) => e.userId).toList(),
80+
)
81+
.listen((status) {
82+
updates++;
83+
});
84+
85+
// Initial state should be received from the partialCallStateStream
86+
await Future<void>.delayed(Duration.zero);
87+
expect(updates, 1);
88+
89+
// Updating participant id should triger partialCallStateStream
90+
callState = callState.copyWith(
91+
callParticipants: [
92+
callState.callParticipants[0],
93+
callState.callParticipants[1].copyWith(userId: 'userId3'),
94+
],
95+
);
96+
notifier.state = callState;
97+
await Future<void>.delayed(Duration.zero);
98+
expect(updates, 2);
99+
100+
// anything else should not trigger partialCallStateStream
101+
callState = callState.copyWith(
102+
callParticipants: [
103+
callState.callParticipants[0],
104+
callState.callParticipants[1].copyWith(name: 'other name'),
105+
],
106+
);
107+
notifier.state = callState;
108+
await Future<void>.delayed(Duration.zero);
109+
expect(updates, 2);
110+
111+
await subscription.cancel();
112+
});
113+
}

packages/stream_video_flutter/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
## Unreleased
22

3+
🔄 Partial State Updates:
4+
* Added `call.partialState` for more specific and efficient state updates.
5+
* Added callbacks in `StreamCallContainer`, `StreamCallContent`, `StreamIncomingCallContent`, and others that no longer return a state.
6+
By (only) using these callbacks the root widgets will use more efficient partial state updates.
7+
* Added `PartialCallStateBuilder` to help with making widgets that depend on `partialState`.
8+
* Deprecated old callbacks
9+
10+
311
✅ Added
412
* Added `handleCallInterruptionCallbacks` method to `RtcMediaDeviceNotifier` that provides an option to handle system audio interruption like incoming calls, or other media playing. See the [documentation](https://getstream.io/video/docs/flutter/advanced/handling-system-audio-interruptions/) for details.
513
* Improved the Picture-in-Picture (PiP) implementation for video calls

0 commit comments

Comments
 (0)