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