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).checkOpPending(context);
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 0 : onTap: !isGroup && !contact.isManaged
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 0 : tooltip: !isGroup && !contact.isManaged
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 contact = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context, listen: false).senderHandle);
403 : if (contact == null) {
404 : // Can't happen
405 0 : } else if (contact.isShadowed == false) {
406 0 : selectConversation(context, contact.identifier, null);
407 0 : var contactIndex = Provider.of<ProfileInfoState>(context, listen: false).filteredList().indexWhere((element) => element.identifier == contact.identifier);
408 : // Scrolling/jumping looks glitchy on short contact lists, so just set it if we don't have enough contacts.
409 0 : if (Provider.of<ProfileInfoState>(context, listen: false).filteredList().length > 15) {
410 0 : Provider.of<ProfileInfoState>(context, listen: false).contactListScrollController.scrollTo(index: contactIndex, duration: Duration(milliseconds: 1), curve: Curves.ease);
411 : } else {
412 : // Only select...
413 : }
414 : }
415 : }
416 :
417 0 : void _btnAdd() {
418 0 : var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
419 0 : if (sender == "") {
420 0 : print("sender not yet loaded");
421 : return;
422 : }
423 0 : var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
424 :
425 0 : showAddContactConfirmAlertDialog(context, profileOnion, sender);
426 : }
427 :
428 0 : showAddContactConfirmAlertDialog(BuildContext context, String profileOnion, String senderOnion) {
429 : // set up the buttons
430 0 : Widget cancelButton = ElevatedButton(
431 0 : child: Text(AppLocalizations.of(context)!.cancel),
432 0 : style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
433 0 : onPressed: () {
434 0 : Navigator.of(context).pop(); // dismiss dialog
435 : },
436 : );
437 0 : Widget continueButton = ElevatedButton(
438 0 : style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
439 0 : child: Text(AppLocalizations.of(context)!.addContact),
440 0 : onPressed: () {
441 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, senderOnion);
442 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact), duration: Duration(seconds: 2));
443 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
444 0 : Navigator.of(context).pop(); // dismiss dialog
445 : },
446 : );
447 :
448 : // set up the AlertDialog
449 0 : AlertDialog alert = AlertDialog(title: Text(AppLocalizations.of(context)!.addContactConfirm.replaceFirst("%1", senderOnion)), actions: [cancelButton, continueButton]);
450 :
451 : // show the dialog
452 0 : showDialog(
453 : context: context,
454 0 : builder: (BuildContext context) {
455 : return alert;
456 : },
457 : );
458 : }
459 : }
460 :
461 0 : void modalShowReplies(
462 : BuildContext ctx,
463 : String replyHeader,
464 : String noRepliesText,
465 : Settings settings,
466 : ProfileInfoState profile,
467 : ContactInfoState cis,
468 : Color borderColor,
469 : MessageCache cache,
470 : int messageID, {
471 : bool showImage = true,
472 : }) {
473 0 : showModalBottomSheet<void>(
474 : context: ctx,
475 0 : builder: (BuildContext bcontext) {
476 0 : List<Message> replies = getReplies(cache, messageID);
477 0 : ScrollController controller = ScrollController();
478 :
479 0 : return MultiProvider(
480 0 : providers: [
481 0 : ChangeNotifierProvider.value(value: Provider.of<ContactInfoState>(ctx)),
482 0 : ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(ctx)),
483 : ],
484 0 : child: LayoutBuilder(
485 0 : builder: (BuildContext context, BoxConstraints viewportConstraints) {
486 0 : var replyWidgets = replies.map((e) {
487 0 : var fromMe = e.getMetadata().senderHandle == profile.onion;
488 :
489 0 : var bubble = StaticMessageBubble(profile, settings, e.getMetadata(), Row(children: [Flexible(child: e.getPreviewWidget(context))]));
490 :
491 0 : String imagePath = e.getMetadata().senderImage!;
492 0 : var sender = profile.contactList.findContact(e.getMetadata().senderHandle);
493 : if (sender != null) {
494 0 : imagePath = showImage ? sender.imagePath : sender.defaultImagePath;
495 : } else {
496 0 : imagePath = RandomProfileImage(e.getMetadata().senderHandle);
497 : }
498 :
499 : if (fromMe) {
500 0 : imagePath = profile.imagePath;
501 : }
502 :
503 0 : var image = Padding(
504 0 : padding: EdgeInsets.all(4.0),
505 0 : child: ProfileImage(imagePath: imagePath, diameter: 48.0, border: borderColor, badgeTextColor: Colors.red, badgeColor: Colors.red),
506 : );
507 :
508 0 : return Padding(
509 0 : padding: EdgeInsets.all(10.0),
510 0 : child: Row(
511 : mainAxisSize: MainAxisSize.min,
512 0 : children: [
513 : image,
514 0 : Flexible(child: bubble),
515 : ],
516 : ),
517 : );
518 0 : }).toList();
519 :
520 : var withHeader = replyWidgets;
521 :
522 0 : var original = StaticMessageBubble(
523 : profile,
524 : settings,
525 0 : cache.cache[messageID]!.metadata,
526 0 : Row(children: [Flexible(child: compileOverlay(cache.cache[messageID]!).getPreviewWidget(context))]),
527 : );
528 :
529 0 : withHeader.insert(
530 : 0,
531 0 : Padding(
532 0 : padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
533 0 : child: Center(child: original),
534 : ),
535 : );
536 :
537 0 : withHeader.insert(
538 : 1,
539 0 : Padding(
540 0 : padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
541 0 : child: Divider(color: settings.theme.mainTextColor),
542 : ),
543 : );
544 :
545 0 : if (replies.isNotEmpty) {
546 0 : withHeader.insert(
547 : 2,
548 0 : Padding(
549 0 : padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
550 0 : child: Text(replyHeader, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
551 : ),
552 : );
553 : } else {
554 0 : withHeader.insert(
555 : 2,
556 0 : Padding(
557 0 : padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
558 0 : child: Center(
559 0 : child: Text(noRepliesText, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
560 : ),
561 : ),
562 : );
563 : }
564 :
565 0 : return Scrollbar(
566 : trackVisibility: true,
567 : controller: controller,
568 0 : child: SingleChildScrollView(
569 : clipBehavior: Clip.antiAlias,
570 0 : child: ConstrainedBox(
571 0 : constraints: BoxConstraints(minHeight: viewportConstraints.maxHeight),
572 0 : child: Padding(
573 0 : padding: EdgeInsets.symmetric(vertical: 0.0, horizontal: 20.0),
574 0 : child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: withHeader),
575 : ),
576 : ),
577 : ),
578 : );
579 : },
580 : ),
581 : );
582 : },
583 : );
584 : }
585 :
586 0 : void modalShowTranslation(BuildContext context, ProfileInfoState profile, Settings settings) async {
587 0 : showModalBottomSheet<void>(
588 0 : builder: (BuildContext bcontext) {
589 0 : return StatefulBuilder(
590 0 : builder: (BuildContext scontext, StateSetter setState /*You can rename this!*/) {
591 0 : if (scontext.mounted) {
592 0 : new Timer.periodic(Duration(seconds: 1), (Timer t) {
593 0 : if (scontext.mounted) {
594 0 : setState(() {});
595 : }
596 : });
597 : }
598 :
599 0 : var bubble = StaticMessageBubble(
600 : profile,
601 : settings,
602 0 : MessageMetadata(profile.onion, Provider.of<ContactInfoState>(context, listen: false).identifier, 1, DateTime.now(), "blodeuwedd", null, null, null, true, false, false, ""),
603 0 : Row(
604 0 : children: [
605 0 : Provider.of<MessageMetadata>(context).translation == ""
606 0 : ? Column(
607 : crossAxisAlignment: CrossAxisAlignment.center,
608 0 : children: [
609 0 : CircularProgressIndicator(color: settings.theme.defaultButtonActiveColor),
610 0 : Padding(padding: EdgeInsets.all(5.0), child: Text(AppLocalizations.of(context)!.blodeuweddProcessing)),
611 : ],
612 : )
613 0 : : Flexible(child: SelectableText(Provider.of<MessageMetadata>(context).translation)),
614 : ],
615 : ),
616 : );
617 :
618 0 : var image = Padding(
619 0 : padding: EdgeInsets.all(4.0),
620 0 : child: ProfileImage(imagePath: "assets/blodeuwedd.png", diameter: 48.0, border: settings.theme.portraitOnlineBorderColor, badgeTextColor: Colors.red, badgeColor: Colors.red),
621 : );
622 :
623 0 : return Container(
624 : height: 300, // bespoke value courtesy of the [TextField] docs
625 0 : child: Container(
626 : alignment: Alignment.center,
627 0 : child: Padding(
628 0 : padding: EdgeInsets.all(10.0),
629 0 : child: Padding(
630 0 : padding: EdgeInsets.all(10.0),
631 0 : child: Row(
632 : mainAxisSize: MainAxisSize.min,
633 0 : children: [
634 : image,
635 0 : Flexible(child: bubble),
636 : ],
637 : ),
638 : ),
639 : ),
640 : ),
641 : );
642 : },
643 : );
644 : },
645 : context: context,
646 : );
647 : }
648 :
649 0 : void modalShowModerationOptions(BuildContext ctx, String? signature, bool isHidden, bool isWithheld) async {
650 : try {
651 0 : showModalBottomSheet<void>(
652 : context: ctx,
653 0 : builder: (BuildContext bcontext) {
654 0 : var elements = <Widget>[CwtchLabel(label: AppLocalizations.of(ctx)!.titleModerationOptions), SizedBox(height: 20)];
655 :
656 : if (isWithheld) {
657 0 : elements.add(
658 0 : ElevatedButton(
659 0 : child: Row(
660 : mainAxisAlignment: MainAxisAlignment.center,
661 : spacing: 12.0,
662 0 : children: [
663 0 : Icon(CwtchIcons.eye_open),
664 0 : Text(AppLocalizations.of(ctx)!.buttonMessageReveal, semanticsLabel: AppLocalizations.of(ctx)!.buttonMessageReveal),
665 : ],
666 : ),
667 0 : onPressed: () {
668 0 : if (signature != null && signature!.isNotEmpty) {
669 0 : Provider.of<ContactInfoState>(ctx, listen: false).isOpPending = true;
670 0 : Provider.of<FlwtchState>(
671 : ctx,
672 : listen: false,
673 0 : ).cwtch.ModerateMessage(Provider.of<ProfileInfoState>(ctx, listen: false).onion, Provider.of<ContactInfoState>(ctx, listen: false).identifier, signature!, "R");
674 : }
675 0 : Navigator.pop(bcontext);
676 : },
677 : ),
678 : );
679 : } else if (isHidden) {
680 0 : elements.add(
681 0 : ElevatedButton(
682 0 : child: Row(
683 : mainAxisAlignment: MainAxisAlignment.center,
684 : spacing: 12.0,
685 0 : children: [
686 0 : Icon(CwtchIcons.eye_open),
687 0 : Text(AppLocalizations.of(ctx)!.buttonMessageUnhide, semanticsLabel: AppLocalizations.of(ctx)!.buttonMessageUnhide),
688 : ],
689 : ),
690 0 : onPressed: () {
691 0 : if (signature != null && signature!.isNotEmpty) {
692 0 : Provider.of<ContactInfoState>(ctx, listen: false).isOpPending = true;
693 0 : Provider.of<FlwtchState>(
694 : ctx,
695 : listen: false,
696 0 : ).cwtch.ModerateMessage(Provider.of<ProfileInfoState>(ctx, listen: false).onion, Provider.of<ContactInfoState>(ctx, listen: false).identifier, signature!, "U");
697 : }
698 0 : Navigator.pop(bcontext);
699 : },
700 : ),
701 : );
702 : } else {
703 : // not hidden
704 0 : elements.add(
705 0 : ElevatedButton(
706 0 : child: Row(
707 : mainAxisAlignment: MainAxisAlignment.center,
708 : spacing: 12.0,
709 0 : children: [
710 0 : Icon(CwtchIcons.eye_closed),
711 0 : Text(AppLocalizations.of(ctx)!.buttonMessageHide, semanticsLabel: AppLocalizations.of(ctx)!.buttonMessageHide),
712 : ],
713 : ),
714 0 : onPressed: () {
715 0 : if (signature != null && signature!.isNotEmpty) {
716 0 : Provider.of<ContactInfoState>(ctx, listen: false).isOpPending = true;
717 0 : Provider.of<FlwtchState>(
718 : ctx,
719 : listen: false,
720 0 : ).cwtch.ModerateMessage(Provider.of<ProfileInfoState>(ctx, listen: false).onion, Provider.of<ContactInfoState>(ctx, listen: false).identifier, signature!, "H");
721 : }
722 0 : Navigator.pop(bcontext);
723 : },
724 : ),
725 : );
726 : }
727 :
728 0 : elements.add(SizedBox(height: 20));
729 0 : elements.add(
730 0 : ElevatedButton(
731 0 : child: Row(
732 : mainAxisAlignment: MainAxisAlignment.center,
733 : spacing: 12.0,
734 0 : children: [
735 0 : Icon(CwtchIcons.delete_24px),
736 0 : Text(AppLocalizations.of(ctx)!.buttonMessageDelete, semanticsLabel: AppLocalizations.of(ctx)!.buttonMessageDelete),
737 : ],
738 : ),
739 0 : onPressed: () {
740 0 : if (signature != null && signature!.isNotEmpty) {
741 0 : Provider.of<ContactInfoState>(ctx, listen: false).isOpPending = true;
742 0 : Provider.of<FlwtchState>(
743 : ctx,
744 : listen: false,
745 0 : ).cwtch.ModerateMessage(Provider.of<ProfileInfoState>(ctx, listen: false).onion, Provider.of<ContactInfoState>(ctx, listen: false).identifier, signature!, "D");
746 : }
747 0 : Navigator.pop(bcontext);
748 : },
749 : ),
750 : );
751 0 : elements.add(SizedBox(height: 20));
752 0 : elements.add(
753 0 : ElevatedButton(
754 0 : child: Text(AppLocalizations.of(ctx)!.cancel, semanticsLabel: AppLocalizations.of(ctx)!.cancel),
755 0 : onPressed: () {
756 0 : Navigator.pop(bcontext);
757 : },
758 : ),
759 : );
760 :
761 0 : return Container(
762 : height: 340,
763 0 : child: Center(
764 0 : child: Padding(
765 0 : padding: EdgeInsets.all(10.0),
766 0 : child: Column(mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: elements),
767 : ),
768 : ),
769 : );
770 : },
771 : );
772 : } catch (e) {
773 0 : print(e);
774 : }
775 : }
776 :
777 : // temporary until we do real picture selection
778 0 : String RandomProfileImage(String onion) {
779 0 : var choices = [
780 : "001-centaur",
781 : "002-kraken",
782 : "003-dinosaur",
783 : "004-tree-1",
784 : "005-hand",
785 : "006-echidna",
786 : "007-robot",
787 : "008-mushroom",
788 : "009-harpy",
789 : "010-phoenix",
790 : "011-dragon-1",
791 : "012-devil",
792 : "013-troll",
793 : "014-alien",
794 : "015-minotaur",
795 : "016-madre-monte",
796 : "017-satyr",
797 : "018-karakasakozou",
798 : "019-pirate",
799 : "020-werewolf",
800 : "021-scarecrow",
801 : "022-valkyrie",
802 : "023-curupira",
803 : "024-loch-ness-monster",
804 : "025-tree",
805 : "026-cerberus",
806 : "027-gryphon",
807 : "028-mermaid",
808 : "029-vampire",
809 : "030-goblin",
810 : "031-yeti",
811 : "032-leprechaun",
812 : "033-medusa",
813 : "034-chimera",
814 : "035-elf",
815 : "036-hydra",
816 : "037-cyclops",
817 : "038-pegasus",
818 : "039-narwhal",
819 : "040-woodcutter",
820 : "041-zombie",
821 : "042-dragon",
822 : "043-frankenstein",
823 : "044-witch",
824 : "045-fairy",
825 : "046-genie",
826 : "047-pinocchio",
827 : "048-ghost",
828 : "049-wizard",
829 : "050-unicorn",
830 : ];
831 0 : var encoding = base32.decode(onion.toUpperCase());
832 0 : return "assets/profiles/" + choices[encoding[33] % choices.length] + ".png";
833 : }
|