diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index d2b9906317..beee803925 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -3,6 +3,7 @@ 🐞 Fixed - Fixed `StreamDraftListController.deleteDraft` removing the wrong draft. +- Fixed `StreamPollVoteListController` adding votes and comments from other polls in the same channel. ## 9.31.0 diff --git a/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart index 3b29d687dc..a01e445846 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart @@ -215,6 +215,8 @@ class StreamPollVoteListController // Returns early if the event is already handled by the listener. if (eventListener?.call(event) ?? false) return; + if (!_belongsToPoll(event)) return; + final eventType = event.type; if (eventType == EventType.pollVoteCasted || eventType == EventType.pollAnswerCasted) { @@ -228,6 +230,15 @@ class StreamPollVoteListController }); } + // Whether the vote or answer carried by [event] belongs to [pollId]. + // + // The subscription receives the events of every poll in [channel], not only + // the one this controller lists. + bool _belongsToPoll(Event event) { + final eventPollId = event.pollVote?.pollId ?? event.poll?.id; + return eventPollId == pollId; + } + // Unsubscribes from all poll vote list events. void _unsubscribeFromPolVoteListEvents() { if (_pollVoteListEventSubscription != null) { diff --git a/packages/stream_chat_flutter_core/test/stream_poll_vote_list_controller_test.dart b/packages/stream_chat_flutter_core/test/stream_poll_vote_list_controller_test.dart new file mode 100644 index 0000000000..010f61996f --- /dev/null +++ b/packages/stream_chat_flutter_core/test/stream_poll_vote_list_controller_test.dart @@ -0,0 +1,145 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/src/stream_poll_vote_list_controller.dart'; + +import 'mocks.dart'; + +const _pollId = 'poll-a'; +const _otherPollId = 'poll-b'; + +void main() { + setUpAll(() { + registerFallbackValue(const PaginationParams()); + }); + + test('a vote-casted event for another poll leaves the list unchanged', + () async { + final existingVote = + PollVote(id: 'vote-1', pollId: _pollId, optionId: 'option-1'); + final (:controller, :events) = + await _loadedController(votes: [existingVote]); + + events.add( + Event( + type: EventType.pollVoteCasted, + pollVote: + PollVote(id: 'vote-2', pollId: _otherPollId, optionId: 'option-2'), + ), + ); + await pumpEventQueue(); + + expect(controller.currentItems, [existingVote]); + }); + + test('a vote-casted event for the same poll adds the vote', () async { + final (:controller, :events) = await _loadedController(); + final vote = PollVote(id: 'vote-1', pollId: _pollId, optionId: 'option-1'); + + events.add(Event(type: EventType.pollVoteCasted, pollVote: vote)); + await pumpEventQueue(); + + expect(controller.currentItems, [vote]); + }); + + test('an answer-casted event for another poll leaves the list unchanged', + () async { + final (:controller, :events) = await _loadedController(); + + events.add( + Event( + type: EventType.pollAnswerCasted, + pollVote: PollVote( + id: 'answer-1', pollId: _otherPollId, answerText: 'Beach!'), + ), + ); + await pumpEventQueue(); + + expect(controller.currentItems, isEmpty); + }); + + test('a vote-changed event for another poll leaves the list unchanged', + () async { + final (:controller, :events) = await _loadedController(); + + events.add( + Event( + type: EventType.pollVoteChanged, + pollVote: + PollVote(id: 'vote-2', pollId: _otherPollId, optionId: 'option-2'), + ), + ); + await pumpEventQueue(); + + expect(controller.currentItems, isEmpty); + }); + + test('a vote-casted event matching only by its event poll is added', + () async { + final (:controller, :events) = await _loadedController(); + final vote = PollVote(id: 'vote-1', optionId: 'option-1'); + + events.add( + Event( + type: EventType.pollVoteCasted, + poll: Poll(id: _pollId, name: 'Lunch', options: const []), + pollVote: vote, + ), + ); + await pumpEventQueue(); + + expect(controller.currentItems, [vote]); + }); + + test('a vote-casted event with no poll id leaves the list unchanged', + () async { + final (:controller, :events) = await _loadedController(); + + events.add( + Event( + type: EventType.pollVoteCasted, + pollVote: PollVote(id: 'vote-2', optionId: 'option-2'), + ), + ); + await pumpEventQueue(); + + expect(controller.currentItems, isEmpty); + }); +} + +// Returns a controller for [_pollId] that has loaded [votes] and listens to +// the returned channel events. +Future< + ({ + StreamPollVoteListController controller, + StreamController events + })> _loadedController({ + List votes = const [], +}) async { + final channel = MockChannel(); + final events = StreamController.broadcast(); + addTearDown(events.close); + + when(channel.on).thenAnswer((_) => events.stream); + when( + () => channel.queryPollVotes( + _pollId, + sort: any(named: 'sort'), + filter: any(named: 'filter'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer( + (_) async => QueryPollVotesResponse() + ..votes = votes + ..next = null, + ); + + final controller = + StreamPollVoteListController(channel: channel, pollId: _pollId); + addTearDown(controller.dispose); + await controller.doInitialLoad(); + + return (controller: controller, events: events); +}