diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index d9340409b..955056dbd 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3412,6 +3412,35 @@ } }, "donate": "Donate", + "startedAPoll": "{username} started a poll.", + "@startedAPoll": { + "type": "String", + "placeholders": { + "username": { + "type": "String" + } + } + }, + "poll": "Poll", + "startPoll": "Start poll", + "endPoll": "End poll", + "answersVisible": "Answers visible", + "answersHidden": "Answers hidden", + "pollQuestion": "Poll question", + "answerOption": "Answer option", + "addAnswerOption": "Add answer option", + "allowMultipleAnswers": "Allow multiple answers", + "pollHasBeenEnded": "Poll has been ended", + "countVotes": "{count} votes", + "@countVotes": { + "type": "int", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "answersWillBeVisibleWhenPollHasEnded": "Answers will be visible when poll has ended", "ignore": "Block", "ignoredUsers": "Blocked users", "writeAMessageLangCodes": "Type in {l1} or {l2}...", diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 69e97a59a..2b51183bb 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -23,6 +23,7 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat_view.dart'; import 'package:fluffychat/pages/chat/event_info_dialog.dart'; import 'package:fluffychat/pages/chat/events/audio_player.dart'; +import 'package:fluffychat/pages/chat/start_poll_bottom_sheet.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_chat_controller.dart'; @@ -70,6 +71,7 @@ import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_reques import 'package:fluffychat/pangea/toolbar/message_practice/message_practice_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/tokens_util.dart'; +import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; @@ -1793,26 +1795,34 @@ class ChatController extends State // Pangea# } - void onAddPopupMenuButtonSelected(String choice) { - room.client.getConfig(); // Preload server file configuration. + void onAddPopupMenuButtonSelected(AddPopupMenuActions choice) { + room.client.getConfig(); - if (choice == 'file') { - sendFileAction(); - } - if (choice == 'image') { - sendFileAction(type: FileSelectorType.images); - } - if (choice == 'video') { - sendFileAction(type: FileSelectorType.videos); - } - if (choice == 'camera') { - openCameraAction(); - } - if (choice == 'camera-video') { - openVideoCameraAction(); - } - if (choice == 'location') { - sendLocationAction(); + switch (choice) { + case AddPopupMenuActions.image: + sendFileAction(type: FileSelectorType.images); + return; + case AddPopupMenuActions.video: + sendFileAction(type: FileSelectorType.videos); + return; + case AddPopupMenuActions.file: + sendFileAction(); + return; + case AddPopupMenuActions.poll: + showAdaptiveBottomSheet( + context: context, + builder: (context) => StartPollBottomSheet(room: room), + ); + return; + case AddPopupMenuActions.photoCamera: + openCameraAction(); + return; + case AddPopupMenuActions.videoCamera: + openVideoCameraAction(); + return; + case AddPopupMenuActions.location: + sendLocationAction(); + return; } } @@ -2521,3 +2531,13 @@ class ChatController extends State ); } } + +enum AddPopupMenuActions { + image, + video, + file, + poll, + photoCamera, + videoCamera, + location, +} diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index dc375f0cb..068b90cff 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -132,16 +132,15 @@ // alignment: Alignment.center, // decoration: const BoxDecoration(), // clipBehavior: Clip.hardEdge, -// child: PopupMenuButton( +// child: PopupMenuButton( // useRootNavigator: true, // icon: const Icon(Icons.add_circle_outline), // iconColor: theme.colorScheme.onPrimaryContainer, // onSelected: controller.onAddPopupMenuButtonSelected, -// itemBuilder: (BuildContext context) => -// >[ +// itemBuilder: (BuildContext context) => [ // if (PlatformInfos.isMobile) -// PopupMenuItem( -// value: 'location', +// PopupMenuItem( +// value: AddPopupMenuActions.location, // child: ListTile( // leading: CircleAvatar( // backgroundColor: @@ -154,8 +153,22 @@ // contentPadding: const EdgeInsets.all(0), // ), // ), -// PopupMenuItem( -// value: 'image', +// PopupMenuItem( +// value: AddPopupMenuActions.poll, +// child: ListTile( +// leading: CircleAvatar( +// backgroundColor: +// theme.colorScheme.onPrimaryContainer, +// foregroundColor: +// theme.colorScheme.primaryContainer, +// child: const Icon(Icons.poll_outlined), +// ), +// title: Text(L10n.of(context).startPoll), +// contentPadding: const EdgeInsets.all(0), +// ), +// ), +// PopupMenuItem( +// value: AddPopupMenuActions.image, // child: ListTile( // leading: CircleAvatar( // backgroundColor: @@ -168,8 +181,8 @@ // contentPadding: const EdgeInsets.all(0), // ), // ), -// PopupMenuItem( -// value: 'video', +// PopupMenuItem( +// value: AddPopupMenuActions.video, // child: ListTile( // leading: CircleAvatar( // backgroundColor: @@ -183,8 +196,8 @@ // contentPadding: const EdgeInsets.all(0), // ), // ), -// PopupMenuItem( -// value: 'file', +// PopupMenuItem( +// value: AddPopupMenuActions.file, // child: ListTile( // leading: CircleAvatar( // backgroundColor: @@ -217,8 +230,8 @@ // onSelected: controller.onAddPopupMenuButtonSelected, // iconColor: theme.colorScheme.onPrimaryContainer, // itemBuilder: (context) => [ -// PopupMenuItem( -// value: 'camera-video', +// PopupMenuItem( +// value: AddPopupMenuActions.videoCamera, // child: ListTile( // leading: CircleAvatar( // backgroundColor: @@ -231,8 +244,8 @@ // contentPadding: const EdgeInsets.all(0), // ), // ), -// PopupMenuItem( -// value: 'camera', +// PopupMenuItem( +// value: AddPopupMenuActions.photoCamera, // child: ListTile( // leading: CircleAvatar( // backgroundColor: @@ -394,7 +407,7 @@ // onSelected: (mxid) => _popupMenuButtonSelected(mxid, context), // itemBuilder: (BuildContext context) => clients // .map( -// (client) => PopupMenuItem( +// (client) => PopupMenuItem( // value: client!.userID, // child: FutureBuilder( // future: client.fetchOwnProfile(), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 0f240f108..2a52f7e71 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -130,6 +130,7 @@ class Message extends StatelessWidget { EventTypes.Sticker, EventTypes.Encrypted, EventTypes.CallInvite, + PollEventContent.startType, }.contains(event.type)) { if (event.type.startsWith('m.call.')) { return const SizedBox.shrink(); diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index d8d0eb6df..d29f68469 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/events/poll.dart'; import 'package:fluffychat/pages/chat/events/video_player.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; @@ -285,27 +286,11 @@ class MessageContent extends StatelessWidget { textmessage: default: if (event.redacted) { - return FutureBuilder( - future: event.redactedBecause?.fetchSenderUser(), - builder: (context, snapshot) { - final reason = - event.redactedBecause?.content.tryGet('reason'); - final redactedBy = snapshot.data?.calcDisplayname() ?? - event.redactedBecause?.senderId.localpart ?? - L10n.of(context).user; - return _ButtonContent( - label: reason == null - ? L10n.of(context).redactedBy(redactedBy) - : L10n.of(context).redactedByBecause( - redactedBy, - reason, - ), - icon: '🗑️', - textColor: buttonTextColor.withAlpha(128), - onPressed: () => onInfoTab!(event), - fontSize: fontSize, - ); - }, + return RedactionWidget( + event: event, + buttonTextColor: buttonTextColor, + onInfoTab: onInfoTab, + fontSize: fontSize, ); } var html = AppSettings.renderHtml.value && event.isRichMessage @@ -362,6 +347,21 @@ class MessageContent extends StatelessWidget { ), ); } + case PollEventContent.startType: + if (event.redacted) { + return RedactionWidget( + event: event, + buttonTextColor: buttonTextColor, + onInfoTab: onInfoTab, + fontSize: fontSize, + ); + } + return PollWidget( + event: event, + timeline: timeline, + textColor: textColor, + linkColor: linkColor, + ); case EventTypes.CallInvite: return FutureBuilder( future: event.fetchSenderUser(), @@ -399,6 +399,46 @@ class MessageContent extends StatelessWidget { } } +class RedactionWidget extends StatelessWidget { + const RedactionWidget({ + super.key, + required this.event, + required this.buttonTextColor, + required this.onInfoTab, + required this.fontSize, + }); + + final Event event; + final Color buttonTextColor; + final void Function(Event p1)? onInfoTab; + final double fontSize; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: event.redactedBecause?.fetchSenderUser(), + builder: (context, snapshot) { + final reason = event.redactedBecause?.content.tryGet('reason'); + final redactedBy = snapshot.data?.calcDisplayname() ?? + event.redactedBecause?.senderId.localpart ?? + L10n.of(context).user; + return _ButtonContent( + label: reason == null + ? L10n.of(context).redactedBy(redactedBy) + : L10n.of(context).redactedByBecause( + redactedBy, + reason, + ), + icon: '🗑️', + textColor: buttonTextColor.withAlpha(128), + onPressed: () => onInfoTab!(event), + fontSize: fontSize, + ); + }, + ); + } +} + class _ButtonContent extends StatelessWidget { final void Function() onPressed; final String label; diff --git a/lib/pages/chat/events/poll.dart b/lib/pages/chat/events/poll.dart new file mode 100644 index 000000000..af948d70f --- /dev/null +++ b/lib/pages/chat/events/poll.dart @@ -0,0 +1,233 @@ +import 'package:flutter/material.dart'; + +import 'package:async/async.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:matrix/matrix.dart' hide Result; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/utils/url_launcher.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; + +class PollWidget extends StatelessWidget { + final Event event; + final Timeline timeline; + final Color textColor; + final Color linkColor; + const PollWidget({ + required this.event, + required this.timeline, + required this.textColor, + required this.linkColor, + super.key, + }); + + void _endPoll(BuildContext context) => showFutureLoadingDialog( + context: context, + future: () => event.endPoll(), + ); + + void _toggleVote( + BuildContext context, + String answerId, + int maxSelection, + ) async { + final userId = event.room.client.userID!; + final answerIds = event.getPollResponses(timeline)[userId] ?? {}; + if (!answerIds.remove(answerId)) { + answerIds.add(answerId); + if (answerIds.length > maxSelection) { + answerIds.clear(); + answerIds.add(answerId); + } + } + + showFutureLoadingDialog( + context: context, + future: () => event.answerPoll(answerIds.toList()), + ); + } + + @override + Widget build(BuildContext context) { + final eventContentResult = Result(() => event.parsedPollEventContent); + final eventContent = eventContentResult.asValue?.value; + if (eventContent == null) { + Logs().w('Invalid poll event', eventContentResult.error); + return const Text('Unable to parse poll event...'); + } + final responses = event.getPollResponses(timeline); + final pollHasBeenEnded = event.getPollHasBeenEnded(timeline); + final canVote = event.room.canSendEvent(PollEventContent.responseType) && + !pollHasBeenEnded; + final maxPolls = responses.length; + final answersVisible = + eventContent.pollStartContent.kind == PollKind.disclosed || + pollHasBeenEnded; + + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Linkify( + text: eventContent.pollStartContent.question.mText, + textScaleFactor: MediaQuery.textScalerOf(context).scale(1), + style: TextStyle( + color: textColor, + fontSize: AppSettings.fontSizeFactor.value * + AppConfig.messageFontSize, + ), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: linkColor, + fontSize: AppSettings.fontSizeFactor.value * + AppConfig.messageFontSize, + decoration: TextDecoration.underline, + decorationColor: linkColor, + ), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + ), + ), + Divider(color: linkColor.withAlpha(64)), + ...eventContent.pollStartContent.answers.map( + (answer) { + final votedUserIds = responses.entries + .where((entry) => entry.value.contains(answer.id)) + .map((entry) => entry.key) + .toSet(); + return Material( + color: Colors.transparent, + clipBehavior: Clip.hardEdge, + child: CheckboxListTile.adaptive( + value: responses[event.room.client.userID!] + ?.contains(answer.id) ?? + false, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + checkboxScaleFactor: 1.5, + checkboxShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(32), + ), + onChanged: !canVote + ? null + : (_) => _toggleVote( + context, + answer.id, + eventContent.pollStartContent.maxSelections, + ), + title: Text( + answer.mText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: textColor, + fontSize: AppConfig.messageFontSize * + AppSettings.fontSizeFactor.value, + ), + ), + subtitle: answersVisible + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Text( + L10n.of(context) + .countVotes(votedUserIds.length), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: linkColor, + fontSize: + 12 * AppSettings.fontSizeFactor.value, + ), + ), + const SizedBox(width: 2), + ...votedUserIds.map((userId) { + final user = event.room + .getState(EventTypes.RoomMember, userId) + ?.asUser(event.room); + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 2.0, + ), + child: Avatar( + mxContent: user?.avatarUrl, + name: user?.calcDisplayname() ?? + userId.localpart, + size: 12 * + AppSettings.fontSizeFactor.value, + ), + ); + }), + const SizedBox(width: 2), + ], + ), + ), + LinearProgressIndicator( + color: linkColor, + backgroundColor: linkColor.withAlpha(128), + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + value: maxPolls == 0 + ? 0 + : votedUserIds.length / maxPolls, + ), + ], + ) + : null, + ), + ); + }, + ), + if (!pollHasBeenEnded && event.senderId == event.room.client.userID) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: OutlinedButton( + onPressed: () => _endPoll(context), + style: OutlinedButton.styleFrom( + foregroundColor: linkColor, + side: BorderSide(color: linkColor.withAlpha(64)), + ), + child: Text(L10n.of(context).endPoll), + ), + ) + else if (!answersVisible) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + L10n.of(context).answersWillBeVisibleWhenPollHasEnded, + style: TextStyle( + color: linkColor, + fontSize: 12 * AppSettings.fontSizeFactor.value, + fontStyle: FontStyle.italic, + ), + ), + ) + else if (pollHasBeenEnded) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + L10n.of(context).pollHasBeenEnded, + style: TextStyle( + color: linkColor, + fontSize: 12 * AppSettings.fontSizeFactor.value, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat/start_poll_bottom_sheet.dart b/lib/pages/chat/start_poll_bottom_sheet.dart new file mode 100644 index 000000000..9c16c13ea --- /dev/null +++ b/lib/pages/chat/start_poll_bottom_sheet.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; + +class StartPollBottomSheet extends StatefulWidget { + final Room room; + const StartPollBottomSheet({required this.room, super.key}); + + @override + State createState() => _StartPollBottomSheetState(); +} + +class _StartPollBottomSheetState extends State { + final TextEditingController _bodyController = TextEditingController(); + bool _allowMultipleAnswers = false; + final List _answers = [ + TextEditingController(), + TextEditingController(), + ]; + PollKind _pollKind = PollKind.disclosed; + + bool _canCreate = false; + + bool isLoading = false; + + String? _txid; + + void _createPoll() async { + try { + var id = 0; + _txid ??= widget.room.client.generateUniqueTransactionId(); + await widget.room.startPoll( + question: _bodyController.text.trim(), + answers: _answers + .map( + (answerController) => PollAnswer( + id: (++id).toString(), + mText: answerController.text.trim(), + ), + ) + .toList(), + kind: _pollKind, + maxSelections: _allowMultipleAnswers ? _answers.length : 1, + txid: _txid, + ); + Navigator.of(context).pop(); + } catch (e, s) { + Logs().w('Unable to create poll', e, s); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toLocalizedString(context))), + ); + } + } + + void _updateCanCreate([_]) { + final newCanCreate = _bodyController.text.trim().isNotEmpty && + !_answers.any((controller) => controller.text.trim().isEmpty); + if (_canCreate != newCanCreate) { + setState(() { + _canCreate = newCanCreate; + }); + } + } + + @override + Widget build(BuildContext context) { + const maxAnswers = 10; + return Scaffold( + appBar: AppBar( + leading: CloseButton( + onPressed: Navigator.of(context).pop, + ), + title: Text(L10n.of(context).startPoll), + ), + body: ListView( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + children: [ + SegmentedButton( + selected: {_pollKind}, + multiSelectionEnabled: false, + onSelectionChanged: (pollKind) => setState(() { + _pollKind = pollKind.first; + }), + segments: [ + ButtonSegment( + value: PollKind.disclosed, + label: Text( + L10n.of(context).answersVisible, + ), + ), + ButtonSegment( + value: PollKind.undisclosed, + label: Text( + L10n.of(context).answersHidden, + ), + ), + ], + ), + const SizedBox(height: 32), + TextField( + controller: _bodyController, + minLines: 1, + maxLines: 4, + maxLength: 512, + onChanged: _updateCanCreate, + decoration: InputDecoration( + hintText: L10n.of(context).pollQuestion, + counter: const SizedBox.shrink(), + ), + ), + const Divider(height: 32), + ..._answers.map( + (answerController) => Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: TextField( + controller: answerController, + onChanged: _updateCanCreate, + maxLength: 64, + decoration: InputDecoration( + counter: const SizedBox.shrink(), + hintText: L10n.of(context).answerOption, + suffixIcon: _answers.length == 2 + ? null + : IconButton( + icon: const Icon(Icons.cancel_outlined), + onPressed: () => setState(() { + _answers.remove(answerController..dispose()); + }), + ), + ), + ), + ), + ), + Align( + alignment: Alignment.centerLeft, + child: TextButton.icon( + icon: const Icon(Icons.add_outlined), + onPressed: _answers.length < maxAnswers + ? () => setState(() { + _answers.add(TextEditingController()); + }) + : null, + label: Text(L10n.of(context).addAnswerOption), + ), + ), + ListTile( + contentPadding: EdgeInsets.zero, + leading: Switch.adaptive( + value: _allowMultipleAnswers, + onChanged: (allow) => setState(() { + _allowMultipleAnswers = allow; + }), + ), + title: Text(L10n.of(context).allowMultipleAnswers), + ), + const SizedBox(height: 8), + ElevatedButton( + onPressed: !isLoading && _canCreate ? _createPoll : null, + child: isLoading + ? const LinearProgressIndicator() + : Text(L10n.of(context).startPoll), + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat_access_settings/chat_access_settings_controller.dart b/lib/pages/chat_access_settings/chat_access_settings_controller.dart index 6ddd95894..163bbf13c 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_controller.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_controller.dart @@ -1,3 +1,8 @@ +import 'package:flutter/material.dart' hide Visibility; + +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/chat_settings/pages/pangea_chat_access_settings.dart'; import 'package:fluffychat/pangea/extensions/join_rule_extension.dart'; @@ -7,9 +12,6 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog. import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart' hide Visibility; -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; class ChatAccessSettings extends StatefulWidget { final String roomId; diff --git a/lib/pangea/chat/widgets/pangea_chat_input_row.dart b/lib/pangea/chat/widgets/pangea_chat_input_row.dart index 62231dfe7..0cfad5355 100644 --- a/lib/pangea/chat/widgets/pangea_chat_input_row.dart +++ b/lib/pangea/chat/widgets/pangea_chat_input_row.dart @@ -81,15 +81,29 @@ class PangeaChatInputRow extends StatelessWidget { alignment: Alignment.center, clipBehavior: Clip.hardEdge, decoration: const BoxDecoration(), - child: PopupMenuButton( + child: PopupMenuButton( useRootNavigator: true, icon: const Icon(Icons.add_outlined), onSelected: controller.onAddPopupMenuButtonSelected, itemBuilder: (BuildContext context) => - >[ + >[ + PopupMenuItem( + value: AddPopupMenuActions.poll, + child: ListTile( + leading: CircleAvatar( + backgroundColor: + theme.colorScheme.onPrimaryContainer, + foregroundColor: + theme.colorScheme.primaryContainer, + child: const Icon(Icons.poll_outlined), + ), + title: Text(L10n.of(context).startPoll), + contentPadding: const EdgeInsets.all(0), + ), + ), if (!isBotDM) - PopupMenuItem( - value: 'file', + PopupMenuItem( + value: AddPopupMenuActions.file, child: ListTile( leading: const CircleAvatar( backgroundColor: Colors.green, @@ -100,8 +114,8 @@ class PangeaChatInputRow extends StatelessWidget { contentPadding: const EdgeInsets.all(0), ), ), - PopupMenuItem( - value: 'image', + PopupMenuItem( + value: AddPopupMenuActions.image, child: ListTile( leading: const CircleAvatar( backgroundColor: Colors.blue, @@ -112,9 +126,24 @@ class PangeaChatInputRow extends StatelessWidget { contentPadding: const EdgeInsets.all(0), ), ), + if (!isBotDM) + PopupMenuItem( + value: AddPopupMenuActions.image, + child: ListTile( + leading: CircleAvatar( + backgroundColor: + theme.colorScheme.onPrimaryContainer, + foregroundColor: + theme.colorScheme.primaryContainer, + child: const Icon(Icons.photo_outlined), + ), + title: Text(L10n.of(context).sendImage), + contentPadding: const EdgeInsets.all(0), + ), + ), if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'camera', + PopupMenuItem( + value: AddPopupMenuActions.photoCamera, child: ListTile( leading: const CircleAvatar( backgroundColor: Colors.purple, @@ -127,8 +156,8 @@ class PangeaChatInputRow extends StatelessWidget { ), if (!isBotDM) if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'camera-video', + PopupMenuItem( + value: AddPopupMenuActions.videoCamera, child: ListTile( leading: const CircleAvatar( backgroundColor: Colors.red, @@ -143,8 +172,8 @@ class PangeaChatInputRow extends StatelessWidget { ), if (!isBotDM) if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'location', + PopupMenuItem( + value: AddPopupMenuActions.location, child: ListTile( leading: const CircleAvatar( backgroundColor: Colors.brown, diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index 4df40fc0c..f10290257 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -25,8 +25,8 @@ extension IsStateExtension on Event { // always filter out edit and reaction relationships !{RelationshipTypes.edit, RelationshipTypes.reaction} .contains(relationshipType) && - // always filter out m.key.* events - !type.startsWith('m.key.verification.') && + // always filter out m.key.* and other known but unimportant events + !isKnownHiddenStates && // event types to hide: redaction and reaction events // if a reaction has been redacted we also want it to be hidden in the timeline !{EventTypes.Reaction, EventTypes.Redaction}.contains(type) && @@ -57,6 +57,12 @@ extension IsStateExtension on Event { EventTypes.RoomTombstone, }.contains(type); + bool get isKnownHiddenStates => + { + PollEventContent.responseType, + }.contains(type) || + type.startsWith('m.key.verification.'); + // #Pangea bool get isVisibleInPangeaGui { if (!room.showActivityChatUI) { @@ -82,6 +88,7 @@ extension IsStateExtension on Event { EventTypes.RoomMember, EventTypes.RoomTombstone, EventTypes.CallInvite, + PollEventContent.startType, PangeaEventTypes.activityPlan, PangeaEventTypes.activityRole, }; diff --git a/lib/utils/matrix_sdk_extensions/matrix_locals.dart b/lib/utils/matrix_sdk_extensions/matrix_locals.dart index f5bb236f6..1d3fba848 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_locals.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_locals.dart @@ -366,4 +366,10 @@ class MatrixLocals extends MatrixLocalizations { @override String get refreshingLastEvent => l10n.loadingPleaseWait; + + @override + String startedAPoll(String senderName) => '$senderName started a poll'; + + @override + String get pollHasBeenEnded => l10n.pollHasBeenEnded; } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 8507a72ef..b125731ee 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -180,7 +180,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e - desktop_drop: 248706031734554504f939cab1ad4c5fbc9c9c72 + desktop_drop: 10a3e6a7fa9dbe350541f2574092fecfa345a07b device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 dynamic_color: cb7c2a300ee67ed3bd96c3e852df3af0300bf610 emoji_picker_flutter: 51ca408e289d84d1e460016b2a28721ec754fcf7 diff --git a/pubspec.lock b/pubspec.lock index 61ddc3288..9dac3e67c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1508,7 +1508,7 @@ packages: path: "/Users/ggurdin/pangea/matrix-dart-sdk" relative: false source: path - version: "3.0.0" + version: "3.0.2" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c65e3bd39..60ac7a713 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: # matrix: #^3.0.1 # git: # url: https://github.com/famedly/matrix-dart-sdk.git - # ref: krille/refactor-update-user-device-keys + # ref: main matrix: path: /Users/ggurdin/pangea/matrix-dart-sdk # Pangea#