Line data Source code
1 : import 'dart:async';
2 : import 'dart:convert';
3 : import 'dart:io';
4 : import 'dart:math';
5 : import 'package:crypto/crypto.dart';
6 : import 'package:cwtch/cwtch/cwtch.dart';
7 : import 'package:cwtch/cwtch_icons_icons.dart';
8 : import 'package:cwtch/models/appstate.dart';
9 : import 'package:cwtch/models/chatmessage.dart';
10 : import 'package:cwtch/models/contact.dart';
11 : import 'package:cwtch/models/message.dart';
12 : import 'package:cwtch/models/messagecache.dart';
13 : import 'package:cwtch/models/messages/quotedmessage.dart';
14 : import 'package:cwtch/models/profile.dart';
15 : import 'package:cwtch/models/search.dart';
16 : import 'package:cwtch/themes/opaque.dart';
17 : import 'package:cwtch/third_party/linkify/flutter_linkify.dart';
18 : import 'package:cwtch/widgets/conversation_options.dart';
19 : import 'package:cwtch/widgets/malformedbubble.dart';
20 : import 'package:cwtch/widgets/messageloadingbubble.dart';
21 : import 'package:cwtch/widgets/profileimage.dart';
22 : import 'package:cwtch/controllers/filesharing.dart' as filesharing;
23 : import 'package:cwtch/widgets/staticmessagebubble.dart';
24 : import 'package:flutter/material.dart';
25 : import 'package:cwtch/views/peersettingsview.dart';
26 : import 'package:cwtch/widgets/DropdownContacts.dart';
27 : import 'package:flutter/services.dart';
28 : import 'package:provider/provider.dart';
29 : import 'package:flutter_gen/gen_l10n/app_localizations.dart';
30 : import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
31 :
32 : import '../config.dart';
33 : import '../constants.dart';
34 : import '../main.dart';
35 : import '../settings.dart';
36 : import '../widgets/messagelist.dart';
37 : import 'filesharingview.dart';
38 : import 'groupsettingsview.dart';
39 :
40 : class MessageView extends StatefulWidget {
41 0 : @override
42 0 : _MessageViewState createState() => _MessageViewState();
43 : }
44 :
45 : class _MessageViewState extends State<MessageView> {
46 : final focusNode = FocusNode();
47 : int selectedContact = -1;
48 : ItemPositionsListener scrollListener = ItemPositionsListener.create();
49 : File? imagePreview;
50 : bool showDown = false;
51 : bool showPreview = false;
52 : final scaffoldKey = GlobalKey<ScaffoldState>(); // <---- Another instance variable
53 0 : @override
54 : void initState() {
55 0 : scrollListener.itemPositions.addListener(() {
56 0 : if (scrollListener.itemPositions.value.length != 0 &&
57 0 : Provider.of<AppState>(context, listen: false).unreadMessagesBelow == true &&
58 0 : scrollListener.itemPositions.value.any((element) => element.index == 0)) {
59 0 : Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
60 0 : Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
61 : }
62 :
63 0 : if (scrollListener.itemPositions.value.length != 0 && !scrollListener.itemPositions.value.any((element) => element.index == 0)) {
64 0 : showDown = true;
65 : } else {
66 0 : showDown = false;
67 : }
68 : });
69 0 : super.initState();
70 : }
71 :
72 0 : @override
73 : void didChangeDependencies() {
74 0 : var appState = Provider.of<AppState>(context, listen: false);
75 :
76 : // using "8" because "# of messages that fit on one screen" isnt trivial to calculate at this point
77 0 : if (appState.initialScrollIndex > 4 && appState.unreadMessagesBelow == false) {
78 0 : WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((timeStamp) {
79 0 : appState.unreadMessagesBelow = true;
80 : });
81 : }
82 0 : super.didChangeDependencies();
83 : }
84 :
85 0 : @override
86 : void dispose() {
87 0 : focusNode.dispose();
88 0 : super.dispose();
89 : }
90 :
91 0 : @override
92 : Widget build(BuildContext context) {
93 : // After leaving a conversation the selected conversation is set to null...
94 0 : if (Provider.of<ContactInfoState>(context, listen: false).profileOnion == "") {
95 0 : return Container(color: Provider.of<Settings>(context).theme.backgroundMainColor, child: Center(child: Text(AppLocalizations.of(context)!.addContactFirst)));
96 : }
97 :
98 0 : var showMessageFormattingPreview = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
99 0 : var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
100 0 : var appBarButtons = <Widget>[];
101 :
102 0 : var profile = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
103 0 : var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
104 :
105 0 : if (Provider.of<FlwtchState>(context, listen: false).cwtch.IsBlodeuweddSupported() && Provider.of<Settings>(context).isExperimentEnabled(BlodeuweddExperiment)) {
106 0 : appBarButtons.add(IconButton(
107 0 : splashRadius: Material.defaultSplashRadius / 2,
108 0 : icon: Icon(Icons.summarize),
109 0 : tooltip: AppLocalizations.of(context)!.blodeuweddSummarize,
110 0 : onPressed: () async {
111 0 : Provider.of<ContactInfoState>(context, listen: false).summary = "";
112 0 : Provider.of<ContactInfoState>(context, listen: false).updateSummaryEvent("");
113 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SummarizeConversation(profile, conversation);
114 0 : _summarizeConversation(context, Provider.of<ProfileInfoState>(context, listen: false), Provider.of<Settings>(context, listen: false));
115 : }));
116 : }
117 :
118 0 : if (Provider.of<ContactInfoState>(context).isOnline()) {
119 : if (showFileSharing) {
120 0 : appBarButtons.add(IconButton(
121 0 : splashRadius: Material.defaultSplashRadius / 2,
122 0 : icon: Icon(CwtchIcons.attached_file_3, size: 26, color: Provider.of<Settings>(context).theme.mainTextColor),
123 0 : tooltip: AppLocalizations.of(context)!.tooltipSendFile,
124 0 : onPressed: Provider.of<AppState>(context).disableFilePicker
125 : ? null
126 0 : : () {
127 0 : imagePreview = null;
128 0 : filesharing.showFilePicker(context, MaxGeneralFileSharingSize, (File file) {
129 0 : _confirmFileSend(context, file.path);
130 0 : }, () {
131 0 : final snackBar = SnackBar(
132 0 : content: Text(AppLocalizations.of(context)!.msgFileTooBig),
133 0 : duration: Duration(seconds: 4),
134 : );
135 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
136 0 : }, () {});
137 : },
138 : ));
139 : }
140 :
141 0 : appBarButtons.add(IconButton(
142 0 : splashRadius: Material.defaultSplashRadius / 2,
143 0 : icon: Icon(CwtchIcons.send_invite, size: 24),
144 0 : tooltip: AppLocalizations.of(context)!.sendInvite,
145 0 : onPressed: () {
146 0 : _modalSendInvitation(context);
147 : }));
148 : }
149 :
150 0 : appBarButtons.add(ConversationOptions(_pushContactSettings, _pushFileSharingSettings));
151 :
152 0 : var appState = Provider.of<AppState>(context);
153 0 : return PopScope(
154 0 : onPopInvoked: _onWillPop,
155 0 : child: Scaffold(
156 0 : backgroundColor: Provider.of<Settings>(context).theme.backgroundMainColor,
157 0 : floatingActionButton: showDown
158 0 : ? FloatingActionButton(
159 : // heroTags need to be unique per screen (important when we pop up and down)...
160 0 : heroTag: "popDown" + Provider.of<ContactInfoState>(context, listen: false).onion,
161 0 : child: Icon(Icons.arrow_downward, color: Provider.of<Settings>(context).current().defaultButtonTextColor),
162 0 : onPressed: () {
163 0 : Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
164 0 : Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
165 0 : Provider.of<ContactInfoState>(context, listen: false).messageScrollController.scrollTo(index: 0, duration: Duration(milliseconds: 600));
166 : })
167 : : null,
168 0 : appBar: AppBar(
169 : // setting leading(Width) to null makes it do the default behaviour; container() hides it
170 0 : leadingWidth: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? 0 : null,
171 0 : leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1
172 0 : ? Container(
173 : padding: EdgeInsets.zero,
174 : margin: EdgeInsets.zero,
175 : width: 0,
176 : height: 0,
177 : )
178 : : null,
179 0 : title: Row(children: [
180 0 : ProfileImage(
181 0 : imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment)
182 0 : ? Provider.of<ContactInfoState>(context).imagePath
183 0 : : Provider.of<ContactInfoState>(context).defaultImagePath,
184 : diameter: 42,
185 0 : border: Provider.of<ContactInfoState>(context).getBorderColor(Provider.of<Settings>(context).theme),
186 0 : disabled: !Provider.of<ContactInfoState>(context).isOnline(),
187 : badgeTextColor: Colors.red,
188 0 : badgeColor: Provider.of<Settings>(context).theme.portraitContactBadgeColor,
189 0 : badgeIcon: Provider.of<ContactInfoState>(context).isGroup
190 0 : ? (Tooltip(
191 0 : message: Provider.of<ContactInfoState>(context).isOnline()
192 0 : ? Provider.of<ContactInfoState>(context).antispamTickets == 0
193 0 : ? AppLocalizations.of(context)!.acquiringTicketsFromServer
194 0 : : AppLocalizations.of(context)!.acquiredTicketsFromServer
195 0 : : AppLocalizations.of(context)!.serverConnectivityDisconnected,
196 0 : child: Provider.of<ContactInfoState>(context).isOnline()
197 0 : ? Provider.of<ContactInfoState>(context).antispamTickets == 0
198 0 : ? Icon(
199 : CwtchIcons.anti_spam_3,
200 : size: 14.0,
201 0 : semanticLabel: AppLocalizations.of(context)!.acquiringTicketsFromServer,
202 0 : color: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor,
203 : )
204 0 : : Icon(
205 : CwtchIcons.anti_spam_2,
206 0 : color: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor,
207 : size: 14.0,
208 : )
209 0 : : Icon(
210 : CwtchIcons.onion_off,
211 0 : color: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor,
212 : size: 14.0,
213 : )))
214 : : null),
215 0 : SizedBox(
216 : width: 10,
217 : ),
218 0 : Expanded(
219 0 : child: Container(
220 : height: 42,
221 : clipBehavior: Clip.hardEdge,
222 0 : decoration: BoxDecoration(),
223 0 : child: Align(
224 : alignment: Alignment.centerLeft,
225 0 : child: Text(
226 0 : Provider.of<ContactInfoState>(context).augmentedNickname(context),
227 0 : style: TextStyle(fontFamily: "Inter", fontWeight: FontWeight.bold, fontSize: 14.0 * Provider.of<Settings>(context).fontScaling),
228 : overflow: TextOverflow.clip,
229 : maxLines: 1,
230 : ))))
231 : ]),
232 : actions: appBarButtons,
233 : ),
234 0 : body: Padding(
235 0 : padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 164.0),
236 0 : child: MessageList(
237 0 : scrollListener,
238 : )),
239 0 : bottomSheet: showPreview && showMessageFormattingPreview ? _buildPreviewBox() : _buildComposeBox(context),
240 : ));
241 : }
242 :
243 0 : Future<bool> _onWillPop(popd) async {
244 0 : Provider.of<ContactInfoState>(context, listen: false).unreadMessages = 0;
245 :
246 0 : var previouslySelected = Provider.of<AppState>(context, listen: false).selectedConversation;
247 : if (previouslySelected != null) {
248 0 : Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(previouslySelected)!.unselected();
249 : }
250 0 : Provider.of<SearchState>(context, listen: false).clearSearch();
251 0 : Provider.of<AppState>(context, listen: false).selectedConversation = null;
252 0 : Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
253 : return true;
254 : }
255 :
256 0 : void _pushFileSharingSettings() {
257 0 : var profileInfoState = Provider.of<ProfileInfoState>(context, listen: false);
258 0 : var contactInfoState = Provider.of<ContactInfoState>(context, listen: false);
259 0 : Navigator.of(context).push(
260 0 : PageRouteBuilder(
261 0 : pageBuilder: (builderContext, a1, a2) {
262 0 : return MultiProvider(
263 0 : providers: [ChangeNotifierProvider.value(value: profileInfoState), ChangeNotifierProvider.value(value: contactInfoState)],
264 0 : child: FileSharingView(),
265 : );
266 : },
267 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
268 0 : transitionDuration: Duration(milliseconds: 200),
269 : ),
270 : );
271 : }
272 :
273 0 : void _pushContactSettings() {
274 0 : var profileInfoState = Provider.of<ProfileInfoState>(context, listen: false);
275 0 : var contactInfoState = Provider.of<ContactInfoState>(context, listen: false);
276 :
277 0 : if (Provider.of<ContactInfoState>(context, listen: false).isGroup == true) {
278 0 : Navigator.of(context).push(
279 0 : PageRouteBuilder(
280 0 : pageBuilder: (builderContext, a1, a2) {
281 0 : return MultiProvider(
282 0 : providers: [ChangeNotifierProvider.value(value: profileInfoState), ChangeNotifierProvider.value(value: contactInfoState)],
283 0 : child: GroupSettingsView(),
284 : );
285 : },
286 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
287 0 : transitionDuration: Duration(milliseconds: 200),
288 : ),
289 : );
290 : } else {
291 0 : Navigator.of(context).push(
292 0 : PageRouteBuilder(
293 0 : pageBuilder: (builderContext, a1, a2) {
294 0 : return MultiProvider(
295 0 : providers: [ChangeNotifierProvider.value(value: profileInfoState), ChangeNotifierProvider.value(value: contactInfoState)],
296 0 : child: PeerSettingsView(),
297 : );
298 : },
299 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
300 0 : transitionDuration: Duration(milliseconds: 200),
301 : ),
302 : );
303 : }
304 : }
305 :
306 : // todo: legacy groups currently have restricted message
307 : // size because of the additional wrapping end encoding
308 : // hybrid groups should allow these numbers to be the same.
309 : static const P2PMessageLengthMax = 7000;
310 : static const GroupMessageLengthMax = 1600;
311 :
312 0 : void _sendMessage([String? ignoredParam]) {
313 : // Do this after we trim to preserve enter-behaviour...
314 0 : bool cannotSend = Provider.of<ContactInfoState>(context, listen: false).canSend() == false;
315 0 : bool isGroup = Provider.of<ContactInfoState>(context, listen: false).isGroup;
316 : if (cannotSend) {
317 : return;
318 : }
319 :
320 0 : var attachedInvite = Provider.of<ContactInfoState>(context, listen: false).messageDraft.getInviteHandle();
321 : if (attachedInvite != null) {
322 0 : this._sendInvitation(attachedInvite);
323 : }
324 :
325 : // Trim message
326 0 : var messageText = Provider.of<ContactInfoState>(context, listen: false).messageDraft.messageText ?? "";
327 0 : final messageWithoutNewLine = messageText.trimRight();
328 :
329 : // peers and groups currently have different length constraints (servers can store less)...
330 0 : var actualMessageLength = messageText.length;
331 0 : var lengthOk = (isGroup && actualMessageLength < GroupMessageLengthMax) || actualMessageLength <= P2PMessageLengthMax;
332 :
333 0 : if (messageWithoutNewLine.isNotEmpty && lengthOk) {
334 0 : if (Provider.of<AppState>(context, listen: false).selectedConversation != null && Provider.of<ContactInfoState>(context, listen: false).messageDraft.getQuotedMessage() != null) {
335 0 : var conversationId = Provider.of<AppState>(context, listen: false).selectedConversation!;
336 0 : MessageCache? cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationId)?.messageCache;
337 0 : ById(Provider.of<ContactInfoState>(context, listen: false).messageDraft.getQuotedMessage()!.index)
338 0 : .get(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(context, listen: false).selectedProfile!, conversationId, cache!)
339 0 : .then((MessageInfo? data) {
340 : try {
341 0 : var bytes1 = utf8.encode(data!.metadata.senderHandle + data.wrapper);
342 0 : var digest1 = sha256.convert(bytes1);
343 0 : var contentHash = base64Encode(digest1.bytes);
344 0 : var quotedMessage = jsonEncode(QuotedMessageStructure(contentHash, messageWithoutNewLine));
345 0 : ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage);
346 0 : Provider.of<FlwtchState>(context, listen: false)
347 0 : .cwtch
348 0 : .SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm))
349 0 : .then(_sendMessageHandler);
350 : } catch (e) {
351 0 : EnvironmentConfig.debugLog("Exception: reply to message could not be found: " + e.toString());
352 : }
353 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearQuotedReference();
354 : });
355 : } else {
356 0 : ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: messageWithoutNewLine);
357 0 : Provider.of<FlwtchState>(context, listen: false)
358 0 : .cwtch
359 0 : .SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm))
360 0 : .then(_sendMessageHandler);
361 : }
362 : }
363 : }
364 :
365 0 : void _sendInvitation(int contact) {
366 0 : Provider.of<FlwtchState>(context, listen: false)
367 0 : .cwtch
368 0 : .SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, contact)
369 0 : .then(_sendMessageHandler);
370 : }
371 :
372 0 : void _sendFile(String filePath) {
373 0 : Provider.of<FlwtchState>(context, listen: false)
374 0 : .cwtch
375 0 : .ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, filePath)
376 0 : .then(_sendMessageHandler);
377 : }
378 :
379 0 : void _sendMessageHandler(dynamic messageJson) {
380 0 : if (Provider.of<ContactInfoState>(context, listen: false).isGroup && Provider.of<ContactInfoState>(context, listen: false).antispamTickets == 0) {
381 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.acquiringTicketsFromServer));
382 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
383 : return;
384 : }
385 :
386 : // At this point we have decided to send the text to the backend, failure is still possible
387 : // but it will show as an error-ed message, as such the draft can be purged.
388 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearDraft();
389 :
390 0 : var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
391 0 : var identifier = Provider.of<ContactInfoState>(context, listen: false).identifier;
392 0 : var profile = Provider.of<ProfileInfoState>(context, listen: false);
393 :
394 0 : var messageInfo = messageJsonToInfo(profileOnion, identifier, messageJson);
395 : if (messageInfo != null) {
396 0 : profile.newMessage(
397 0 : messageInfo.metadata.conversationIdentifier,
398 0 : messageInfo.metadata.messageID,
399 0 : messageInfo.metadata.timestamp,
400 0 : messageInfo.metadata.senderHandle,
401 0 : messageInfo.metadata.senderImage ?? "",
402 0 : messageInfo.metadata.isAuto,
403 0 : messageInfo.wrapper,
404 0 : messageInfo.metadata.contenthash,
405 : true,
406 : true,
407 : );
408 : }
409 :
410 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileOnion, identifier, LastMessageSeenTimeKey, DateTime.now().toIso8601String());
411 0 : focusNode.requestFocus();
412 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearDraft();
413 : }
414 :
415 0 : Widget senderInviteChrome(String chrome, String targetName) {
416 0 : var settings = Provider.of<Settings>(context);
417 :
418 0 : return Wrap(children: [
419 0 : SelectableText(
420 0 : chrome + '\u202F',
421 0 : style: settings.scaleFonts(defaultMessageTextStyle.copyWith(color: Provider.of<Settings>(context).theme.messageFromMeTextColor)),
422 : textAlign: TextAlign.left,
423 : maxLines: 2,
424 : textWidthBasis: TextWidthBasis.longestLine,
425 : ),
426 0 : SelectableText(
427 0 : targetName + '\u202F',
428 0 : style: settings.scaleFonts(defaultMessageTextStyle.copyWith(color: Provider.of<Settings>(context).theme.messageFromMeTextColor)),
429 : textAlign: TextAlign.left,
430 : maxLines: 2,
431 : textWidthBasis: TextWidthBasis.longestLine,
432 : )
433 : ]);
434 : }
435 :
436 0 : Widget _buildPreviewBox() {
437 0 : var showClickableLinks = Provider.of<Settings>(context).isExperimentEnabled(ClickableLinksExperiment);
438 :
439 0 : var wdgMessage = Padding(
440 0 : padding: EdgeInsets.all(8),
441 0 : child: SelectableLinkify(
442 0 : text: Provider.of<ContactInfoState>(context).messageDraft.messageText + '\n',
443 0 : options: LinkifyOptions(messageFormatting: true, parseLinks: showClickableLinks, looseUrl: true, defaultToHttps: true),
444 0 : linkifiers: [UrlLinkifier()],
445 : onOpen: showClickableLinks ? null : null,
446 0 : style: TextStyle(
447 0 : color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
448 : fontFamily: "Inter",
449 : fontWeight: FontWeight.normal,
450 0 : fontSize: 16.0 * Provider.of<Settings>(context).fontScaling,
451 : ),
452 0 : linkStyle: TextStyle(
453 0 : color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
454 : fontFamily: "Inter",
455 : fontWeight: FontWeight.normal,
456 0 : fontSize: 16.0 * Provider.of<Settings>(context).fontScaling,
457 : ),
458 0 : codeStyle: TextStyle(
459 : // note: these colors are flipped
460 : fontWeight: FontWeight.normal,
461 0 : fontSize: 16.0 * Provider.of<Settings>(context).fontScaling,
462 0 : color: Provider.of<Settings>(context).theme.messageFromOtherTextColor,
463 0 : backgroundColor: Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor),
464 : textAlign: TextAlign.left,
465 : textWidthBasis: TextWidthBasis.longestLine,
466 : constraints: null,
467 : ));
468 :
469 0 : var showMessageFormattingPreview = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
470 : var preview = showMessageFormattingPreview
471 0 : ? IconButton(
472 0 : tooltip: AppLocalizations.of(context)!.tooltipBackToMessageEditing,
473 0 : icon: Icon(Icons.text_fields),
474 0 : onPressed: () {
475 0 : setState(() {
476 0 : showPreview = false;
477 : });
478 : })
479 0 : : Container();
480 :
481 0 : var composeBox = Container(
482 0 : color: Provider.of<Settings>(context).theme.backgroundMainColor,
483 0 : padding: EdgeInsets.all(2),
484 0 : margin: EdgeInsets.all(2),
485 :
486 : // 164 minimum height + 16px for every line of text so the entire message is displayed when previewed.
487 0 : height: 164 + ((Provider.of<ContactInfoState>(context).messageDraft.messageText.split("\n").length - 1) * 16),
488 0 : child: Column(
489 0 : children: [
490 0 : Container(
491 0 : decoration: BoxDecoration(color: Provider.of<Settings>(context).theme.toolbarBackgroundColor),
492 0 : child: Row(mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [preview])),
493 0 : Container(
494 0 : decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor))),
495 0 : child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [wdgMessage])),
496 : ],
497 : ),
498 : );
499 0 : return Container(
500 0 : color: Provider.of<Settings>(context).theme.backgroundMainColor, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [composeBox]));
501 : }
502 :
503 0 : Widget _buildComposeBox(BuildContext context) {
504 0 : bool cannotSend = Provider.of<ContactInfoState>(context).canSend() == false;
505 0 : bool isGroup = Provider.of<ContactInfoState>(context).isGroup;
506 0 : var showToolbar = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
507 0 : var charLength = Provider.of<ContactInfoState>(context).messageDraft.messageText.characters.length;
508 0 : var expectedLength = Provider.of<ContactInfoState>(context).messageDraft.messageText.length;
509 0 : var numberOfBytesMoreThanChar = (expectedLength - charLength);
510 :
511 0 : var bold = IconButton(
512 0 : icon: Icon(Icons.format_bold),
513 0 : tooltip: AppLocalizations.of(context)!.tooltipBoldText,
514 0 : onPressed: () {
515 0 : setState(() {
516 0 : var ctrlrCompose = Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose;
517 0 : var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
518 0 : var selection = ctrlrCompose.selection;
519 0 : var start = ctrlrCompose.selection.start;
520 0 : var end = ctrlrCompose.selection.end;
521 0 : ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "**" + selected + "**");
522 0 : ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 2, extentOffset: selection.start + 2);
523 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose = ctrlrCompose;
524 : });
525 : });
526 :
527 0 : var italic = IconButton(
528 0 : icon: Icon(Icons.format_italic),
529 0 : tooltip: AppLocalizations.of(context)!.tooltipItalicize,
530 0 : onPressed: () {
531 0 : setState(() {
532 0 : var ctrlrCompose = Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose;
533 0 : var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
534 0 : var selection = ctrlrCompose.selection;
535 0 : var start = ctrlrCompose.selection.start;
536 0 : var end = ctrlrCompose.selection.end;
537 0 : ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "*" + selected + "*");
538 0 : ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
539 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose = ctrlrCompose;
540 : });
541 : });
542 :
543 0 : var code = IconButton(
544 0 : icon: Icon(Icons.code),
545 0 : tooltip: AppLocalizations.of(context)!.tooltipCode,
546 0 : onPressed: () {
547 0 : setState(() {
548 0 : var ctrlrCompose = Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose;
549 0 : var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
550 0 : var selection = ctrlrCompose.selection;
551 0 : var start = ctrlrCompose.selection.start;
552 0 : var end = ctrlrCompose.selection.end;
553 0 : ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "`" + selected + "`");
554 0 : ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
555 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose = ctrlrCompose;
556 : });
557 : });
558 :
559 0 : var superscript = IconButton(
560 0 : icon: Icon(Icons.superscript),
561 0 : tooltip: AppLocalizations.of(context)!.tooltipSuperscript,
562 0 : onPressed: () {
563 0 : setState(() {
564 0 : var ctrlrCompose = Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose;
565 0 : var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
566 0 : var selection = ctrlrCompose.selection;
567 0 : var start = ctrlrCompose.selection.start;
568 0 : var end = ctrlrCompose.selection.end;
569 0 : ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "^" + selected + "^");
570 0 : ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
571 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose = ctrlrCompose;
572 : });
573 : });
574 :
575 0 : var strikethrough = IconButton(
576 0 : icon: Icon(Icons.format_strikethrough),
577 0 : tooltip: AppLocalizations.of(context)!.tooltipStrikethrough,
578 0 : onPressed: () {
579 0 : setState(() {
580 0 : var ctrlrCompose = Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose;
581 0 : var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
582 0 : var selection = ctrlrCompose.selection;
583 0 : var start = ctrlrCompose.selection.start;
584 0 : var end = ctrlrCompose.selection.end;
585 0 : ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "~~" + selected + "~~");
586 0 : ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 2, extentOffset: selection.start + 2);
587 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose = ctrlrCompose;
588 : });
589 : });
590 :
591 0 : var preview = IconButton(
592 0 : icon: Icon(Icons.text_format),
593 0 : tooltip: AppLocalizations.of(context)!.tooltipPreviewFormatting,
594 0 : onPressed: () {
595 0 : setState(() {
596 0 : showPreview = true;
597 : });
598 : });
599 :
600 0 : var vline = Padding(
601 0 : padding: EdgeInsets.symmetric(vertical: 1, horizontal: 2), child: Container(height: 16, width: 1, decoration: BoxDecoration(color: Provider.of<Settings>(context).theme.toolbarIconColor)));
602 :
603 0 : var formattingToolbar = Container(
604 0 : decoration: BoxDecoration(color: Provider.of<Settings>(context).theme.toolbarBackgroundColor),
605 0 : child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [bold, italic, code, superscript, strikethrough, vline, preview]));
606 :
607 0 : var textField = Container(
608 0 : decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.backgroundHilightElementColor))),
609 0 : child: KeyboardListener(
610 0 : focusNode: FocusNode(),
611 0 : onKeyEvent: handleKeyPress,
612 0 : child: Padding(
613 0 : padding: EdgeInsets.all(8),
614 0 : child: TextFormField(
615 0 : key: Key('txtCompose'),
616 0 : controller: Provider.of<ContactInfoState>(context).messageDraft.ctrlCompose,
617 0 : focusNode: focusNode,
618 0 : autofocus: !Platform.isAndroid,
619 : textInputAction: TextInputAction.newline,
620 : textCapitalization: TextCapitalization.sentences,
621 : keyboardType: TextInputType.multiline,
622 : enableIMEPersonalizedLearning: false,
623 : minLines: 1,
624 0 : maxLength: max(1, (isGroup ? GroupMessageLengthMax : P2PMessageLengthMax) - numberOfBytesMoreThanChar),
625 : autocorrect: true,
626 0 : buildCounter: (context, {currentLength = 0, isFocused = true, maxLength}) {
627 0 : return Text("$currentLength/$maxLength", style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle));
628 : },
629 : maxLengthEnforcement: MaxLengthEnforcement.enforced,
630 : maxLines: 3,
631 0 : onFieldSubmitted: _sendMessage,
632 0 : style: Provider.of<Settings>(context).scaleFonts(defaultMessageTextStyle).copyWith(
633 : fontWeight: FontWeight.w500,
634 : ),
635 : enabled: true, // always allow editing...
636 :
637 0 : onChanged: (String x) {
638 0 : setState(() {
639 : // we need to force a rerender here to update the max length count
640 : });
641 : },
642 0 : decoration: InputDecoration(
643 0 : hintText: AppLocalizations.of(context)!.placeholderEnterMessage,
644 : hintStyle:
645 0 : Provider.of<Settings>(context).scaleFonts(defaultMessageTextStyle).copyWith(color: Provider.of<Settings>(context).theme.sendHintTextColor, fontWeight: FontWeight.bold),
646 : enabledBorder: InputBorder.none,
647 : focusedBorder: InputBorder.none,
648 : enabled: true,
649 0 : suffixIcon: ElevatedButton(
650 0 : key: Key("btnSend"),
651 0 : style: ElevatedButton.styleFrom(padding: EdgeInsets.all(0.0), shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(45.0))),
652 0 : child: Tooltip(
653 : message: cannotSend
654 0 : ? (isGroup ? AppLocalizations.of(context)!.serverNotSynced : AppLocalizations.of(context)!.peerOfflineMessage)
655 0 : : (isGroup && Provider.of<ContactInfoState>(context, listen: false).antispamTickets == 0)
656 0 : ? AppLocalizations.of(context)!.acquiringTicketsFromServer
657 0 : : AppLocalizations.of(context)!.sendMessage,
658 0 : child: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.defaultButtonTextColor)),
659 0 : onPressed: cannotSend || (isGroup && Provider.of<ContactInfoState>(context, listen: false).antispamTickets == 0) ? null : _sendMessage,
660 : ))),
661 : )));
662 :
663 : var textEditChildren;
664 : if (showToolbar) {
665 0 : textEditChildren = [formattingToolbar, textField];
666 : } else {
667 0 : textEditChildren = [textField];
668 : }
669 :
670 : var composeBox =
671 0 : Container(color: Provider.of<Settings>(context).theme.backgroundMainColor, padding: EdgeInsets.all(2), margin: EdgeInsets.all(2), height: 164, child: Column(children: textEditChildren));
672 :
673 : var children;
674 0 : Widget invite = Container();
675 0 : if (Provider.of<ContactInfoState>(context).messageDraft.getInviteHandle() != null) {
676 0 : invite = FutureBuilder(
677 0 : future: Future.value(Provider.of<ContactInfoState>(context).messageDraft.getInviteHandle()),
678 0 : builder: (context, snapshot) {
679 0 : if (snapshot.hasData) {
680 0 : var contactInvite = snapshot.data! as int;
681 0 : var contact = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(contactInvite);
682 0 : return Container(
683 0 : margin: EdgeInsets.all(5),
684 0 : padding: EdgeInsets.all(5),
685 0 : color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
686 0 : child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
687 0 : Stack(children: [
688 0 : Container(
689 0 : margin: EdgeInsets.all(5),
690 0 : padding: EdgeInsets.all(5),
691 : clipBehavior: Clip.antiAlias,
692 0 : decoration: BoxDecoration(color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor),
693 : height: 75,
694 0 : child: Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [
695 0 : Padding(padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), child: Icon(CwtchIcons.send_invite, size: 32)),
696 0 : Flexible(
697 0 : child: DefaultTextStyle(
698 : textWidthBasis: TextWidthBasis.parent,
699 0 : child: senderInviteChrome("", contact!.nickname),
700 0 : style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle),
701 : overflow: TextOverflow.fade,
702 : ))
703 : ])),
704 0 : Align(
705 : alignment: Alignment.topRight,
706 0 : child: IconButton(
707 0 : icon: Icon(Icons.highlight_remove),
708 0 : splashRadius: Material.defaultSplashRadius / 2,
709 0 : color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
710 0 : tooltip: AppLocalizations.of(context)!.tooltipRemoveThisQuotedMessage,
711 0 : onPressed: () {
712 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearInvite();
713 0 : setState(() {});
714 : },
715 : )),
716 : ]),
717 : ]));
718 : }
719 0 : return Container();
720 : });
721 : }
722 :
723 0 : if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<ContactInfoState>(context).messageDraft.getQuotedMessage() != null) {
724 0 : var quoted = FutureBuilder(
725 0 : future: messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!,
726 0 : ById(Provider.of<ContactInfoState>(context).messageDraft.getQuotedMessage()!.index)),
727 0 : builder: (context, snapshot) {
728 0 : if (snapshot.hasData) {
729 0 : var message = snapshot.data! as Message;
730 0 : var qTextColor = message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
731 0 : ? Provider.of<Settings>(context).theme.messageFromOtherTextColor
732 0 : : Provider.of<Settings>(context).theme.messageFromMeTextColor;
733 0 : return Container(
734 0 : margin: EdgeInsets.all(5),
735 0 : padding: EdgeInsets.all(5),
736 0 : color: message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
737 0 : ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor
738 0 : : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
739 0 : child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
740 0 : Stack(children: [
741 0 : Container(
742 0 : margin: EdgeInsets.all(5),
743 0 : padding: EdgeInsets.all(5),
744 : clipBehavior: Clip.antiAlias,
745 0 : decoration: BoxDecoration(
746 0 : color: message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
747 0 : ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor
748 0 : : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
749 : ),
750 : height: 75,
751 0 : child: Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [
752 0 : Padding(padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), child: Icon(Icons.reply, size: 32, color: qTextColor)),
753 0 : Flexible(
754 0 : child: DefaultTextStyle(
755 : textWidthBasis: TextWidthBasis.parent,
756 0 : child: message.getPreviewWidget(context),
757 0 : style: TextStyle(color: qTextColor),
758 : overflow: TextOverflow.fade,
759 : ))
760 : ])),
761 0 : Align(
762 : alignment: Alignment.topRight,
763 0 : child: IconButton(
764 0 : icon: Icon(Icons.highlight_remove),
765 0 : splashRadius: Material.defaultSplashRadius / 2,
766 0 : color: message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
767 0 : ? Provider.of<Settings>(context).theme.messageFromOtherTextColor
768 0 : : Provider.of<Settings>(context).theme.messageFromMeTextColor,
769 0 : tooltip: AppLocalizations.of(context)!.tooltipRemoveThisQuotedMessage,
770 0 : onPressed: () {
771 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearQuotedReference();
772 0 : setState(() {});
773 : },
774 : )),
775 : ]),
776 : ]));
777 : } else {
778 0 : return MessageLoadingBubble();
779 : }
780 : },
781 : );
782 :
783 0 : children = [invite, quoted, composeBox];
784 : } else {
785 0 : children = [invite, composeBox];
786 : }
787 :
788 0 : return Container(
789 0 : color: Provider.of<Settings>(context).theme.backgroundMainColor, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: children));
790 : }
791 :
792 : // Send the message if enter is pressed without the shift key...
793 0 : void handleKeyPress(KeyEvent event) {
794 0 : var key = event.logicalKey;
795 0 : if ((event.logicalKey == LogicalKeyboardKey.enter && !HardwareKeyboard.instance.isShiftPressed) || event.logicalKey == LogicalKeyboardKey.numpadEnter && HardwareKeyboard.instance.isShiftPressed) {
796 : // Don't send when inserting a new line that is not at the end of the message
797 0 : if (Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose.selection.baseOffset !=
798 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose.text.length) {
799 : return;
800 : }
801 0 : _sendMessage();
802 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearDraft();
803 : }
804 : }
805 :
806 : // explicitly passing BuildContext ctx here is important, change at risk to own health
807 : // otherwise some Providers will become inaccessible to subwidgets...?
808 : // https://stackoverflow.com/a/63818697
809 0 : void _modalSendInvitation(BuildContext ctx) {
810 0 : showModalBottomSheet<void>(
811 : context: ctx,
812 0 : builder: (BuildContext bcontext) {
813 0 : return Container(
814 : height: 200, // bespoke value courtesy of the [TextField] docs
815 0 : child: Center(
816 0 : child: Padding(
817 0 : padding: EdgeInsets.all(10.0),
818 0 : child: Column(
819 : mainAxisAlignment: MainAxisAlignment.center,
820 : mainAxisSize: MainAxisSize.min,
821 : crossAxisAlignment: CrossAxisAlignment.stretch,
822 0 : children: <Widget>[
823 0 : Text(AppLocalizations.of(bcontext)!.invitationLabel),
824 0 : SizedBox(
825 : height: 20,
826 : ),
827 0 : ChangeNotifierProvider.value(
828 0 : value: Provider.of<ProfileInfoState>(ctx, listen: false),
829 0 : child: DropdownContacts(filter: (contact) {
830 0 : return contact.onion != Provider.of<ContactInfoState>(ctx).onion;
831 0 : }, onChanged: (newVal) {
832 0 : setState(() {
833 0 : this.selectedContact = Provider.of<ProfileInfoState>(ctx, listen: false).contactList.findContact(newVal)!.identifier;
834 : });
835 : })),
836 0 : SizedBox(
837 : height: 20,
838 : ),
839 0 : ElevatedButton(
840 0 : child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn),
841 0 : onPressed: () {
842 0 : if (this.selectedContact != -1) {
843 0 : Provider.of<ContactInfoState>(context, listen: false).messageDraft.attachInvite(this.selectedContact);
844 : }
845 0 : Navigator.pop(bcontext);
846 0 : setState(() {});
847 : },
848 : ),
849 : ],
850 : )),
851 : ));
852 : });
853 : }
854 :
855 0 : void _confirmFileSend(BuildContext ctx, String path) async {
856 0 : showModalBottomSheet<void>(
857 : context: ctx,
858 0 : builder: (BuildContext bcontext) {
859 : var showPreview = false;
860 0 : if (Provider.of<Settings>(context, listen: false).shouldPreview(path)) {
861 : showPreview = true;
862 0 : if (imagePreview == null) {
863 0 : imagePreview = new File(path);
864 : }
865 : }
866 0 : return Container(
867 : height: 300, // bespoke value courtesy of the [TextField] docs
868 0 : child: Center(
869 0 : child: Padding(
870 0 : padding: EdgeInsets.all(10.0),
871 0 : child: Column(
872 : mainAxisAlignment: MainAxisAlignment.center,
873 : mainAxisSize: MainAxisSize.min,
874 0 : children: <Widget>[
875 0 : Text(AppLocalizations.of(context)!.msgConfirmSend + " $path?"),
876 0 : SizedBox(
877 : height: 20,
878 : ),
879 0 : Visibility(
880 : visible: showPreview,
881 : child: showPreview
882 0 : ? Image.file(
883 0 : imagePreview!,
884 : cacheHeight: 150, // limit the amount of space the image can decode too, we keep this high-ish to allow quality previews...
885 : filterQuality: FilterQuality.medium,
886 : fit: BoxFit.fill,
887 : alignment: Alignment.center,
888 : height: 150,
889 : isAntiAlias: false,
890 0 : errorBuilder: (context, error, stackTrace) {
891 0 : return MalformedBubble();
892 : },
893 : )
894 0 : : Container()),
895 0 : Visibility(
896 : visible: showPreview,
897 0 : child: SizedBox(
898 : height: 10,
899 : )),
900 0 : Row(mainAxisAlignment: MainAxisAlignment.center, children: [
901 0 : OutlinedButton(
902 0 : child: Text(AppLocalizations.of(context)!.cancel, semanticsLabel: AppLocalizations.of(context)!.cancel),
903 0 : onPressed: () {
904 0 : Navigator.pop(bcontext);
905 : },
906 : ),
907 0 : SizedBox(
908 : width: 20,
909 : ),
910 0 : FilledButton(
911 0 : child: Text(AppLocalizations.of(context)!.btnSendFile, semanticsLabel: AppLocalizations.of(context)!.btnSendFile),
912 0 : onPressed: () {
913 0 : _sendFile(path);
914 0 : Navigator.pop(bcontext);
915 : },
916 : ),
917 : ]),
918 : ],
919 : )),
920 : ));
921 : });
922 : }
923 : }
924 :
925 0 : void _summarizeConversation(BuildContext context, ProfileInfoState profile, Settings settings) async {
926 0 : showModalBottomSheet<void>(
927 0 : builder: (
928 : BuildContext bcontext,
929 : ) {
930 0 : return StatefulBuilder(builder: (BuildContext scontext, StateSetter setState /*You can rename this!*/) {
931 0 : if (scontext.mounted) {
932 0 : new Timer.periodic(Duration(seconds: 1), (Timer t) {
933 0 : if (scontext.mounted) {
934 0 : setState(() {});
935 : }
936 : });
937 : }
938 :
939 0 : var bubble = StaticMessageBubble(
940 : profile,
941 : settings,
942 0 : MessageMetadata(profile.onion, Provider.of<ContactInfoState>(context).identifier, 1, DateTime.now(), "blodeuwedd", null, null, null, true, false, false, ""),
943 0 : Row(children: [
944 0 : Provider.of<ContactInfoState>(context).summary == ""
945 0 : ? Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
946 0 : CircularProgressIndicator(color: settings.theme.defaultButtonActiveColor),
947 0 : Padding(padding: EdgeInsets.all(5.0), child: Text(AppLocalizations.of(context)!.blodeuweddProcessing))
948 : ])
949 0 : : Flexible(child: Text(Provider.of<ContactInfoState>(context).summary))
950 : ]));
951 :
952 0 : var image = Padding(
953 0 : padding: EdgeInsets.all(4.0),
954 0 : child: ProfileImage(
955 : imagePath: "assets/blodeuwedd.png",
956 : diameter: 48.0,
957 0 : border: settings.theme.portraitOnlineBorderColor,
958 : badgeTextColor: Colors.red,
959 : badgeColor: Colors.red,
960 : ));
961 :
962 0 : return Container(
963 : height: 300, // bespoke value courtesy of the [TextField] docs
964 0 : child: Container(
965 : alignment: Alignment.center,
966 0 : child: Padding(
967 0 : padding: EdgeInsets.all(10.0),
968 0 : child: Padding(
969 0 : padding: EdgeInsets.all(10.0),
970 0 : child: Row(
971 : mainAxisSize: MainAxisSize.min,
972 0 : children: [image, Flexible(child: bubble)],
973 : )))));
974 : });
975 : },
976 : context: context);
977 : }
|