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