Line data Source code
1 : import 'dart:async';
2 : import 'dart:io';
3 :
4 : import 'package:cwtch/config.dart';
5 : import 'package:cwtch/cwtch_icons_icons.dart';
6 : import 'package:cwtch/models/contact.dart';
7 : import 'package:cwtch/models/message.dart';
8 : import 'package:cwtch/models/profile.dart';
9 : import 'package:cwtch/themes/opaque.dart';
10 : import 'package:cwtch/third_party/base32/base32.dart';
11 : import 'package:cwtch/views/contactsview.dart';
12 : import 'package:cwtch/widgets/cwtchlabel.dart';
13 : import 'package:cwtch/widgets/deletedbubble.dart';
14 : import 'package:cwtch/widgets/eventbubble.dart';
15 : import 'package:cwtch/widgets/messageloadingbubble.dart';
16 : import 'package:cwtch/widgets/staticmessagebubble.dart';
17 : import 'package:flutter/material.dart';
18 : import 'package:cwtch/widgets/profileimage.dart';
19 : import 'package:flutter/physics.dart';
20 : import 'package:provider/provider.dart';
21 : import 'package:cwtch/l10n/app_localizations.dart';
22 :
23 : import '../main.dart';
24 : import '../models/messagecache.dart';
25 : import '../models/redaction.dart';
26 : import '../settings.dart';
27 :
28 : class MessageRow extends StatefulWidget {
29 : final Widget child;
30 : final int index;
31 :
32 0 : MessageRow(this.child, this.index, {Key? key}) : super(key: key);
33 :
34 0 : @override
35 0 : MessageRowState createState() => MessageRowState();
36 : }
37 :
38 : class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMixin {
39 : bool showBlockedMessage = false;
40 : late AnimationController _controller;
41 : late Animation<Alignment> _animation;
42 : late Alignment _dragAlignment = Alignment.center;
43 : Alignment _dragAffinity = Alignment.center;
44 :
45 0 : @override
46 : void initState() {
47 0 : super.initState();
48 0 : _controller = AnimationController(vsync: this);
49 0 : _controller.addListener(() {
50 0 : setState(() {
51 0 : _dragAlignment = _animation.value;
52 : });
53 : });
54 : }
55 :
56 0 : @override
57 : void dispose() {
58 0 : _controller.dispose();
59 0 : super.dispose();
60 : }
61 :
62 0 : @override
63 : Widget build(BuildContext context) {
64 : // message cache will return a malformed metadata object
65 : // if for whatever reason this message doesn't exist
66 0 : if (Provider.of<MessageMetadata>(context).senderHandle == "") {
67 0 : EnvironmentConfig.debugLog("error, cache returned malformed message. This is likely a programming bug.");
68 0 : return MessageLoadingBubble();
69 : }
70 0 : var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
71 0 : var isContact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle) != null;
72 0 : var isGroup = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context, listen: false).conversationIdentifier)!.isGroup;
73 0 : var isBlocked = isContact ? Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle)!.isBlocked : false;
74 0 : var actualMessage = Flexible(flex: Platform.isAndroid ? 10 : 100, fit: FlexFit.loose, child: widget.child);
75 0 : bool pending = Provider.of<ContactInfoState>(context).isOpPending;
76 :
77 0 : _dragAffinity = fromMe ? Alignment.centerRight : Alignment.centerLeft;
78 0 : _dragAlignment = fromMe ? Alignment.centerRight : Alignment.centerLeft;
79 :
80 : var senderDisplayStr = "";
81 : if (!fromMe) {
82 0 : ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
83 : if (contact != null) {
84 0 : senderDisplayStr = redactedNick(context, contact.onion, contact.nickname);
85 : } else {
86 0 : senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
87 : }
88 : }
89 :
90 0 : Widget wdgReply = Platform.isAndroid
91 0 : ? SizedBox.shrink()
92 0 : : Visibility(
93 0 : visible: EnvironmentConfig.TEST_MODE || Provider.of<ContactInfoState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
94 : maintainSize: true,
95 : maintainAnimation: true,
96 : maintainState: true,
97 : maintainInteractivity: false,
98 0 : child: IconButton(
99 0 : tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage,
100 0 : splashRadius: Material.defaultSplashRadius / 2,
101 0 : onPressed: () {
102 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.quotedReference = Provider.of<MessageMetadata>(context, listen: false).messageID;
103 0 : Provider.of<ContactInfoState>(context, listen: false).notifyMessageDraftUpdate();
104 0 : setState(() {});
105 : },
106 0 : icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.chatReactionIconColor),
107 : ),
108 : );
109 :
110 0 : var settings = Provider.of<Settings>(context);
111 0 : var pis = Provider.of<ProfileInfoState>(context);
112 0 : var cis = Provider.of<ContactInfoState>(context);
113 0 : var borderColor = Provider.of<Settings>(context).theme.portraitOnlineBorderColor;
114 0 : var messageID = Provider.of<MessageMetadata>(context).messageID;
115 0 : var cache = Provider.of<ContactInfoState>(context).messageCache;
116 :
117 0 : Widget wdgSeeReplies = Platform.isAndroid
118 0 : ? SizedBox.shrink()
119 0 : : Visibility(
120 0 : visible: EnvironmentConfig.TEST_MODE || Provider.of<ContactInfoState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
121 : maintainSize: true,
122 : maintainAnimation: true,
123 : maintainState: true,
124 : maintainInteractivity: false,
125 0 : child: IconButton(
126 0 : tooltip: AppLocalizations.of(context)!.viewReplies,
127 0 : splashRadius: Material.defaultSplashRadius / 2,
128 0 : onPressed: () {
129 0 : modalShowReplies(context, AppLocalizations.of(context)!.headingReplies, AppLocalizations.of(context)!.messageNoReplies, settings, pis, cis, borderColor, cache, messageID);
130 : },
131 0 : icon: Icon(CwtchIcons.view_replies, color: Provider.of<Settings>(context).theme.chatReactionIconColor),
132 : ),
133 : );
134 :
135 0 : var profile = Provider.of<ProfileInfoState>(context, listen: false);
136 0 : var conversation = Provider.of<ContactInfoState>(context, listen: false);
137 0 : var message = Provider.of<MessageMetadata>(context, listen: false);
138 :
139 0 : Widget wdgTranslateMessage = Platform.isAndroid
140 0 : ? SizedBox.shrink()
141 0 : : Visibility(
142 : visible:
143 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.IsBlodeuweddSupported() &&
144 0 : Provider.of<Settings>(context).isExperimentEnabled(BlodeuweddExperiment) &&
145 0 : (EnvironmentConfig.TEST_MODE || Provider.of<ContactInfoState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID),
146 : maintainSize: true,
147 : maintainAnimation: true,
148 : maintainState: true,
149 : maintainInteractivity: false,
150 0 : child: IconButton(
151 0 : tooltip: AppLocalizations.of(context)!.blodeuweddTranslate,
152 0 : splashRadius: Material.defaultSplashRadius / 2,
153 0 : onPressed: () {
154 0 : Provider.of<MessageMetadata>(context, listen: false).translation = "";
155 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.TranslateMessage(profile.onion, conversation.identifier, message.messageID, "French");
156 0 : modalShowTranslation(context, profile, settings);
157 : },
158 0 : icon: Icon(Icons.translate, color: Provider.of<Settings>(context).theme.chatReactionIconColor),
159 : ),
160 : );
161 :
162 0 : bool isHidden = Provider.of<MessageMetadata>(context).attributes["hidden"] == "true";
163 0 : bool isWithheld = Provider.of<MessageMetadata>(context).attributes["withheld"] == "true";
164 0 : bool canMod = conversation.isManaged && conversation.isOnline() && ['admin', 'op', 'mod'].contains(conversation.getRole(profile.onion));
165 0 : bool isDeleted = widget.child.runtimeType == DeletedBubble;
166 :
167 0 : Widget wdgHideMessage = (!canMod || isDeleted || Platform.isAndroid)
168 0 : ? SizedBox.shrink()
169 0 : : Visibility(
170 0 : visible: (EnvironmentConfig.TEST_MODE || Provider.of<ContactInfoState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID),
171 : maintainSize: true,
172 : maintainAnimation: true,
173 : maintainState: true,
174 : maintainInteractivity: false,
175 0 : child: IconButton(
176 : tooltip: isWithheld
177 0 : ? AppLocalizations.of(context)!.buttonMessageReveal
178 0 : : (isHidden ? AppLocalizations.of(context)!.buttonMessageUnhide : AppLocalizations.of(context)!.buttonMessageHide),
179 0 : splashRadius: Material.defaultSplashRadius / 2,
180 : onPressed: pending
181 : ? null
182 0 : : () {
183 0 : if (message.signature == null || message.signature!.isEmpty) {
184 : return;
185 : }
186 0 : Provider.of<ContactInfoState>(context, listen: false).isOpPending = true;
187 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ModerateMessage(profile.onion, conversation.identifier, message.signature!, isWithheld ? "R" : (isHidden ? "U" : "H"));
188 : },
189 0 : icon: Icon((isWithheld || isHidden) ? CwtchIcons.eye_open : CwtchIcons.eye_closed, color: Provider.of<Settings>(context).theme.chatReactionIconColor),
190 : ),
191 : );
192 :
193 0 : Widget wdgAndroidMod = (!canMod || isDeleted || !Platform.isAndroid || pending)
194 0 : ? SizedBox.shrink()
195 0 : : IconButton(
196 0 : tooltip: AppLocalizations.of(context)!.titleModerationOptions,
197 0 : splashRadius: Material.defaultSplashRadius / 2,
198 0 : onPressed: () {
199 0 : modalShowModerationOptions(context, message.signature, isHidden, isWithheld);
200 : },
201 0 : icon: Icon(CwtchIcons.member_mod, color: Provider.of<Settings>(context).theme.chatReactionIconColor),
202 : );
203 :
204 0 : Widget wdgDeleteMessage = (!canMod || isDeleted || Platform.isAndroid)
205 0 : ? SizedBox.shrink()
206 0 : : Visibility(
207 0 : visible: (EnvironmentConfig.TEST_MODE || Provider.of<ContactInfoState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID),
208 : maintainSize: true,
209 : maintainAnimation: true,
210 : maintainState: true,
211 : maintainInteractivity: false,
212 0 : child: IconButton(
213 0 : tooltip: AppLocalizations.of(context)!.buttonMessageDelete,
214 0 : splashRadius: Material.defaultSplashRadius / 2,
215 : onPressed: pending
216 : ? null
217 0 : : () {
218 0 : if (message.signature == null || message.signature!.isEmpty) {
219 : return;
220 : }
221 0 : Provider.of<ContactInfoState>(context, listen: false).isOpPending = true;
222 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ModerateMessage(profile.onion, conversation.identifier, message.signature!, "D");
223 : },
224 0 : icon: Icon(CwtchIcons.delete_24px, color: Provider.of<Settings>(context).theme.chatReactionIconColor),
225 : ),
226 : );
227 :
228 0 : Widget wdgSpacer = Flexible(flex: 1, child: SizedBox(width: Platform.isAndroid ? 20 : 60, height: 10));
229 0 : var widgetRow = <Widget>[];
230 :
231 0 : if (widget.child.runtimeType == EventBubble) {
232 0 : widgetRow = <Widget>[wdgSpacer, actualMessage];
233 : } else if (fromMe) {
234 0 : widgetRow = <Widget>[wdgSpacer, wdgTranslateMessage, wdgHideMessage, wdgDeleteMessage, wdgSeeReplies, wdgReply, wdgAndroidMod, actualMessage];
235 0 : } else if (isBlocked && !showBlockedMessage) {
236 0 : Color blockedMessageBackground = Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor;
237 0 : Widget wdgPortrait = Padding(padding: EdgeInsets.all(4.0), child: Icon(CwtchIcons.account_blocked));
238 0 : widgetRow = <Widget>[
239 : wdgPortrait,
240 0 : Container(
241 0 : padding: EdgeInsets.all(2.0),
242 0 : decoration: BoxDecoration(
243 : color: blockedMessageBackground,
244 0 : border: Border.all(color: blockedMessageBackground, width: 2),
245 0 : borderRadius: BorderRadius.only(topLeft: Radius.circular(15.0), topRight: Radius.circular(15.0), bottomLeft: Radius.circular(15.0), bottomRight: Radius.circular(15.0)),
246 : ),
247 0 : child: Padding(
248 0 : padding: EdgeInsets.all(9.0),
249 0 : child: Row(
250 : crossAxisAlignment: CrossAxisAlignment.center,
251 0 : children: [
252 0 : SelectableText(
253 0 : AppLocalizations.of(context)!.blockedMessageMessage,
254 : //key: Key(myKey),
255 0 : style: TextStyle(color: Provider.of<Settings>(context).theme.messageFromOtherTextColor),
256 : textAlign: TextAlign.center,
257 : textWidthBasis: TextWidthBasis.longestLine,
258 : ),
259 0 : IconButton(
260 : enableFeedback: true,
261 0 : splashRadius: Material.defaultSplashRadius / 2,
262 0 : tooltip: AppLocalizations.of(context)!.showMessageButton,
263 0 : icon: Icon(Icons.preview, color: Provider.of<Settings>(context).current().mainTextColor),
264 0 : onPressed: () {
265 0 : setState(() {
266 0 : this.showBlockedMessage = true;
267 : });
268 : },
269 : ),
270 : ],
271 : ),
272 : ),
273 : ),
274 : wdgSpacer,
275 : ];
276 : } else {
277 0 : var contact = Provider.of<ContactInfoState>(context);
278 0 : ContactInfoState? sender = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
279 :
280 0 : String imagePath = Provider.of<MessageMetadata>(context).senderImage!;
281 : if (sender != null) {
282 0 : imagePath = Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment) ? sender.imagePath : sender.defaultImagePath;
283 : } else {
284 0 : imagePath = RandomProfileImage(Provider.of<MessageMetadata>(context).senderHandle);
285 : }
286 0 : Widget wdgPortrait = GestureDetector(
287 : onTap: !isGroup
288 : ? null
289 : : isContact
290 0 : ? _btnGoto
291 0 : : _btnAdd,
292 0 : child: Padding(
293 0 : padding: EdgeInsets.all(4.0),
294 0 : child: ProfileImage(
295 : diameter: 48.0,
296 : // default to the contact image...otherwise use a derived sender image...
297 : imagePath: imagePath,
298 0 : border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor : Provider.of<Settings>(context).theme.portraitOfflineBorderColor,
299 : badgeTextColor: Colors.red,
300 : badgeColor: Colors.red,
301 : tooltip: !isGroup
302 : ? ""
303 : : isContact
304 0 : ? AppLocalizations.of(context)!.contactGoto.replaceFirst("%1", senderDisplayStr)
305 0 : : AppLocalizations.of(context)!.addContact,
306 : ),
307 : ),
308 : );
309 :
310 0 : widgetRow = <Widget>[wdgPortrait, actualMessage, wdgAndroidMod, wdgReply, wdgSeeReplies, wdgHideMessage, wdgDeleteMessage, wdgTranslateMessage, wdgSpacer];
311 : }
312 0 : var size = MediaQuery.of(context).size;
313 0 : var mr = MouseRegion(
314 : // For desktop...
315 0 : onHover: (event) {
316 0 : if (Provider.of<ContactInfoState>(context, listen: false).hoveredIndex != Provider.of<MessageMetadata>(context, listen: false).messageID) {
317 0 : Provider.of<ContactInfoState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
318 : }
319 : },
320 0 : onExit: (event) {
321 0 : Provider.of<ContactInfoState>(context, listen: false).hoveredIndex = -1;
322 : },
323 0 : child: GestureDetector(
324 0 : onPanUpdate: (details) {
325 0 : setState(() {
326 0 : _dragAlignment += Alignment(details.delta.dx / (size.width * 0.5), 0);
327 : });
328 : },
329 0 : onPanDown: (details) {
330 0 : _controller.stop();
331 : },
332 0 : onPanEnd: (details) {
333 0 : _runAnimation(details.velocity.pixelsPerSecond, size);
334 0 : if (Platform.isAndroid) {
335 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.quotedReference = Provider.of<MessageMetadata>(context, listen: false).messageID;
336 0 : Provider.of<ContactInfoState>(context, listen: false).notifyMessageDraftUpdate();
337 0 : setState(() {});
338 : }
339 : },
340 0 : onLongPress: () async {
341 0 : if (Platform.isAndroid) {
342 0 : modalShowReplies(context, AppLocalizations.of(context)!.headingReplies, AppLocalizations.of(context)!.messageNoReplies, settings, pis, cis, borderColor, cache, messageID);
343 : }
344 : },
345 0 : child: Padding(
346 0 : padding: EdgeInsets.all(2),
347 0 : child: Align(
348 : widthFactor: 1,
349 0 : alignment: _dragAlignment,
350 0 : child: Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: widgetRow),
351 : ),
352 : ),
353 : ),
354 : );
355 :
356 0 : if (Provider.of<ContactInfoState>(context).newMarkerMsgIndex == widget.index) {
357 0 : return Column(
358 : crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
359 0 : children: [
360 0 : Align(
361 : alignment: Alignment.center,
362 0 : child: Padding(padding: EdgeInsets.all(5.0), child: _bubbleNew()),
363 : ),
364 : mr,
365 : ],
366 : );
367 : } else {
368 : return mr;
369 : }
370 : }
371 :
372 0 : Widget _bubbleNew() {
373 0 : return Container(
374 0 : decoration: BoxDecoration(
375 0 : color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
376 0 : border: Border.all(color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor, width: 1),
377 0 : borderRadius: BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8), bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8)),
378 : ),
379 0 : child: Padding(
380 0 : padding: EdgeInsets.all(9.0),
381 0 : child: Text(AppLocalizations.of(context)!.newMessagesLabel, style: Provider.of<Settings>(context).scaleFonts(defaultTextButtonStyle)),
382 : ),
383 : );
384 : }
385 :
386 0 : void _runAnimation(Offset pixelsPerSecond, Size size) {
387 0 : _animation = _controller.drive(AlignmentTween(begin: _dragAlignment, end: _dragAffinity));
388 : // Calculate the velocity relative to the unit interval, [0,1],
389 : // used by the animation controller.
390 0 : final unitsPerSecondX = pixelsPerSecond.dx / size.width;
391 0 : final unitsPerSecondY = pixelsPerSecond.dy / size.height;
392 0 : final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
393 0 : final unitVelocity = unitsPerSecond.distance;
394 :
395 : const spring = SpringDescription(mass: 30, stiffness: 1, damping: 1);
396 :
397 0 : final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
398 0 : _controller.animateWith(simulation);
399 : }
400 :
401 0 : void _btnGoto() {
402 0 : var id = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context, listen: false).senderHandle)?.identifier;
403 : if (id == null) {
404 : // Can't happen
405 : } else {
406 0 : selectConversation(context, id, null);
407 0 : var contactIndex = Provider.of<ProfileInfoState>(context, listen: false).filteredList().indexWhere((element) => element.identifier == id);
408 0 : Provider.of<ProfileInfoState>(context, listen: false).contactListScrollController.jumpTo(index: contactIndex);
409 : }
410 : }
411 :
412 0 : void _btnAdd() {
413 0 : var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
414 0 : if (sender == "") {
415 0 : print("sender not yet loaded");
416 : return;
417 : }
418 0 : var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
419 :
420 0 : showAddContactConfirmAlertDialog(context, profileOnion, sender);
421 : }
422 :
423 0 : showAddContactConfirmAlertDialog(BuildContext context, String profileOnion, String senderOnion) {
424 : // set up the buttons
425 0 : Widget cancelButton = ElevatedButton(
426 0 : child: Text(AppLocalizations.of(context)!.cancel),
427 0 : style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
428 0 : onPressed: () {
429 0 : Navigator.of(context).pop(); // dismiss dialog
430 : },
431 : );
432 0 : Widget continueButton = ElevatedButton(
433 0 : style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
434 0 : child: Text(AppLocalizations.of(context)!.addContact),
435 0 : onPressed: () {
436 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, senderOnion);
437 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact), duration: Duration(seconds: 2));
438 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
439 0 : Navigator.of(context).pop(); // dismiss dialog
440 : },
441 : );
442 :
443 : // set up the AlertDialog
444 0 : AlertDialog alert = AlertDialog(title: Text(AppLocalizations.of(context)!.addContactConfirm.replaceFirst("%1", senderOnion)), actions: [cancelButton, continueButton]);
445 :
446 : // show the dialog
447 0 : showDialog(
448 : context: context,
449 0 : builder: (BuildContext context) {
450 : return alert;
451 : },
452 : );
453 : }
454 : }
455 :
456 0 : void modalShowReplies(
457 : BuildContext ctx,
458 : String replyHeader,
459 : String noRepliesText,
460 : Settings settings,
461 : ProfileInfoState profile,
462 : ContactInfoState cis,
463 : Color borderColor,
464 : MessageCache cache,
465 : int messageID, {
466 : bool showImage = true,
467 : }) {
468 0 : showModalBottomSheet<void>(
469 : context: ctx,
470 0 : builder: (BuildContext bcontext) {
471 0 : List<Message> replies = getReplies(cache, messageID);
472 0 : ScrollController controller = ScrollController();
473 :
474 0 : return MultiProvider(
475 0 : providers: [
476 0 : ChangeNotifierProvider.value(value: Provider.of<ContactInfoState>(ctx)),
477 0 : ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(ctx)),
478 : ],
479 0 : child: LayoutBuilder(
480 0 : builder: (BuildContext context, BoxConstraints viewportConstraints) {
481 0 : var replyWidgets = replies.map((e) {
482 0 : var fromMe = e.getMetadata().senderHandle == profile.onion;
483 :
484 0 : var bubble = StaticMessageBubble(profile, settings, e.getMetadata(), Row(children: [Flexible(child: e.getPreviewWidget(context))]));
485 :
486 0 : String imagePath = e.getMetadata().senderImage!;
487 0 : var sender = profile.contactList.findContact(e.getMetadata().senderHandle);
488 : if (sender != null) {
489 0 : imagePath = showImage ? sender.imagePath : sender.defaultImagePath;
490 : } else {
491 0 : imagePath = RandomProfileImage(e.getMetadata().senderHandle);
492 : }
493 :
494 : if (fromMe) {
495 0 : imagePath = profile.imagePath;
496 : }
497 :
498 0 : var image = Padding(
499 0 : padding: EdgeInsets.all(4.0),
500 0 : child: ProfileImage(imagePath: imagePath, diameter: 48.0, border: borderColor, badgeTextColor: Colors.red, badgeColor: Colors.red),
501 : );
502 :
503 0 : return Padding(
504 0 : padding: EdgeInsets.all(10.0),
505 0 : child: Row(
506 : mainAxisSize: MainAxisSize.min,
507 0 : children: [
508 : image,
509 0 : Flexible(child: bubble),
510 : ],
511 : ),
512 : );
513 0 : }).toList();
514 :
515 : var withHeader = replyWidgets;
516 :
517 0 : var original = StaticMessageBubble(
518 : profile,
519 : settings,
520 0 : cache.cache[messageID]!.metadata,
521 0 : Row(children: [Flexible(child: compileOverlay(cache.cache[messageID]!).getPreviewWidget(context))]),
522 : );
523 :
524 0 : withHeader.insert(
525 : 0,
526 0 : Padding(
527 0 : padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
528 0 : child: Center(child: original),
529 : ),
530 : );
531 :
532 0 : withHeader.insert(
533 : 1,
534 0 : Padding(
535 0 : padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
536 0 : child: Divider(color: settings.theme.mainTextColor),
537 : ),
538 : );
539 :
540 0 : if (replies.isNotEmpty) {
541 0 : withHeader.insert(
542 : 2,
543 0 : Padding(
544 0 : padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
545 0 : child: Text(replyHeader, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
546 : ),
547 : );
548 : } else {
549 0 : withHeader.insert(
550 : 2,
551 0 : Padding(
552 0 : padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
553 0 : child: Center(
554 0 : child: Text(noRepliesText, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
555 : ),
556 : ),
557 : );
558 : }
559 :
560 0 : return Scrollbar(
561 : trackVisibility: true,
562 : controller: controller,
563 0 : child: SingleChildScrollView(
564 : clipBehavior: Clip.antiAlias,
565 0 : child: ConstrainedBox(
566 0 : constraints: BoxConstraints(minHeight: viewportConstraints.maxHeight),
567 0 : child: Padding(
568 0 : padding: EdgeInsets.symmetric(vertical: 0.0, horizontal: 20.0),
569 0 : child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: withHeader),
570 : ),
571 : ),
572 : ),
573 : );
574 : },
575 : ),
576 : );
577 : },
578 : );
579 : }
580 :
581 0 : void modalShowTranslation(BuildContext context, ProfileInfoState profile, Settings settings) async {
582 0 : showModalBottomSheet<void>(
583 0 : builder: (BuildContext bcontext) {
584 0 : return StatefulBuilder(
585 0 : builder: (BuildContext scontext, StateSetter setState /*You can rename this!*/) {
586 0 : if (scontext.mounted) {
587 0 : new Timer.periodic(Duration(seconds: 1), (Timer t) {
588 0 : if (scontext.mounted) {
589 0 : setState(() {});
590 : }
591 : });
592 : }
593 :
594 0 : var bubble = StaticMessageBubble(
595 : profile,
596 : settings,
597 0 : MessageMetadata(profile.onion, Provider.of<ContactInfoState>(context, listen: false).identifier, 1, DateTime.now(), "blodeuwedd", null, null, null, true, false, false, ""),
598 0 : Row(
599 0 : children: [
600 0 : Provider.of<MessageMetadata>(context).translation == ""
601 0 : ? Column(
602 : crossAxisAlignment: CrossAxisAlignment.center,
603 0 : children: [
604 0 : CircularProgressIndicator(color: settings.theme.defaultButtonActiveColor),
605 0 : Padding(padding: EdgeInsets.all(5.0), child: Text(AppLocalizations.of(context)!.blodeuweddProcessing)),
606 : ],
607 : )
608 0 : : Flexible(child: SelectableText(Provider.of<MessageMetadata>(context).translation)),
609 : ],
610 : ),
611 : );
612 :
613 0 : var image = Padding(
614 0 : padding: EdgeInsets.all(4.0),
615 0 : child: ProfileImage(imagePath: "assets/blodeuwedd.png", diameter: 48.0, border: settings.theme.portraitOnlineBorderColor, badgeTextColor: Colors.red, badgeColor: Colors.red),
616 : );
617 :
618 0 : return Container(
619 : height: 300, // bespoke value courtesy of the [TextField] docs
620 0 : child: Container(
621 : alignment: Alignment.center,
622 0 : child: Padding(
623 0 : padding: EdgeInsets.all(10.0),
624 0 : child: Padding(
625 0 : padding: EdgeInsets.all(10.0),
626 0 : child: Row(
627 : mainAxisSize: MainAxisSize.min,
628 0 : children: [
629 : image,
630 0 : Flexible(child: bubble),
631 : ],
632 : ),
633 : ),
634 : ),
635 : ),
636 : );
637 : },
638 : );
639 : },
640 : context: context,
641 : );
642 : }
643 :
644 0 : void modalShowModerationOptions(BuildContext ctx, String? signature, bool isHidden, bool isWithheld) async {
645 : try {
646 0 : showModalBottomSheet<void>(
647 : context: ctx,
648 0 : builder: (BuildContext bcontext) {
649 0 : var elements = <Widget>[CwtchLabel(label: AppLocalizations.of(ctx)!.titleModerationOptions), SizedBox(height: 20)];
650 :
651 : if (isWithheld) {
652 0 : elements.add(
653 0 : ElevatedButton(
654 0 : child: Row(
655 : mainAxisAlignment: MainAxisAlignment.center,
656 : spacing: 12.0,
657 0 : children: [
658 0 : Icon(CwtchIcons.eye_open),
659 0 : Text(AppLocalizations.of(ctx)!.buttonMessageReveal, semanticsLabel: AppLocalizations.of(ctx)!.buttonMessageReveal),
660 : ],
661 : ),
662 0 : onPressed: () {
663 0 : if (signature != null && signature!.isNotEmpty) {
664 0 : Provider.of<ContactInfoState>(ctx, listen: false).isOpPending = true;
665 0 : Provider.of<FlwtchState>(
666 : ctx,
667 : listen: false,
668 0 : ).cwtch.ModerateMessage(Provider.of<ProfileInfoState>(ctx, listen: false).onion, Provider.of<ContactInfoState>(ctx, listen: false).identifier, signature!, "R");
669 : }
670 0 : Navigator.pop(bcontext);
671 : },
672 : ),
673 : );
674 : } else if (isHidden) {
675 0 : elements.add(
676 0 : ElevatedButton(
677 0 : child: Row(
678 : mainAxisAlignment: MainAxisAlignment.center,
679 : spacing: 12.0,
680 0 : children: [
681 0 : Icon(CwtchIcons.eye_open),
682 0 : Text(AppLocalizations.of(ctx)!.buttonMessageUnhide, semanticsLabel: AppLocalizations.of(ctx)!.buttonMessageUnhide),
683 : ],
684 : ),
685 0 : onPressed: () {
686 0 : if (signature != null && signature!.isNotEmpty) {
687 0 : Provider.of<ContactInfoState>(ctx, listen: false).isOpPending = true;
688 0 : Provider.of<FlwtchState>(
689 : ctx,
690 : listen: false,
691 0 : ).cwtch.ModerateMessage(Provider.of<ProfileInfoState>(ctx, listen: false).onion, Provider.of<ContactInfoState>(ctx, listen: false).identifier, signature!, "U");
692 : }
693 0 : Navigator.pop(bcontext);
694 : },
695 : ),
696 : );
697 : } else {
698 : // not hidden
699 0 : elements.add(
700 0 : ElevatedButton(
701 0 : child: Row(
702 : mainAxisAlignment: MainAxisAlignment.center,
703 : spacing: 12.0,
704 0 : children: [
705 0 : Icon(CwtchIcons.eye_closed),
706 0 : Text(AppLocalizations.of(ctx)!.buttonMessageHide, semanticsLabel: AppLocalizations.of(ctx)!.buttonMessageHide),
707 : ],
708 : ),
709 0 : onPressed: () {
710 0 : if (signature != null && signature!.isNotEmpty) {
711 0 : Provider.of<ContactInfoState>(ctx, listen: false).isOpPending = true;
712 0 : Provider.of<FlwtchState>(
713 : ctx,
714 : listen: false,
715 0 : ).cwtch.ModerateMessage(Provider.of<ProfileInfoState>(ctx, listen: false).onion, Provider.of<ContactInfoState>(ctx, listen: false).identifier, signature!, "H");
716 : }
717 0 : Navigator.pop(bcontext);
718 : },
719 : ),
720 : );
721 : }
722 :
723 0 : elements.add(SizedBox(height: 20));
724 0 : elements.add(
725 0 : ElevatedButton(
726 0 : child: Row(
727 : mainAxisAlignment: MainAxisAlignment.center,
728 : spacing: 12.0,
729 0 : children: [
730 0 : Icon(CwtchIcons.delete_24px),
731 0 : Text(AppLocalizations.of(ctx)!.buttonMessageDelete, semanticsLabel: AppLocalizations.of(ctx)!.buttonMessageDelete),
732 : ],
733 : ),
734 0 : onPressed: () {
735 0 : if (signature != null && signature!.isNotEmpty) {
736 0 : Provider.of<ContactInfoState>(ctx, listen: false).isOpPending = true;
737 0 : Provider.of<FlwtchState>(
738 : ctx,
739 : listen: false,
740 0 : ).cwtch.ModerateMessage(Provider.of<ProfileInfoState>(ctx, listen: false).onion, Provider.of<ContactInfoState>(ctx, listen: false).identifier, signature!, "D");
741 : }
742 0 : Navigator.pop(bcontext);
743 : },
744 : ),
745 : );
746 0 : elements.add(SizedBox(height: 20));
747 0 : elements.add(
748 0 : ElevatedButton(
749 0 : child: Text(AppLocalizations.of(ctx)!.cancel, semanticsLabel: AppLocalizations.of(ctx)!.cancel),
750 0 : onPressed: () {
751 0 : Navigator.pop(bcontext);
752 : },
753 : ),
754 : );
755 :
756 0 : return Container(
757 : height: 340,
758 0 : child: Center(
759 0 : child: Padding(
760 0 : padding: EdgeInsets.all(10.0),
761 0 : child: Column(mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: elements),
762 : ),
763 : ),
764 : );
765 : },
766 : );
767 : } catch (e) {
768 0 : print(e);
769 : }
770 : }
771 :
772 : // temporary until we do real picture selection
773 0 : String RandomProfileImage(String onion) {
774 0 : var choices = [
775 : "001-centaur",
776 : "002-kraken",
777 : "003-dinosaur",
778 : "004-tree-1",
779 : "005-hand",
780 : "006-echidna",
781 : "007-robot",
782 : "008-mushroom",
783 : "009-harpy",
784 : "010-phoenix",
785 : "011-dragon-1",
786 : "012-devil",
787 : "013-troll",
788 : "014-alien",
789 : "015-minotaur",
790 : "016-madre-monte",
791 : "017-satyr",
792 : "018-karakasakozou",
793 : "019-pirate",
794 : "020-werewolf",
795 : "021-scarecrow",
796 : "022-valkyrie",
797 : "023-curupira",
798 : "024-loch-ness-monster",
799 : "025-tree",
800 : "026-cerberus",
801 : "027-gryphon",
802 : "028-mermaid",
803 : "029-vampire",
804 : "030-goblin",
805 : "031-yeti",
806 : "032-leprechaun",
807 : "033-medusa",
808 : "034-chimera",
809 : "035-elf",
810 : "036-hydra",
811 : "037-cyclops",
812 : "038-pegasus",
813 : "039-narwhal",
814 : "040-woodcutter",
815 : "041-zombie",
816 : "042-dragon",
817 : "043-frankenstein",
818 : "044-witch",
819 : "045-fairy",
820 : "046-genie",
821 : "047-pinocchio",
822 : "048-ghost",
823 : "049-wizard",
824 : "050-unicorn",
825 : ];
826 0 : var encoding = base32.decode(onion.toUpperCase());
827 0 : return "assets/profiles/" + choices[encoding[33] % choices.length] + ".png";
828 : }
|