From 7d9d03deb1a85251d86af87eecb6b171e82955ea Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sun, 11 Jan 2026 13:41:05 -0500 Subject: [PATCH] wip polls --- lib/controllers/room_chat_controller.dart | 12 +-- lib/helpers/extensions/event_to_message.dart | 16 ++- lib/helpers/extensions/list_to_messages.dart | 7 +- lib/widgets/chat_page/room_chat.dart | 104 +++++++++++++++++++ pubspec.lock | 66 +++++++++++- pubspec.yaml | 4 +- 6 files changed, 195 insertions(+), 14 deletions(-) diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index e9bd7ba..9175e36 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -16,7 +16,7 @@ class RoomChatController extends AsyncNotifier { @override Future 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 { 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 { ); return InMemoryChatController( - messages: await response.events.toMessages(room), + messages: await timeline.events.toMessages(room, timeline), ); } @@ -87,18 +87,18 @@ class RoomChatController extends AsyncNotifier { Future 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(); diff --git a/lib/helpers/extensions/event_to_message.dart b/lib/helpers/extensions/event_to_message.dart index 2481388..5591952 100644 --- a/lib/helpers/extensions/event_to_message.dart +++ b/lib/helpers/extensions/event_to_message.dart @@ -3,7 +3,8 @@ import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:matrix/matrix.dart"; extension EventToMessage on Event { - Future toMessage({ + Future 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, diff --git a/lib/helpers/extensions/list_to_messages.dart b/lib/helpers/extensions/list_to_messages.dart index c14618b..edddb25 100644 --- a/lib/helpers/extensions/list_to_messages.dart +++ b/lib/helpers/extensions/list_to_messages.dart @@ -3,7 +3,8 @@ import "package:matrix/matrix.dart"; import "package:nexus/helpers/extensions/event_to_message.dart"; extension ListToMessages on List { - Future> toMessages(Room room) async => (await Future.wait( - map((event) => Event.fromMatrixEvent(event, room).toMessage()), - )).nonNulls.toList().reversed.toList(); + Future> toMessages(Room room, Timeline timeline) async => + (await Future.wait( + map((event) => Event.fromMatrixEvent(event, room).toMessage(timeline)), + )).nonNulls.toList().reversed.toList(); } diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index 28df7a4..5b14543 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -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 + >) + .values + .expand((set) => set) + .fold({}, ( + 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, diff --git a/pubspec.lock b/pubspec.lock index c24a860..b299330 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index 2e3ddfd..c8d465c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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