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