Compare commits
1 commit
main
...
krille/pol
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c7bc747d2 |
11 changed files with 433 additions and 33 deletions
|
|
@ -2821,5 +2821,27 @@
|
||||||
"invalidUrl": "Invalid url",
|
"invalidUrl": "Invalid url",
|
||||||
"addLink": "Add link",
|
"addLink": "Add link",
|
||||||
"unableToJoinChat": "Unable to join chat. Maybe the other party has already closed the conversation.",
|
"unableToJoinChat": "Unable to join chat. Maybe the other party has already closed the conversation.",
|
||||||
"previous": "Previous"
|
"previous": "Previous",
|
||||||
|
"poll": "Poll",
|
||||||
|
"question": "Question",
|
||||||
|
"answer": "Answer",
|
||||||
|
"resultsDisclosed": "Results disclosed",
|
||||||
|
"resultsUndisclosed": "Results undisclosed",
|
||||||
|
"addAnswer": "Add answer",
|
||||||
|
"deleteAnswer": "Delete answer",
|
||||||
|
"startedAPoll": "{sender} started a poll",
|
||||||
|
"@startedAPoll": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {
|
||||||
|
"sender": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"countVotes": "{votes, plural, =1{1 vote} other{{votes} votes}} - {percentage}%",
|
||||||
|
"@countVotes": {
|
||||||
|
"type": "integer",
|
||||||
|
"placeholders": {
|
||||||
|
"votes": {},
|
||||||
|
"percentage": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
import 'package:matrix/msc_extensions/msc_3381_polls/models/poll_event_content.dart';
|
||||||
|
import 'package:matrix/msc_extensions/msc_3381_polls/poll_room_extension.dart';
|
||||||
import 'package:record/record.dart';
|
import 'package:record/record.dart';
|
||||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
@ -21,10 +23,13 @@ import 'package:universal_html/html.dart' as html;
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/config/setting_keys.dart';
|
import 'package:fluffychat/config/setting_keys.dart';
|
||||||
import 'package:fluffychat/config/themes.dart';
|
import 'package:fluffychat/config/themes.dart';
|
||||||
|
import 'package:fluffychat/pages/chat/chat_input_row.dart';
|
||||||
import 'package:fluffychat/pages/chat/chat_view.dart';
|
import 'package:fluffychat/pages/chat/chat_view.dart';
|
||||||
import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
||||||
|
import 'package:fluffychat/pages/chat/poll_edit_bottom_sheet.dart';
|
||||||
import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
||||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||||
|
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||||
import 'package:fluffychat/utils/error_reporter.dart';
|
import 'package:fluffychat/utils/error_reporter.dart';
|
||||||
import 'package:fluffychat/utils/file_selector.dart';
|
import 'package:fluffychat/utils/file_selector.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
||||||
|
|
@ -1117,24 +1122,44 @@ class ChatController extends State<ChatPageWithRoom>
|
||||||
FocusScope.of(context).requestFocus(inputFocus);
|
FocusScope.of(context).requestFocus(inputFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onAddPopupMenuButtonSelected(String choice) {
|
void onAddPopupMenuButtonSelected(AttachmentButtonAction choice) {
|
||||||
if (choice == 'file') {
|
switch (choice) {
|
||||||
sendFileAction();
|
case AttachmentButtonAction.file:
|
||||||
}
|
sendFileAction();
|
||||||
if (choice == 'image') {
|
break;
|
||||||
sendImageAction();
|
case AttachmentButtonAction.image:
|
||||||
}
|
sendImageAction();
|
||||||
if (choice == 'camera') {
|
break;
|
||||||
openCameraAction();
|
case AttachmentButtonAction.camera:
|
||||||
}
|
openCameraAction();
|
||||||
if (choice == 'camera-video') {
|
break;
|
||||||
openVideoCameraAction();
|
case AttachmentButtonAction.video:
|
||||||
}
|
openVideoCameraAction();
|
||||||
if (choice == 'location') {
|
break;
|
||||||
sendLocationAction();
|
case AttachmentButtonAction.location:
|
||||||
|
sendLocationAction();
|
||||||
|
break;
|
||||||
|
case AttachmentButtonAction.poll:
|
||||||
|
sendPollAction();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void sendPollAction() async {
|
||||||
|
final poll = await showAdaptiveBottomSheet<PollStartContent>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const PollEditBottomSheet(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (poll == null) return;
|
||||||
|
await room.startPoll(
|
||||||
|
question: poll.question.mText,
|
||||||
|
answers: poll.answers,
|
||||||
|
maxSelections: poll.maxSelections,
|
||||||
|
kind: poll.kind ?? PollKind.undisclosed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
unpinEvent(String eventId) async {
|
unpinEvent(String eventId) async {
|
||||||
final response = await showOkCancelAlertDialog(
|
final response = await showOkCancelAlertDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
|
||||||
|
|
@ -102,13 +102,12 @@ class ChatInputRow extends StatelessWidget {
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
decoration: const BoxDecoration(),
|
decoration: const BoxDecoration(),
|
||||||
child: PopupMenuButton<String>(
|
child: PopupMenuButton<AttachmentButtonAction>(
|
||||||
icon: const Icon(Icons.add_outlined),
|
icon: const Icon(Icons.add_outlined),
|
||||||
onSelected: controller.onAddPopupMenuButtonSelected,
|
onSelected: controller.onAddPopupMenuButtonSelected,
|
||||||
itemBuilder: (BuildContext context) =>
|
itemBuilder: (BuildContext context) => [
|
||||||
<PopupMenuEntry<String>>[
|
PopupMenuItem(
|
||||||
PopupMenuItem<String>(
|
value: AttachmentButtonAction.file,
|
||||||
value: 'file',
|
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const CircleAvatar(
|
leading: const CircleAvatar(
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
|
|
@ -119,8 +118,8 @@ class ChatInputRow extends StatelessWidget {
|
||||||
contentPadding: const EdgeInsets.all(0),
|
contentPadding: const EdgeInsets.all(0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem(
|
||||||
value: 'image',
|
value: AttachmentButtonAction.image,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const CircleAvatar(
|
leading: const CircleAvatar(
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: Colors.blue,
|
||||||
|
|
@ -132,8 +131,8 @@ class ChatInputRow extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (PlatformInfos.isMobile)
|
if (PlatformInfos.isMobile)
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem(
|
||||||
value: 'camera',
|
value: AttachmentButtonAction.camera,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const CircleAvatar(
|
leading: const CircleAvatar(
|
||||||
backgroundColor: Colors.purple,
|
backgroundColor: Colors.purple,
|
||||||
|
|
@ -145,8 +144,8 @@ class ChatInputRow extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (PlatformInfos.isMobile)
|
if (PlatformInfos.isMobile)
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem(
|
||||||
value: 'camera-video',
|
value: AttachmentButtonAction.video,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const CircleAvatar(
|
leading: const CircleAvatar(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
|
|
@ -158,8 +157,8 @@ class ChatInputRow extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (PlatformInfos.isMobile)
|
if (PlatformInfos.isMobile)
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem(
|
||||||
value: 'location',
|
value: AttachmentButtonAction.location,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const CircleAvatar(
|
leading: const CircleAvatar(
|
||||||
backgroundColor: Colors.brown,
|
backgroundColor: Colors.brown,
|
||||||
|
|
@ -170,6 +169,18 @@ class ChatInputRow extends StatelessWidget {
|
||||||
contentPadding: const EdgeInsets.all(0),
|
contentPadding: const EdgeInsets.all(0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: AttachmentButtonAction.poll,
|
||||||
|
child: ListTile(
|
||||||
|
leading: const CircleAvatar(
|
||||||
|
backgroundColor: Colors.deepOrange,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
child: Icon(Icons.ballot_outlined),
|
||||||
|
),
|
||||||
|
title: Text(L10n.of(context).poll),
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -309,7 +320,7 @@ class _ChatAccountPicker extends StatelessWidget {
|
||||||
onSelected: (mxid) => _popupMenuButtonSelected(mxid, context),
|
onSelected: (mxid) => _popupMenuButtonSelected(mxid, context),
|
||||||
itemBuilder: (BuildContext context) => clients
|
itemBuilder: (BuildContext context) => clients
|
||||||
.map(
|
.map(
|
||||||
(client) => PopupMenuItem<String>(
|
(client) => PopupMenuItem(
|
||||||
value: client!.userID,
|
value: client!.userID,
|
||||||
child: FutureBuilder<Profile>(
|
child: FutureBuilder<Profile>(
|
||||||
future: client.fetchOwnProfile(),
|
future: client.fetchOwnProfile(),
|
||||||
|
|
@ -338,3 +349,5 @@ class _ChatAccountPicker extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AttachmentButtonAction { file, image, camera, video, location, poll }
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
import 'package:matrix/msc_extensions/msc_3381_polls/models/poll_event_content.dart';
|
||||||
import 'package:swipe_to_action/swipe_to_action.dart';
|
import 'package:swipe_to_action/swipe_to_action.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/themes.dart';
|
import 'package:fluffychat/config/themes.dart';
|
||||||
|
|
@ -66,6 +67,7 @@ class Message extends StatelessWidget {
|
||||||
EventTypes.Sticker,
|
EventTypes.Sticker,
|
||||||
EventTypes.Encrypted,
|
EventTypes.Encrypted,
|
||||||
EventTypes.CallInvite,
|
EventTypes.CallInvite,
|
||||||
|
PollEventContent.startType,
|
||||||
}.contains(event.type)) {
|
}.contains(event.type)) {
|
||||||
if (event.type.startsWith('m.call.')) {
|
if (event.type.startsWith('m.call.')) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:fluffychat/pages/chat/events/poll_event.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
|
@ -12,6 +13,7 @@ import 'package:fluffychat/utils/date_time_extension.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
import 'package:matrix/msc_extensions/msc_3381_polls/models/poll_event_content.dart';
|
||||||
import '../../../config/app_config.dart';
|
import '../../../config/app_config.dart';
|
||||||
import '../../../utils/platform_infos.dart';
|
import '../../../utils/platform_infos.dart';
|
||||||
import '../../../utils/url_launcher.dart';
|
import '../../../utils/url_launcher.dart';
|
||||||
|
|
@ -292,6 +294,8 @@ class MessageContent extends StatelessWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
case PollEventContent.startType:
|
||||||
|
return PollEvent(event, textColor: textColor, timeline: timeline);
|
||||||
default:
|
default:
|
||||||
return FutureBuilder<User?>(
|
return FutureBuilder<User?>(
|
||||||
future: event.fetchSenderUser(),
|
future: event.fetchSenderUser(),
|
||||||
|
|
|
||||||
106
lib/pages/chat/events/poll_event.dart
Normal file
106
lib/pages/chat/events/poll_event.dart
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
|
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:matrix/msc_extensions/msc_3381_polls/poll_event_extension.dart';
|
||||||
|
|
||||||
|
class PollEvent extends StatelessWidget {
|
||||||
|
final Event event;
|
||||||
|
final Timeline timeline;
|
||||||
|
final Color textColor;
|
||||||
|
const PollEvent(
|
||||||
|
this.event, {
|
||||||
|
required this.textColor,
|
||||||
|
required this.timeline,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||||
|
final content = event.parsedPollEventContent.pollStartContent;
|
||||||
|
final answers = event.getPollResponses(timeline);
|
||||||
|
final answersLength = answers.length;
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
content.question.mText,
|
||||||
|
style: TextStyle(color: textColor, fontSize: fontSize),
|
||||||
|
),
|
||||||
|
for (final answer in content.answers)
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final votes =
|
||||||
|
answers.values.where((v) => v.contains(answer.id)).length;
|
||||||
|
final percentage = answersLength == 0 ? 0 : votes / answersLength;
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 32,
|
||||||
|
child: Material(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppConfig.borderRadius),
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
final ownAnswers =
|
||||||
|
answers[event.room.client.userID] ?? {};
|
||||||
|
if (ownAnswers.contains(answer.id)) {
|
||||||
|
ownAnswers.remove(answer.id);
|
||||||
|
} else {
|
||||||
|
ownAnswers.add(answer.id);
|
||||||
|
}
|
||||||
|
showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => event.answerPoll(ownAnswers.toList()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppConfig.borderRadius),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
LinearProgressIndicator(
|
||||||
|
minHeight: 32,
|
||||||
|
backgroundColor: textColor.withAlpha(16),
|
||||||
|
color: textColor.withAlpha(64),
|
||||||
|
value: percentage.toDouble(),
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppConfig.borderRadius),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
answer.mText,
|
||||||
|
style: TextStyle(color: textColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (answersLength > 0)
|
||||||
|
Text(
|
||||||
|
L10n.of(context).countVotes(
|
||||||
|
votes,
|
||||||
|
(percentage * 100).round(),
|
||||||
|
),
|
||||||
|
style: theme.textTheme.labelSmall
|
||||||
|
?.copyWith(color: textColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
222
lib/pages/chat/poll_edit_bottom_sheet.dart
Normal file
222
lib/pages/chat/poll_edit_bottom_sheet.dart
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:matrix/msc_extensions/msc_3381_polls/models/poll_event_content.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
|
|
||||||
|
class PollEditBottomSheet extends StatefulWidget {
|
||||||
|
final PollStartContent? oldPoll;
|
||||||
|
const PollEditBottomSheet({this.oldPoll, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PollEditBottomSheet> createState() => _PollEditBottomSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PollEditBottomSheetState extends State<PollEditBottomSheet> {
|
||||||
|
final TextEditingController _questionController = TextEditingController();
|
||||||
|
final List<TextEditingController> _answerController = [
|
||||||
|
TextEditingController(),
|
||||||
|
TextEditingController(),
|
||||||
|
];
|
||||||
|
PollKind _kind = PollKind.disclosed;
|
||||||
|
int _maxSelection = 1;
|
||||||
|
|
||||||
|
bool _canFinish = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
final oldPoll = widget.oldPoll;
|
||||||
|
if (oldPoll != null) {
|
||||||
|
_questionController.text = oldPoll.question.mText;
|
||||||
|
_answerController.clear();
|
||||||
|
_answerController.addAll(
|
||||||
|
oldPoll.answers.map(
|
||||||
|
(answer) => TextEditingController(text: answer.mText),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_kind = oldPoll.kind ?? _kind;
|
||||||
|
_maxSelection = oldPoll.maxSelections;
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkCanFinish([_]) {
|
||||||
|
final canFinish = _questionController.text.isNotEmpty &&
|
||||||
|
_answerController.length >= 2 &&
|
||||||
|
!_answerController.any((c) => c.text.isEmpty);
|
||||||
|
if (canFinish != _canFinish) {
|
||||||
|
setState(() {
|
||||||
|
_canFinish = canFinish;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _deleteAnswer(int i) {
|
||||||
|
setState(() {
|
||||||
|
_answerController.removeAt(i);
|
||||||
|
if (_maxSelection > _answerController.length) _maxSelection--;
|
||||||
|
});
|
||||||
|
_checkCanFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addAnswer() {
|
||||||
|
setState(() {
|
||||||
|
_answerController.add(TextEditingController());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateMaxSelection(int? maxSelection) {
|
||||||
|
if (maxSelection == null) return;
|
||||||
|
setState(() {
|
||||||
|
_maxSelection = maxSelection;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateKind(PollKind? kind) {
|
||||||
|
if (kind == null) return;
|
||||||
|
setState(() {
|
||||||
|
_kind = kind;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _finish() {
|
||||||
|
_checkCanFinish();
|
||||||
|
context.pop(
|
||||||
|
PollStartContent(
|
||||||
|
maxSelections: _maxSelection,
|
||||||
|
question: PollQuestion(
|
||||||
|
mText: _questionController.text,
|
||||||
|
),
|
||||||
|
kind: _kind,
|
||||||
|
answers: _answerController
|
||||||
|
.map((c) => c.text)
|
||||||
|
.where((text) => text.isNotEmpty)
|
||||||
|
.mapIndexed(
|
||||||
|
(i, text) => PollAnswer(
|
||||||
|
mText: text,
|
||||||
|
id: '$i$text'.hashCode.toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: CloseButton(
|
||||||
|
onPressed: () => context.pop(null),
|
||||||
|
),
|
||||||
|
title: Text(L10n.of(context).poll),
|
||||||
|
actions: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 12.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _canFinish ? _finish : null,
|
||||||
|
child: Text(L10n.of(context).send),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _questionController,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 4,
|
||||||
|
maxLength: 1024,
|
||||||
|
onChanged: _checkCanFinish,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
counterText: '',
|
||||||
|
labelText: L10n.of(context).question,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
for (var i = 0; i < _answerController.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: _answerController[i],
|
||||||
|
maxLength: 128,
|
||||||
|
onChanged: _checkCanFinish,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: L10n.of(context).answer,
|
||||||
|
counterText: '',
|
||||||
|
suffixIcon: i > 1
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outlined),
|
||||||
|
tooltip: L10n.of(context).deleteAnswer,
|
||||||
|
onPressed: () => _deleteAnswer(i),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: () => _addAnswer(),
|
||||||
|
icon: const Icon(Icons.add_outlined),
|
||||||
|
label: Text(L10n.of(context).addAnswer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Wrap(
|
||||||
|
children: [
|
||||||
|
DropdownButton<PollKind>(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
value: _kind,
|
||||||
|
items: PollKind.values
|
||||||
|
.map(
|
||||||
|
(kind) => DropdownMenuItem(
|
||||||
|
value: kind,
|
||||||
|
child: Text(kind.getLocalizedString(context)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: _updateKind,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
DropdownButton<int>(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
value: _maxSelection,
|
||||||
|
items: [
|
||||||
|
for (var i = 1; i <= _answerController.length; i++)
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: i,
|
||||||
|
child: Text('Max selection: $i'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: _updateMaxSelection,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on PollKind {
|
||||||
|
String getLocalizedString(BuildContext context) {
|
||||||
|
switch (this) {
|
||||||
|
case PollKind.disclosed:
|
||||||
|
return L10n.of(context).resultsDisclosed;
|
||||||
|
case PollKind.undisclosed:
|
||||||
|
return L10n.of(context).resultsUndisclosed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -59,5 +59,6 @@ extension IsStateExtension on Event {
|
||||||
EventTypes.Message,
|
EventTypes.Message,
|
||||||
EventTypes.Sticker,
|
EventTypes.Sticker,
|
||||||
EventTypes.Encrypted,
|
EventTypes.Encrypted,
|
||||||
|
'org.matrix.msc3381.poll.start',
|
||||||
}.contains(type);
|
}.contains(type);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -350,4 +350,7 @@ class MatrixLocals extends MatrixLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancelledSend => l10n.sendCanceled;
|
String get cancelledSend => l10n.sendCanceled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String startedAPoll(String senderName) => l10n.startedAPoll(senderName);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1158,8 +1158,8 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: "krille/implement-polls-msc"
|
||||||
resolved-ref: "928f6ba96f259ab586ccfeb6c1674aa61ea6d16a"
|
resolved-ref: "2d08d24c87c3baa02e6a73e58d790df29a8546fd"
|
||||||
url: "https://github.com/famedly/matrix-dart-sdk.git"
|
url: "https://github.com/famedly/matrix-dart-sdk.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.36.0"
|
version: "0.36.0"
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,9 @@ dependencies:
|
||||||
latlong2: ^0.9.1
|
latlong2: ^0.9.1
|
||||||
linkify: ^5.0.0
|
linkify: ^5.0.0
|
||||||
matrix:
|
matrix:
|
||||||
git: https://github.com/famedly/matrix-dart-sdk.git
|
git:
|
||||||
|
url: https://github.com/famedly/matrix-dart-sdk.git
|
||||||
|
ref: krille/implement-polls-msc
|
||||||
mime: ^1.0.6
|
mime: ^1.0.6
|
||||||
native_imaging: ^0.1.1
|
native_imaging: ^0.1.1
|
||||||
opus_caf_converter_dart: ^1.0.1
|
opus_caf_converter_dart: ^1.0.1
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue