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