Skip to content

Commit f8f9875

Browse files
committed
feat(persistence): add support for polls and poll votes
1 parent 8e2a19d commit f8f9875

34 files changed

+6194
-2866
lines changed

packages/stream_chat/lib/src/core/models/poll.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ const _nullConst = _NullConst();
2020
/// {@endtemplate}
2121
enum VotingVisibility {
2222
/// The voting process is anonymous.
23+
@JsonValue('anonymous')
2324
anonymous,
2425

2526
/// The voting process is public.
27+
@JsonValue('public')
2628
public,
2729
}
2830

packages/stream_chat/lib/src/core/models/poll_vote.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,28 @@ class PollVote extends Equatable {
7070
/// Serialize to json
7171
Map<String, dynamic> toJson() => _$PollVoteToJson(this);
7272

73+
/// Creates a copy of [PollVote] with specified attributes overridden.
74+
PollVote copyWith({
75+
String? id,
76+
String? pollId,
77+
String? optionId,
78+
String? answerText,
79+
DateTime? createdAt,
80+
DateTime? updatedAt,
81+
String? userId,
82+
User? user,
83+
}) =>
84+
PollVote(
85+
id: id ?? this.id,
86+
pollId: pollId ?? this.pollId,
87+
optionId: optionId ?? this.optionId,
88+
answerText: answerText ?? this.answerText,
89+
createdAt: createdAt ?? this.createdAt,
90+
updatedAt: updatedAt ?? this.updatedAt,
91+
userId: userId ?? this.userId,
92+
user: user ?? this.user,
93+
);
94+
7395
@override
7496
List<Object?> get props => [
7597
id,

packages/stream_chat/lib/src/db/chat_persistence_client.dart

Lines changed: 83 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'package:stream_chat/src/core/models/event.dart';
66
import 'package:stream_chat/src/core/models/filter.dart';
77
import 'package:stream_chat/src/core/models/member.dart';
88
import 'package:stream_chat/src/core/models/message.dart';
9+
import 'package:stream_chat/src/core/models/poll.dart';
10+
import 'package:stream_chat/src/core/models/poll_vote.dart';
911
import 'package:stream_chat/src/core/models/reaction.dart';
1012
import 'package:stream_chat/src/core/models/read.dart';
1113
import 'package:stream_chat/src/core/models/user.dart';
@@ -169,6 +171,12 @@ abstract class ChatPersistenceClient {
169171
/// Updates all the channels using the new [channels] data.
170172
Future<void> updateChannels(List<ChannelModel> channels);
171173

174+
/// Updates all the polls using the new [polls] data.
175+
Future<void> updatePolls(List<Poll> polls);
176+
177+
/// Deletes all the polls by [pollIds].
178+
Future<void> deletePollsByIds(List<String> pollIds);
179+
172180
/// Updates all the members of a particular channle [cid]
173181
/// with the new [members] data
174182
Future<void> updateMembers(String cid, List<Member> members) =>
@@ -194,12 +202,18 @@ abstract class ChatPersistenceClient {
194202
/// Updates the pinned message reactions data with the new [reactions] data
195203
Future<void> updatePinnedMessageReactions(List<Reaction> reactions);
196204

205+
/// Updates the poll votes data with the new [pollVotes] data
206+
Future<void> updatePollVotes(List<PollVote> pollVotes);
207+
197208
/// Deletes all the reactions by [messageIds]
198209
Future<void> deleteReactionsByMessageId(List<String> messageIds);
199210

200211
/// Deletes all the pinned messages reactions by [messageIds]
201212
Future<void> deletePinnedMessageReactionsByMessageId(List<String> messageIds);
202213

214+
/// Deletes all the poll votes by [pollIds]
215+
Future<void> deletePollVotesByPollIds(List<String> pollIds);
216+
203217
/// Deletes all the members by channel [cids]
204218
Future<void> deleteMembersByCids(List<String> cids);
205219

@@ -245,50 +259,64 @@ abstract class ChatPersistenceClient {
245259
final reactions = <Reaction>[];
246260
final pinnedReactions = <Reaction>[];
247261

262+
final polls = <Poll>[];
263+
final pollVotes = <PollVote>[];
264+
final pollVotesToDelete = <String>[];
265+
248266
for (final state in channelStates) {
249267
final channel = state.channel;
250-
if (channel != null) {
251-
channels.add(channel);
252-
253-
final cid = channel.cid;
254-
final reads = state.read;
255-
final members = state.members;
256-
final Iterable<Message>? messages;
257-
if (CurrentPlatform.isWeb) {
258-
messages = state.messages?.where(
268+
// Continue if channel is not available.
269+
if (channel == null) continue;
270+
channels.add(channel);
271+
272+
final cid = channel.cid;
273+
final reads = state.read;
274+
final members = state.members;
275+
final messages = switch (CurrentPlatform.isWeb) {
276+
true => state.messages?.where(
259277
(it) => !it.attachments.any(
260278
(it) => it.uploadState != const UploadState.success(),
261279
),
262-
);
263-
} else {
264-
messages = state.messages;
265-
}
266-
final pinnedMessages = state.pinnedMessages;
267-
268-
// Preparing deletion data
269-
membersToDelete.add(cid);
270-
reactionsToDelete.addAll(state.messages?.map((it) => it.id) ?? []);
271-
pinnedReactionsToDelete
272-
.addAll(state.pinnedMessages?.map((it) => it.id) ?? []);
273-
274-
// preparing addition data
275-
channelWithReads[cid] = reads;
276-
channelWithMembers[cid] = members;
277-
channelWithMessages[cid] = messages?.toList();
278-
channelWithPinnedMessages[cid] = pinnedMessages;
279-
280-
reactions.addAll(messages?.expand(_expandReactions) ?? []);
281-
pinnedReactions.addAll(pinnedMessages?.expand(_expandReactions) ?? []);
282-
283-
users.addAll([
284-
channel.createdBy,
285-
...messages?.map((it) => it.user) ?? <User>[],
286-
...reads?.map((it) => it.user) ?? <User>[],
287-
...members?.map((it) => it.user) ?? <User>[],
288-
...reactions.map((it) => it.user),
289-
...pinnedReactions.map((it) => it.user),
290-
].withNullifyer);
291-
}
280+
),
281+
_ => state.messages,
282+
};
283+
284+
final pinnedMessages = state.pinnedMessages;
285+
286+
// Preparing deletion data
287+
membersToDelete.add(cid);
288+
reactionsToDelete.addAll(messages?.map((it) => it.id) ?? []);
289+
pinnedReactionsToDelete.addAll(pinnedMessages?.map((it) => it.id) ?? []);
290+
291+
// preparing addition data
292+
channelWithReads[cid] = reads;
293+
channelWithMembers[cid] = members;
294+
channelWithMessages[cid] = messages?.toList();
295+
channelWithPinnedMessages[cid] = pinnedMessages;
296+
297+
reactions.addAll(messages?.expand(_expandReactions) ?? []);
298+
pinnedReactions.addAll(pinnedMessages?.expand(_expandReactions) ?? []);
299+
300+
polls.addAll([
301+
...?messages?.map((it) => it.poll),
302+
...?pinnedMessages?.map((it) => it.poll),
303+
].withNullifyer);
304+
305+
pollVotesToDelete.addAll(polls.map((it) => it.id));
306+
307+
pollVotes.addAll(polls.expand(_expandPollVotes));
308+
309+
users.addAll([
310+
channel.createdBy,
311+
...?messages?.map((it) => it.user),
312+
...?pinnedMessages?.map((it) => it.user),
313+
...?reads?.map((it) => it.user),
314+
...?members?.map((it) => it.user),
315+
...reactions.map((it) => it.user),
316+
...pinnedReactions.map((it) => it.user),
317+
...polls.map((it) => it.createdBy),
318+
...pollVotes.map((it) => it.user),
319+
].withNullifyer);
292320
}
293321

294322
// Removing old members and reactions data as they may have
@@ -297,12 +325,14 @@ abstract class ChatPersistenceClient {
297325
deleteMembersByCids(membersToDelete),
298326
deleteReactionsByMessageId(reactionsToDelete),
299327
deletePinnedMessageReactionsByMessageId(pinnedReactionsToDelete),
328+
deletePollVotesByPollIds(pollVotesToDelete),
300329
]);
301330

302331
// Updating first as does not depend on any other table.
303332
await Future.wait([
304333
updateUsers(users.toList(growable: false)),
305334
updateChannels(channels.toList(growable: false)),
335+
updatePolls(polls.toList(growable: false)),
306336
]);
307337

308338
// All has a foreign key relation with channels table.
@@ -315,10 +345,9 @@ abstract class ChatPersistenceClient {
315345

316346
// Both has a foreign key relation with messages, pinnedMessages table.
317347
await Future.wait([
318-
updateReactions(reactions.toList(growable: false)),
319-
updatePinnedMessageReactions(
320-
pinnedReactions.toList(growable: false),
321-
),
348+
updateReactions(reactions),
349+
updatePinnedMessageReactions(pinnedReactions),
350+
updatePollVotes(pollVotes),
322351
]);
323352
}
324353

@@ -330,4 +359,15 @@ abstract class ChatPersistenceClient {
330359
if (latest != null) ...latest.where((r) => r.userId != null),
331360
];
332361
}
362+
363+
List<PollVote> _expandPollVotes(Poll poll) {
364+
final latestAnswers = poll.latestAnswers;
365+
final latestVotes = poll.latestVotesByOption.values;
366+
final ownVotesAndAnswers = poll.ownVotesAndAnswers;
367+
return [
368+
...latestAnswers,
369+
...latestVotes.expand((it) => it),
370+
...ownVotesAndAnswers,
371+
];
372+
}
333373
}

packages/stream_chat/test/src/db/chat_persistence_client_test.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import 'package:stream_chat/src/core/models/event.dart';
55
import 'package:stream_chat/src/core/models/filter.dart';
66
import 'package:stream_chat/src/core/models/member.dart';
77
import 'package:stream_chat/src/core/models/message.dart';
8+
import 'package:stream_chat/src/core/models/poll.dart';
9+
import 'package:stream_chat/src/core/models/poll_vote.dart';
810
import 'package:stream_chat/src/core/models/reaction.dart';
911
import 'package:stream_chat/src/core/models/read.dart';
1012
import 'package:stream_chat/src/core/models/user.dart';
@@ -49,6 +51,9 @@ class TestPersistenceClient extends ChatPersistenceClient {
4951
List<String> messageIds) =>
5052
Future.value();
5153

54+
@override
55+
Future<void> deletePollVotesByPollIds(List<String> pollIds) => Future.value();
56+
5257
@override
5358
Future<void> disconnect({bool flush = false}) => throw UnimplementedError();
5459

@@ -119,6 +124,9 @@ class TestPersistenceClient extends ChatPersistenceClient {
119124
Future<void> updatePinnedMessageReactions(List<Reaction> reactions) =>
120125
Future.value();
121126

127+
@override
128+
Future<void> updatePollVotes(List<PollVote> pollVotes) => Future.value();
129+
122130
@override
123131
Future<void> updateUsers(List<User> users) => Future.value();
124132

@@ -137,6 +145,12 @@ class TestPersistenceClient extends ChatPersistenceClient {
137145
@override
138146
Future<void> bulkUpdateReads(Map<String, List<Read>?> reads) =>
139147
Future.value();
148+
149+
@override
150+
Future<void> deletePollsByIds(List<String> pollIds) => Future.value();
151+
152+
@override
153+
Future<void> updatePolls(List<Poll> polls) => Future.value();
140154
}
141155

142156
void main() {
@@ -169,6 +183,16 @@ void main() {
169183
expect(channelState, isNotNull);
170184
});
171185

186+
test('deletePollsByIds', () {
187+
const pollIds = ['poll-id'];
188+
persistenceClient.deletePollsByIds(pollIds);
189+
});
190+
191+
test('updatePolls', () async {
192+
final poll = Poll(id: 'poll-id', name: 'poll-name', options: const []);
193+
persistenceClient.updatePolls([poll]);
194+
});
195+
172196
test('updateChannelThreads', () async {
173197
const cid = 'test:cid';
174198
final user = User(id: 'test-user-id');

packages/stream_chat_flutter/test/src/poll/interactor/poll_header_test.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import 'package:flutter/foundation.dart';
21
import 'package:flutter/material.dart';
32
import 'package:golden_toolkit/golden_toolkit.dart';
43
import 'package:stream_chat_flutter/src/poll/interactor/poll_header.dart';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export 'list_converter.dart';
22
export 'map_converter.dart';
3+
export 'voting_visibility_converter.dart';
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'package:drift/drift.dart';
2+
import 'package:stream_chat/stream_chat.dart';
3+
4+
/// A [TypeConverter] that serializes [VotingVisibility] to a [String] column.
5+
class VotingVisibilityConverter
6+
extends TypeConverter<VotingVisibility, String> {
7+
/// Constant default constructor.
8+
const VotingVisibilityConverter();
9+
10+
@override
11+
VotingVisibility fromSql(String fromDb) {
12+
for (final entry in _votingVisibilityEnumMap.entries) {
13+
if (entry.value == fromDb) {
14+
return entry.key;
15+
}
16+
}
17+
18+
throw ArgumentError(
19+
'`$fromDb` is not one of the supported values: '
20+
'${_votingVisibilityEnumMap.values.join(', ')}',
21+
);
22+
}
23+
24+
@override
25+
String toSql(VotingVisibility value) {
26+
return _votingVisibilityEnumMap[value]!;
27+
}
28+
}
29+
30+
const _votingVisibilityEnumMap = {
31+
VotingVisibility.anonymous: 'anonymous',
32+
VotingVisibility.public: 'public',
33+
};

packages/stream_chat_persistence/lib/src/dao/dao.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export 'member_dao.dart';
55
export 'message_dao.dart';
66
export 'pinned_message_dao.dart';
77
export 'pinned_message_reaction_dao.dart';
8+
export 'poll_dao.dart';
9+
export 'poll_vote_dao.dart';
810
export 'reaction_dao.dart';
911
export 'read_dao.dart';
1012
export 'user_dao.dart';

packages/stream_chat_persistence/lib/src/dao/message_dao.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,18 @@ class MessageDao extends DatabaseAccessor<DriftChatDatabase>
5050
if (quotedMessageId != null) {
5151
quotedMessage = await getMessageById(quotedMessageId);
5252
}
53+
Poll? poll;
54+
final pollId = msgEntity.pollId;
55+
if (pollId != null) {
56+
poll = await _db.pollDao.getPollById(pollId);
57+
}
5358
return msgEntity.toMessage(
5459
user: userEntity?.toUser(),
5560
pinnedBy: pinnedByEntity?.toUser(),
5661
latestReactions: latestReactions,
5762
ownReactions: ownReactions,
5863
quotedMessage: quotedMessage,
64+
poll: poll,
5965
);
6066
}
6167

packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,18 @@ class PinnedMessageDao extends DatabaseAccessor<DriftChatDatabase>
5151
if (quotedMessageId != null) {
5252
quotedMessage = await getMessageById(quotedMessageId);
5353
}
54+
Poll? poll;
55+
final pollId = msgEntity.pollId;
56+
if (pollId != null) {
57+
poll = await _db.pollDao.getPollById(pollId);
58+
}
5459
return msgEntity.toMessage(
5560
user: userEntity?.toUser(),
5661
pinnedBy: pinnedByEntity?.toUser(),
5762
latestReactions: latestReactions,
5863
ownReactions: ownReactions,
5964
quotedMessage: quotedMessage,
65+
poll: poll,
6066
);
6167
}
6268

0 commit comments

Comments
 (0)