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