wip polls

This commit is contained in:
Henry Hiles 2026-01-11 13:41:05 -05:00
commit 7d9d03deb1
No known key found for this signature in database
6 changed files with 195 additions and 14 deletions

View file

@ -16,7 +16,7 @@ class RoomChatController extends AsyncNotifier<ChatController> {
@override
Future<ChatController> build() async {
final response = await ref.watch(EventsController.provider(room).future);
final timeline = await ref.watch(EventsController.provider(room).future);
ref.onDispose(
room.client.onTimelineEvent.stream.listen((event) async {
@ -31,7 +31,7 @@ class RoomChatController extends AsyncNotifier<ChatController> {
await controller.removeMessage(message);
} else {
final message = await event.toMessage(includeEdits: true);
final message = await event.toMessage(includeEdits: true, timeline);
if (event.relationshipType == RelationshipTypes.edit) {
final controller = await future;
final oldMessage = controller.messages.firstWhereOrNull(
@ -60,7 +60,7 @@ class RoomChatController extends AsyncNotifier<ChatController> {
);
return InMemoryChatController(
messages: await response.events.toMessages(room),
messages: await timeline.events.toMessages(room, timeline),
);
}
@ -87,18 +87,18 @@ class RoomChatController extends AsyncNotifier<ChatController> {
Future<void> loadOlder() async {
final currentEvents = await future;
await ref.watch(EventsController.provider(room).notifier).prev();
final newEvents = await ref.watch(EventsController.provider(room).future);
final timeline = await ref.watch(EventsController.provider(room).future);
final controller = await future;
await controller.insertAllMessages(
await newEvents.events
await timeline.events
.where(
(event) => !currentEvents.messages.any(
(existingEvent) => existingEvent.id == event.eventId,
),
)
.toList()
.toMessages(room),
.toMessages(room, timeline),
index: 0,
);
ref.notifyListeners();

View file

@ -3,7 +3,8 @@ import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:matrix/matrix.dart";
extension EventToMessage on Event {
Future<Message?> toMessage({
Future<Message?> toMessage(
Timeline timeline, {
bool mustBeText = false,
bool includeEdits = false,
}) async {
@ -25,7 +26,7 @@ extension EventToMessage on Event {
event.content["formatted_body"] ??
event.content["body"] ??
"",
"reply": await replyEvent?.toMessage(mustBeText: true),
"reply": await replyEvent?.toMessage(mustBeText: true, timeline),
"body": newContent?["body"] ?? event.content["body"],
"eventType": event.type,
"avatarUrl": sender.avatarUrl.toString(),
@ -65,12 +66,21 @@ extension EventToMessage on Event {
as TextMessage;
if (mustBeText) return asText;
return switch (type) {
EventTypes.Encrypted => asText.copyWith(
text: "Unable to decrypt message.",
metadata: {...metadata, "formatted": "Unable to decrypt message."},
),
PollEventContent.startType => Message.custom(
metadata: {
...metadata,
"poll": event.parsedPollEventContent.pollStartContent,
"responses": event.getPollResponses(timeline),
},
id: eventId,
deliveredAt: originServerTs,
authorId: senderId,
),
(EventTypes.Sticker || EventTypes.Message) => switch (messageType) {
(MessageTypes.Sticker || MessageTypes.Image) => Message.image(
metadata: metadata,

View file

@ -3,7 +3,8 @@ import "package:matrix/matrix.dart";
import "package:nexus/helpers/extensions/event_to_message.dart";
extension ListToMessages on List<MatrixEvent> {
Future<List<Message>> toMessages(Room room) async => (await Future.wait(
map((event) => Event.fromMatrixEvent(event, room).toMessage()),
Future<List<Message>> toMessages(Room room, Timeline timeline) async =>
(await Future.wait(
map((event) => Event.fromMatrixEvent(event, room).toMessage(timeline)),
)).nonNulls.toList().reversed.toList();
}

View file

@ -3,11 +3,13 @@ import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_chat_ui/flutter_chat_ui.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:flutter_link_previewer/flutter_link_previewer.dart";
import "package:flutter_polls/flutter_polls.dart";
import "package:flyer_chat_file_message/flyer_chat_file_message.dart";
import "package:flyer_chat_image_message/flyer_chat_image_message.dart";
import "package:flyer_chat_system_message/flyer_chat_system_message.dart";
import "package:flyer_chat_text_message/flyer_chat_text_message.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:matrix/matrix.dart";
import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/controllers/room_chat_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
@ -236,6 +238,108 @@ class RoomChat extends HookConsumerWidget {
replyToMessage.value = null,
room: room.roomData,
),
customMessageBuilder:
(
context,
message,
index, {
required bool isSentByMe,
MessageGroupStatus? groupStatus,
}) {
final poll =
message.metadata?["poll"]
as PollStartContent;
final responses =
(message.metadata?["responses"]
as Map<
String,
Set<String>
>)
.values
.expand((set) => set)
.fold(<String, int>{}, (
acc,
value,
) {
acc[value] =
(acc[value] ?? 0) + 1;
return acc;
});
return Container(
decoration: BoxDecoration(
border: Border.all(
color: theme
.colorScheme
.primaryContainer,
width: 4,
),
borderRadius:
BorderRadius.circular(12),
),
padding: EdgeInsets.all(8),
width: 500,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
TopWidget(
message,
headers: room
.roomData
.client
.headers,
groupStatus: groupStatus,
),
FlutterPolls(
votedCheckmark: const Icon(
Icons
.check_circle_outline_rounded,
size: 16,
),
pollId: message.id,
onVoted:
(
pollOption,
newTotalVotes,
) async {
return true;
},
pollOptionsSplashColor: theme
.colorScheme
.primaryContainer,
voteInProgressColor: theme
.colorScheme
.primaryContainer,
leadingVotedProgessColor:
theme
.colorScheme
.primaryContainer,
votedBackgroundColor: theme
.colorScheme
.surfaceContainer,
pollTitle: Text(
poll.question.mText,
),
pollOptions: poll.answers
.map(
(option) => PollOption(
title: Text(
option.mText,
),
votes:
responses[option
.id] ??
0,
),
)
.toList(),
),
],
),
);
},
textMessageBuilder:
(
context,

View file

@ -57,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.7.0"
asn1lib:
dependency: transitive
description:
name: asn1lib
sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024"
url: "https://pub.dev"
source: hosted
version: "1.6.5"
async:
dependency: transitive
description:
@ -161,6 +169,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
@ -337,6 +353,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
dynamic_polls:
dependency: "direct main"
description:
name: dynamic_polls
sha256: fba71ee6fb0ae8f3bebf7d07b3f2a79347d496956de88fb24d3daa32d47e0774
url: "https://pub.dev"
source: hosted
version: "0.0.6"
dynamic_system_colors:
dependency: "direct main"
description:
@ -345,6 +369,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.8.0"
encrypt:
dependency: transitive
description:
name: encrypt
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
fake_async:
dependency: transitive
description:
@ -664,6 +696,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
get_x_storage:
dependency: transitive
description:
name: get_x_storage
sha256: c9c65de2baa228783f46a55137538dc599a3c9b1834130cfd3b417ec3b643813
url: "https://pub.dev"
source: hosted
version: "0.0.8"
glob:
dependency: transitive
description:
@ -1064,6 +1104,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev"
source: hosted
version: "3.9.1"
pool:
dependency: transitive
description:
@ -1485,6 +1533,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
universal_html:
dependency: transitive
description:
name: universal_html
sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4
url: "https://pub.dev"
source: hosted
version: "2.3.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
url: "https://pub.dev"
source: hosted
version: "2.3.1"
unorm_dart:
dependency: transitive
description:
@ -1720,5 +1784,5 @@ packages:
source: hosted
version: "2.2.3"
sdks:
dart: ">=3.10.0 <4.0.0"
dart: ">=3.10.4 <4.0.0"
flutter: ">=3.35.0"

View file

@ -19,13 +19,13 @@ dependency_overrides:
path: dart
analyzer: ^8.4.0
source_gen: ^4.0.2
flutter_hooks: ^0.21.2
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
flutter_hooks: ^0.21.2
flutter_riverpod: ^3.0.3
hooks_riverpod: ^3.0.3
intl: ^0.20.1
@ -71,6 +71,8 @@ dependencies:
mention_tag_text_field: ^0.0.9
fluttertagger: ^2.3.1
flutter_secure_storage: ^10.0.0
dynamic_polls: ^0.0.6
flutter_hooks: ^0.21.3+1
dev_dependencies:
build_runner: ^2.4.11