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