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