Skip to content
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 commet/lib/client/components/component_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'package:commet/client/matrix/components/key_verification_component/matri
import 'package:commet/client/matrix/components/photo_album_room/matrix_photo_album_room_component.dart';
import 'package:commet/client/matrix/components/pinned_messages/matrix_pinned_messages_component.dart';
import 'package:commet/client/matrix/components/message_effects/matrix_message_effects_component.dart';
import 'package:commet/client/matrix/components/polls/matrix_poll_component.dart';
import 'package:commet/client/matrix/components/profile/matrix_profile_component.dart';
import 'package:commet/client/matrix/components/push_notifications/matrix_push_notification_component.dart';
import 'package:commet/client/matrix/components/space_banner/matrix_space_banner_component.dart';
Expand Down Expand Up @@ -62,6 +63,7 @@ class ComponentRegistry {
MatrixUserColorComponent(client),
MatrixDonationAwardsComponent(client),
MatrixKeyVerificationComponent(client),
MatrixPollComponent(client),
];
}

Expand Down
50 changes: 50 additions & 0 deletions commet/lib/client/components/polls/poll_component.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:commet/client/client.dart';
import 'package:commet/client/components/component.dart';
import 'package:commet/client/timeline_events/timeline_event.dart';

class PollAnswer {
String id;
String answer;

PollAnswer(this.id, this.answer);
}

class PollCreateArgs {
String question;
List<String> options;
bool multiAnswer;
bool publicAnswers;

PollCreateArgs({
required this.question,
required this.options,
this.multiAnswer = false,
this.publicAnswers = false,
});
}

abstract class PollComponent<R extends Client> implements Component<R> {
Map<String, Set<String>> getPollResponses(
Timeline timeline, TimelineEvent event);

bool isPollEvent(TimelineEvent event);

String getPollQuestion(TimelineEvent event);

int getMaxSelections(TimelineEvent event);

bool shouldShowResults(TimelineEvent<Client> event, Timeline timeline);

bool isFinished(TimelineEvent<Client> event, Timeline timeline);

List<PollAnswer> getAllowedPollAnswers(TimelineEvent event);

Future<void> createPoll(Room room, PollCreateArgs args);

Future<void> endPoll(Room room, TimelineEvent event);

bool canEndPoll(Room room, TimelineEvent event, Timeline timeline);

Future<void> setAnswer(
TimelineEvent event, Room room, List<PollAnswer> answer);
}
135 changes: 135 additions & 0 deletions commet/lib/client/matrix/components/polls/matrix_poll_component.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import 'package:commet/client/client.dart';
import 'package:commet/client/components/polls/poll_component.dart';
import 'package:commet/client/matrix/matrix_client.dart';
import 'package:commet/client/matrix/matrix_room.dart';
import 'package:commet/client/matrix/matrix_timeline.dart';
import 'package:commet/client/matrix/timeline_events/matrix_timeline_event.dart';
import 'package:commet/client/timeline_events/timeline_event.dart';
import 'package:commet/utils/rng.dart';
import 'package:matrix/matrix.dart' as matrix;
import 'package:matrix/msc_extensions/msc_3381_polls/poll_event_extension.dart';

class MatrixPollComponent implements PollComponent<MatrixClient> {
@override
MatrixClient client;

MatrixPollComponent(this.client);

bool isPollEvent(TimelineEvent event) {
var e = event as MatrixTimelineEvent;

return e.event.type == "org.matrix.msc3381.poll.start";
}

matrix.PollEventContent _parse(TimelineEvent event) {
var mxEvent = (event as MatrixTimelineEvent).event;
var c = mxEvent.content;

if (c.containsKey(matrix.PollEventContent.mTextJsonKey) == false) {
if (c["body"] is String) {
c[matrix.PollEventContent.mTextJsonKey] = c["body"];
} else {
c[matrix.PollEventContent.mTextJsonKey] = "";
}
}

return matrix.PollEventContent.fromJson(c);
}

@override
List<PollAnswer> getAllowedPollAnswers(TimelineEvent event) {
return _parse(event)
.pollStartContent
.answers
.map((i) => PollAnswer(i.id, i.mText))
.toList();
}

@override
int getMaxSelections(TimelineEvent event) {
return _parse(event).pollStartContent.maxSelections;
}

@override
Map<String, Set<String>> getPollResponses(
Timeline timeline, TimelineEvent event) {
Map<String, Set<String>> responses = {};

var mxEvent = (event as MatrixTimelineEvent).event;

var mxResponses =
mxEvent.getPollResponses((timeline as MatrixTimeline).matrixTimeline!);

for (var userId in mxResponses.keys) {
for (var answer in mxResponses[userId]!) {
if (responses.containsKey(answer) == false) {
responses[answer] = Set();
}
responses[answer]!.add(userId);
}
}

return responses;
}

@override
String getPollQuestion(TimelineEvent<Client> event) {
return _parse(event).pollStartContent.question.mText;
}

@override
Future<void> setAnswer(
TimelineEvent<Client> event, Room room, List<PollAnswer> answers) async {
var mxEvent = (event as MatrixTimelineEvent).event;
await mxEvent.answerPoll(answers.map((i) => i.id).toList());
}

@override
bool shouldShowResults(TimelineEvent<Client> event, Timeline timeline) {
var mxEvent = (event as MatrixTimelineEvent).event;

if (_parse(event).pollStartContent.kind == matrix.PollKind.disclosed) {
return true;
}

return mxEvent
.getPollHasBeenEnded((timeline as MatrixTimeline).matrixTimeline!);
}

@override
bool isFinished(TimelineEvent<Client> event, Timeline timeline) {
var mxEvent = (event as MatrixTimelineEvent).event;

return mxEvent
.getPollHasBeenEnded((timeline as MatrixTimeline).matrixTimeline!);
}

@override
Future<void> createPoll(Room room, PollCreateArgs args) async {
await (room as MatrixRoom).matrixRoom.startPoll(
question: args.question,
answers: args.options
.map((i) => matrix.PollAnswer(
id: RandomUtils.getRandomString(10), mText: i))
.toList(),
kind: args.publicAnswers
? matrix.PollKind.disclosed
: matrix.PollKind.undisclosed,
maxSelections: args.multiAnswer ? args.options.length : 1);
}

@override
bool canEndPoll(Room room, TimelineEvent<Client> event, Timeline timeline) {
if (isFinished(event, timeline)) {
return false;
}

return event.senderId == room.client.self!.identifier;
}

@override
Future<void> endPoll(Room room, TimelineEvent<Client> event) async {
var mxEvent = (event as MatrixTimelineEvent).event;
await mxEvent.endPoll();
}
}
66 changes: 51 additions & 15 deletions commet/lib/ui/molecules/message_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import 'package:commet/client/client.dart';
import 'package:commet/client/components/emoticon/dynamic_emoticon_pack.dart';
import 'package:commet/client/components/emoticon_recent/recent_emoticon_component.dart';
import 'package:commet/client/components/gif/gif_component.dart';
import 'package:commet/client/components/polls/poll_component.dart';
import 'package:commet/config/build_config.dart';
import 'package:commet/config/layout_config.dart';
import 'package:commet/config/platform_utils.dart';
import 'package:commet/main.dart';
import 'package:commet/ui/atoms/adaptive_context_menu.dart';
import 'package:commet/ui/atoms/emoji_widget.dart';
import 'package:commet/ui/atoms/keyboard_adaptor.dart';
import 'package:commet/ui/atoms/random_emoji_button.dart';
import 'package:commet/ui/atoms/rich_text_field.dart';
import 'package:commet/ui/molecules/attachment_icon.dart';
import 'package:commet/ui/molecules/overlapping_panels.dart';
import 'package:commet/ui/molecules/poll_creator.dart';
import 'package:commet/ui/organisms/attachment_processor/attachment_processor.dart';
import 'package:commet/ui/molecules/emoticon_picker.dart';
import 'package:commet/ui/navigation/adaptive_dialog.dart';
Expand Down Expand Up @@ -319,6 +322,8 @@ class MessageInputState extends State<MessageInput> {
?.call(controller.text.trim(), overrideClient: senderOverride);
}

void showMoreAttachmentOptions() {}

// This duration is to try and hide the transition from keyboard popup animation
Debouncer removeHeightOverrideDebouncer =
Debouncer(delay: Duration(seconds: 1));
Expand Down Expand Up @@ -886,7 +891,10 @@ class MessageInputState extends State<MessageInput> {
}

Widget sendMessageButton() {
bool canSend = controller.text.isNotEmpty;
bool canSend =
controller.text.isNotEmpty || widget.attachments?.isNotEmpty == true;

var pollComponent = widget.room?.client.getComponent<PollComponent>();

double targetValue = canSend ? 1 : 0;
return Padding(
Expand All @@ -895,20 +903,48 @@ class MessageInputState extends State<MessageInput> {
tween: Tween<double>(begin: 0, end: targetValue),
duration: Durations.medium1,
builder: (context, value, child) {
return SizedBox(
width: widget.size,
height: widget.size,
child: tiamat.CircleButton(
icon: Icons.send,
radius: widget.size * widget.iconScale,
onPressed: sendMessage,
color: Color.lerp(
Theme.of(context).colorScheme.primary.withAlpha(0),
Theme.of(context).colorScheme.primary,
value),
iconColor: Color.lerp(Theme.of(context).colorScheme.secondary,
Theme.of(context).colorScheme.onPrimary, value),
));
return ClipRRect(
borderRadius: BorderRadiusGeometry.circular(widget.size),
child: Material(
child: AdaptiveContextMenu(
modal: true,
items: [
if (pollComponent != null)
tiamat.ContextMenuItem(
text: "Poll",
icon: Icons.poll,
onPressed: () async {
var createArgs =
await AdaptiveDialog.show<PollCreateArgs>(context,
title: "Create Poll",
builder: (context) => PollCreator());

if (createArgs != null) {
print(createArgs);
pollComponent.createPoll(widget.room!, createArgs);
}
},
)
],
child: SizedBox(
width: widget.size,
height: widget.size,
child: tiamat.CircleButton(
icon: canSend ? Icons.send : Icons.more_horiz,
radius: widget.size * widget.iconScale,
onPressed: canSend ? sendMessage : null,
color: Color.lerp(
Theme.of(context).colorScheme.primary.withAlpha(0),
Theme.of(context).colorScheme.primary,
value),
iconColor: Color.lerp(
Theme.of(context).colorScheme.secondary,
Theme.of(context).colorScheme.onPrimary,
value),
)),
),
),
);
},
));
}
Expand Down
Loading
Loading