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