Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(persistence): add support for polls and poll votes #2060

Open
wants to merge 3 commits into
base: feat/poll-message-widget
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/stream_chat/lib/src/core/models/poll.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ const _nullConst = _NullConst();
/// {@endtemplate}
enum VotingVisibility {
/// The voting process is anonymous.
@JsonValue('anonymous')
anonymous,

/// The voting process is public.
@JsonValue('public')
public,
}

Expand Down
22 changes: 22 additions & 0 deletions packages/stream_chat/lib/src/core/models/poll_vote.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,28 @@ class PollVote extends Equatable {
/// Serialize to json
Map<String, dynamic> toJson() => _$PollVoteToJson(this);

/// Creates a copy of [PollVote] with specified attributes overridden.
PollVote copyWith({
String? id,
String? pollId,
String? optionId,
String? answerText,
DateTime? createdAt,
DateTime? updatedAt,
String? userId,
User? user,
}) =>
PollVote(
id: id ?? this.id,
pollId: pollId ?? this.pollId,
optionId: optionId ?? this.optionId,
answerText: answerText ?? this.answerText,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
userId: userId ?? this.userId,
user: user ?? this.user,
);

@override
List<Object?> get props => [
id,
Expand Down
126 changes: 83 additions & 43 deletions packages/stream_chat/lib/src/db/chat_persistence_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import 'package:stream_chat/src/core/models/event.dart';
import 'package:stream_chat/src/core/models/filter.dart';
import 'package:stream_chat/src/core/models/member.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/core/models/poll.dart';
import 'package:stream_chat/src/core/models/poll_vote.dart';
import 'package:stream_chat/src/core/models/reaction.dart';
import 'package:stream_chat/src/core/models/read.dart';
import 'package:stream_chat/src/core/models/user.dart';
Expand Down Expand Up @@ -169,6 +171,12 @@ abstract class ChatPersistenceClient {
/// Updates all the channels using the new [channels] data.
Future<void> updateChannels(List<ChannelModel> channels);

/// Updates all the polls using the new [polls] data.
Future<void> updatePolls(List<Poll> polls);

/// Deletes all the polls by [pollIds].
Future<void> deletePollsByIds(List<String> pollIds);

/// Updates all the members of a particular channle [cid]
/// with the new [members] data
Future<void> updateMembers(String cid, List<Member> members) =>
Expand All @@ -194,12 +202,18 @@ abstract class ChatPersistenceClient {
/// Updates the pinned message reactions data with the new [reactions] data
Future<void> updatePinnedMessageReactions(List<Reaction> reactions);

/// Updates the poll votes data with the new [pollVotes] data
Future<void> updatePollVotes(List<PollVote> pollVotes);

/// Deletes all the reactions by [messageIds]
Future<void> deleteReactionsByMessageId(List<String> messageIds);

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

/// Deletes all the poll votes by [pollIds]
Future<void> deletePollVotesByPollIds(List<String> pollIds);

/// Deletes all the members by channel [cids]
Future<void> deleteMembersByCids(List<String> cids);

Expand Down Expand Up @@ -245,50 +259,64 @@ abstract class ChatPersistenceClient {
final reactions = <Reaction>[];
final pinnedReactions = <Reaction>[];

final polls = <Poll>[];
final pollVotes = <PollVote>[];
final pollVotesToDelete = <String>[];

for (final state in channelStates) {
final channel = state.channel;
if (channel != null) {
channels.add(channel);

final cid = channel.cid;
final reads = state.read;
final members = state.members;
final Iterable<Message>? messages;
if (CurrentPlatform.isWeb) {
messages = state.messages?.where(
// Continue if channel is not available.
if (channel == null) continue;
channels.add(channel);

final cid = channel.cid;
final reads = state.read;
final members = state.members;
final messages = switch (CurrentPlatform.isWeb) {
true => state.messages?.where(
(it) => !it.attachments.any(
(it) => it.uploadState != const UploadState.success(),
),
);
} else {
messages = state.messages;
}
final pinnedMessages = state.pinnedMessages;

// Preparing deletion data
membersToDelete.add(cid);
reactionsToDelete.addAll(state.messages?.map((it) => it.id) ?? []);
pinnedReactionsToDelete
.addAll(state.pinnedMessages?.map((it) => it.id) ?? []);

// preparing addition data
channelWithReads[cid] = reads;
channelWithMembers[cid] = members;
channelWithMessages[cid] = messages?.toList();
channelWithPinnedMessages[cid] = pinnedMessages;

reactions.addAll(messages?.expand(_expandReactions) ?? []);
pinnedReactions.addAll(pinnedMessages?.expand(_expandReactions) ?? []);

users.addAll([
channel.createdBy,
...messages?.map((it) => it.user) ?? <User>[],
...reads?.map((it) => it.user) ?? <User>[],
...members?.map((it) => it.user) ?? <User>[],
...reactions.map((it) => it.user),
...pinnedReactions.map((it) => it.user),
].withNullifyer);
}
),
_ => state.messages,
};

final pinnedMessages = state.pinnedMessages;

// Preparing deletion data
membersToDelete.add(cid);
reactionsToDelete.addAll(messages?.map((it) => it.id) ?? []);
pinnedReactionsToDelete.addAll(pinnedMessages?.map((it) => it.id) ?? []);

// preparing addition data
channelWithReads[cid] = reads;
channelWithMembers[cid] = members;
channelWithMessages[cid] = messages?.toList();
channelWithPinnedMessages[cid] = pinnedMessages;

reactions.addAll(messages?.expand(_expandReactions) ?? []);
pinnedReactions.addAll(pinnedMessages?.expand(_expandReactions) ?? []);

polls.addAll([
...?messages?.map((it) => it.poll),
...?pinnedMessages?.map((it) => it.poll),
].withNullifyer);

pollVotesToDelete.addAll(polls.map((it) => it.id));

pollVotes.addAll(polls.expand(_expandPollVotes));

users.addAll([
channel.createdBy,
...?messages?.map((it) => it.user),
...?pinnedMessages?.map((it) => it.user),
...?reads?.map((it) => it.user),
...?members?.map((it) => it.user),
...reactions.map((it) => it.user),
...pinnedReactions.map((it) => it.user),
...polls.map((it) => it.createdBy),
...pollVotes.map((it) => it.user),
].withNullifyer);
}

// Removing old members and reactions data as they may have
Expand All @@ -297,12 +325,14 @@ abstract class ChatPersistenceClient {
deleteMembersByCids(membersToDelete),
deleteReactionsByMessageId(reactionsToDelete),
deletePinnedMessageReactionsByMessageId(pinnedReactionsToDelete),
deletePollVotesByPollIds(pollVotesToDelete),
]);

// Updating first as does not depend on any other table.
await Future.wait([
updateUsers(users.toList(growable: false)),
updateChannels(channels.toList(growable: false)),
updatePolls(polls.toList(growable: false)),
]);

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

// Both has a foreign key relation with messages, pinnedMessages table.
await Future.wait([
updateReactions(reactions.toList(growable: false)),
updatePinnedMessageReactions(
pinnedReactions.toList(growable: false),
),
updateReactions(reactions),
updatePinnedMessageReactions(pinnedReactions),
updatePollVotes(pollVotes),
]);
}

Expand All @@ -330,4 +359,15 @@ abstract class ChatPersistenceClient {
if (latest != null) ...latest.where((r) => r.userId != null),
];
}

List<PollVote> _expandPollVotes(Poll poll) {
final latestAnswers = poll.latestAnswers;
final latestVotes = poll.latestVotesByOption.values;
final ownVotesAndAnswers = poll.ownVotesAndAnswers;
return [
...latestAnswers,
...latestVotes.expand((it) => it),
...ownVotesAndAnswers,
];
}
}
24 changes: 24 additions & 0 deletions packages/stream_chat/test/src/db/chat_persistence_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import 'package:stream_chat/src/core/models/event.dart';
import 'package:stream_chat/src/core/models/filter.dart';
import 'package:stream_chat/src/core/models/member.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/core/models/poll.dart';
import 'package:stream_chat/src/core/models/poll_vote.dart';
import 'package:stream_chat/src/core/models/reaction.dart';
import 'package:stream_chat/src/core/models/read.dart';
import 'package:stream_chat/src/core/models/user.dart';
Expand Down Expand Up @@ -49,6 +51,9 @@ class TestPersistenceClient extends ChatPersistenceClient {
List<String> messageIds) =>
Future.value();

@override
Future<void> deletePollVotesByPollIds(List<String> pollIds) => Future.value();

@override
Future<void> disconnect({bool flush = false}) => throw UnimplementedError();

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

@override
Future<void> updatePollVotes(List<PollVote> pollVotes) => Future.value();

@override
Future<void> updateUsers(List<User> users) => Future.value();

Expand All @@ -137,6 +145,12 @@ class TestPersistenceClient extends ChatPersistenceClient {
@override
Future<void> bulkUpdateReads(Map<String, List<Read>?> reads) =>
Future.value();

@override
Future<void> deletePollsByIds(List<String> pollIds) => Future.value();

@override
Future<void> updatePolls(List<Poll> polls) => Future.value();
}

void main() {
Expand Down Expand Up @@ -169,6 +183,16 @@ void main() {
expect(channelState, isNotNull);
});

test('deletePollsByIds', () {
const pollIds = ['poll-id'];
persistenceClient.deletePollsByIds(pollIds);
});

test('updatePolls', () async {
final poll = Poll(id: 'poll-id', name: 'poll-name', options: const []);
persistenceClient.updatePolls([poll]);
});

test('updateChannelThreads', () async {
const cid = 'test:cid';
final user = User(id: 'test-user-id');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:stream_chat_flutter/src/poll/interactor/poll_header.dart';
Expand Down
10 changes: 6 additions & 4 deletions packages/stream_chat_persistence/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Upcoming

- Added support for `Poll` and `PollVote` entities in the database.

## 8.3.0

- Updated `stream_chat` dependency to [`8.3.0`](https://pub.dev/packages/stream_chat/changelog).
Expand Down Expand Up @@ -69,8 +73,7 @@

## 6.7.0

- [[#1683]](https://github.com/GetStream/stream-chat-flutter/issues/1683) Fixed SqliteException no such
column `messages.state`.
- [[#1683]](https://github.com/GetStream/stream-chat-flutter/issues/1683) Fixed SqliteException no such column `messages.state`.
- Updated `stream_chat` dependency to [`6.7.0`](https://pub.dev/packages/stream_chat/changelog).

## 6.6.0
Expand Down Expand Up @@ -165,8 +168,7 @@
## 3.0.0

- Updated `stream_chat` dependency to [`3.0.0`](https://pub.dev/packages/stream_chat/changelog).
- [[#604]](https://github.com/GetStream/stream-chat-flutter/issues/604) Fix cascade deletion by
enabling `pragma foreign_keys`.
- [[#604]](https://github.com/GetStream/stream-chat-flutter/issues/604) Fix cascade deletion by enabling `pragma foreign_keys`.
- Added a new table `PinnedMessageReactions` and dao `PinnedMessageReactionDao` specifically for pinned messages.

## 2.2.0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export 'list_converter.dart';
export 'map_converter.dart';
export 'voting_visibility_converter.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';

/// A [TypeConverter] that serializes [VotingVisibility] to a [String] column.
class VotingVisibilityConverter
extends TypeConverter<VotingVisibility, String> {
/// Constant default constructor.
const VotingVisibilityConverter();

@override
VotingVisibility fromSql(String fromDb) {
for (final entry in _votingVisibilityEnumMap.entries) {
if (entry.value == fromDb) {
return entry.key;
}
}

throw ArgumentError(
'`$fromDb` is not one of the supported values: '
'${_votingVisibilityEnumMap.values.join(', ')}',
);
}

@override
String toSql(VotingVisibility value) {
return _votingVisibilityEnumMap[value]!;
}
}

const _votingVisibilityEnumMap = {
VotingVisibility.anonymous: 'anonymous',
VotingVisibility.public: 'public',
};
2 changes: 2 additions & 0 deletions packages/stream_chat_persistence/lib/src/dao/dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export 'member_dao.dart';
export 'message_dao.dart';
export 'pinned_message_dao.dart';
export 'pinned_message_reaction_dao.dart';
export 'poll_dao.dart';
export 'poll_vote_dao.dart';
export 'reaction_dao.dart';
export 'read_dao.dart';
export 'user_dao.dart';
6 changes: 6 additions & 0 deletions packages/stream_chat_persistence/lib/src/dao/message_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,18 @@ class MessageDao extends DatabaseAccessor<DriftChatDatabase>
if (quotedMessageId != null) {
quotedMessage = await getMessageById(quotedMessageId);
}
Poll? poll;
final pollId = msgEntity.pollId;
if (pollId != null) {
poll = await _db.pollDao.getPollById(pollId);
}
return msgEntity.toMessage(
user: userEntity?.toUser(),
pinnedBy: pinnedByEntity?.toUser(),
latestReactions: latestReactions,
ownReactions: ownReactions,
quotedMessage: quotedMessage,
poll: poll,
);
}

Expand Down
Loading
Loading