Line data Source code
1 : import 'dart:io';
2 :
3 : import 'package:cwtch/cwtch/cwtch.dart';
4 : import 'package:cwtch/cwtch_icons_icons.dart';
5 : import 'package:cwtch/models/appstate.dart';
6 : import 'package:cwtch/models/contact.dart';
7 : import 'package:cwtch/models/contactlist.dart';
8 : import 'package:cwtch/models/profile.dart';
9 : import 'package:cwtch/models/profilelist.dart';
10 : import 'package:cwtch/models/search.dart';
11 : import 'package:cwtch/views/globalsettingsview.dart';
12 : import 'package:cwtch/views/profileserversview.dart';
13 : import 'package:flutter/material.dart';
14 : import 'package:cwtch/widgets/contactrow.dart';
15 : import 'package:cwtch/widgets/profileimage.dart';
16 : import 'package:cwtch/widgets/textfield.dart';
17 : import 'package:flutter/services.dart';
18 : import 'package:provider/provider.dart';
19 : import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
20 : import '../config.dart';
21 : import '../main.dart';
22 : import '../models/redaction.dart';
23 : import '../settings.dart';
24 : import '../themes/opaque.dart';
25 : import 'addcontactview.dart';
26 : import 'package:cwtch/l10n/app_localizations.dart';
27 : import 'package:qr_flutter/qr_flutter.dart';
28 :
29 : import 'addeditprofileview.dart';
30 : import 'messageview.dart';
31 :
32 : enum ShareMenu { copyCode, qrcode }
33 :
34 : enum ProfileStatusMenu { available, away, busy, appearOnline, appearOffline, allowUnknownContacts, blockUnknownContacts, enableProfile, disableProfile, editProfile, reconnectProfile }
35 :
36 : class ContactsView extends StatefulWidget {
37 0 : const ContactsView({Key? key}) : super(key: key);
38 :
39 0 : @override
40 0 : _ContactsViewState createState() => _ContactsViewState();
41 : }
42 :
43 : // selectConversation can be called from anywhere to set the active conversation
44 0 : void selectConversation(BuildContext context, int handle, int? messageIndex) {
45 : int? index;
46 :
47 : if (messageIndex != null) {
48 : // this message is loaded
49 0 : Provider.of<SearchState>(context, listen: false).selectedSearchMessage = messageIndex;
50 0 : Provider.of<AppState>(context, listen: false).initialScrollIndex = messageIndex;
51 0 : EnvironmentConfig.debugLog("Looked up index $messageIndex");
52 : }
53 :
54 0 : if (handle == Provider.of<AppState>(context, listen: false).selectedConversation) {
55 : if (messageIndex != null) {
56 0 : Provider.of<ContactInfoState>(context, listen: false).messageScrollController.scrollTo(index: messageIndex, duration: Duration(milliseconds: 100));
57 : }
58 : return;
59 : }
60 :
61 : // requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
62 0 : var unread = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
63 0 : var previouslySelected = Provider.of<AppState>(context, listen: false).selectedConversation;
64 : if (previouslySelected != null) {
65 0 : Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(previouslySelected)!.unselected();
66 : }
67 0 : Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.selected();
68 :
69 : // triggers update in Double/TripleColumnView
70 0 : Provider.of<ContactInfoState>(context, listen: false).hoveredIndex = -1;
71 0 : Provider.of<AppState>(context, listen: false).selectedConversation = handle;
72 :
73 : // if in singlepane mode, push to the stack
74 0 : var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
75 0 : if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle);
76 :
77 : // Set last message seen time in backend
78 0 : Provider.of<FlwtchState>(
79 : context,
80 : listen: false,
81 0 : ).cwtch.SetConversationAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, handle, LastMessageSeenTimeKey, DateTime.now().toUtc().toIso8601String());
82 : }
83 :
84 0 : void _pushMessageView(BuildContext context, int handle) {
85 0 : var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
86 :
87 0 : Navigator.of(context).push(
88 0 : PageRouteBuilder(
89 0 : settings: RouteSettings(name: "messages"),
90 0 : pageBuilder: (builderContext, a1, a2) {
91 0 : var profile = Provider.of<FlwtchState>(builderContext).profs.getProfile(profileOnion)!;
92 0 : return MultiProvider(
93 0 : providers: [
94 0 : ChangeNotifierProvider.value(value: profile),
95 0 : ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)!),
96 0 : ChangeNotifierProvider.value(value: profile.searchState),
97 : ],
98 0 : builder: (context, child) => MessageView(),
99 : );
100 : },
101 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
102 0 : transitionDuration: Duration(milliseconds: 200),
103 : ),
104 : );
105 : }
106 :
107 : class _ContactsViewState extends State<ContactsView> {
108 : late TextEditingController ctrlrFilter;
109 : final scaffoldKey = GlobalKey<ScaffoldMessengerState>();
110 :
111 0 : @override
112 : void initState() {
113 0 : super.initState();
114 0 : ctrlrFilter = new TextEditingController(text: Provider.of<SearchState>(context, listen: false).filter);
115 : }
116 :
117 0 : @override
118 : Widget build(BuildContext context) {
119 0 : var enabled = Provider.of<ProfileInfoState>(context, listen: false).enabled;
120 0 : var appearOffline = Provider.of<ProfileInfoState>(context, listen: false).appearOffline;
121 0 : var settings = Provider.of<Settings>(context, listen: false);
122 0 : if (!Provider.of<SearchState>(context).searchActive) {
123 0 : ctrlrFilter.text = "";
124 : }
125 0 : return ScaffoldMessenger(
126 0 : key: scaffoldKey,
127 0 : child: Scaffold(
128 0 : backgroundColor: Provider.of<Settings>(context).theme.backgroundMainColor,
129 : endDrawerEnableOpenDragGesture: false,
130 : drawerEnableOpenDragGesture: false,
131 0 : appBar: AppBar(
132 0 : leading: Stack(
133 0 : children: [
134 0 : Align(
135 : alignment: Alignment.center,
136 0 : child: IconButton(
137 0 : icon: Icon(Icons.arrow_back),
138 0 : tooltip: MaterialLocalizations.of(context).backButtonTooltip,
139 0 : onPressed: () {
140 0 : if (Provider.of<SearchState>(context, listen: false).searchActive) {
141 0 : Provider.of<SearchState>(context, listen: false).clearSearch();
142 : } else {
143 0 : Provider.of<ProfileInfoState>(context, listen: false).recountUnread();
144 0 : Provider.of<AppState>(context, listen: false).selectedProfile = "";
145 0 : Navigator.of(context).pop();
146 : }
147 : },
148 : ),
149 : ),
150 0 : Positioned(
151 : bottom: 5.0,
152 : right: 5.0,
153 0 : child: StreamBuilder<bool>(
154 0 : stream: Provider.of<AppState>(context).getUnreadProfileNotifyStream(),
155 0 : builder: (BuildContext context, AsyncSnapshot<bool> unreadCountSnapshot) {
156 0 : int unreadCount = Provider.of<ProfileListState>(context).generateUnreadCount(Provider.of<AppState>(context).selectedProfile ?? "");
157 :
158 0 : return Visibility(
159 0 : visible: unreadCount > 0,
160 0 : child: CircleAvatar(
161 : radius: 10.0,
162 0 : backgroundColor: Provider.of<Settings>(context).theme.portraitProfileBadgeColor,
163 0 : child: Text(unreadCount > 99 ? "99+" : unreadCount.toString(), style: TextStyle(color: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor, fontSize: 8.0)),
164 : ),
165 : );
166 : },
167 : ),
168 : ),
169 : ],
170 : ),
171 0 : title: Row(
172 0 : children: [
173 0 : PopupMenuButton<ProfileStatusMenu>(
174 0 : icon: ProfileImage(
175 0 : imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment)
176 0 : ? Provider.of<ProfileInfoState>(context).imagePath
177 0 : : Provider.of<ProfileInfoState>(context).defaultImagePath,
178 : diameter: 42,
179 0 : border: Provider.of<ProfileInfoState>(context).getBorderColor(Provider.of<Settings>(context).theme),
180 : badgeTextColor: Colors.red,
181 : badgeColor: Colors.red,
182 : ),
183 : iconSize: 42,
184 0 : tooltip: AppLocalizations.of(context)!.availabilityStatusTooltip,
185 0 : splashRadius: Material.defaultSplashRadius / 2,
186 0 : onSelected: (ProfileStatusMenu item) {
187 0 : String onion = Provider.of<ProfileInfoState>(context, listen: false).onion;
188 : switch (item) {
189 0 : case ProfileStatusMenu.available:
190 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.profile-status", "available");
191 : break;
192 0 : case ProfileStatusMenu.away:
193 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.profile-status", "away");
194 : break;
195 0 : case ProfileStatusMenu.busy:
196 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.profile-status", "busy");
197 : break;
198 0 : case ProfileStatusMenu.reconnectProfile:
199 0 : Provider.of<ProfileInfoState>(context, listen: false).deactivatePeerEngine(context);
200 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ConfigureConnections(onion, true, true, true);
201 : break;
202 0 : case ProfileStatusMenu.appearOffline:
203 0 : Provider.of<ProfileInfoState>(context, listen: false).appearOffline = true;
204 0 : Provider.of<ProfileInfoState>(context, listen: false).deactivatePeerEngine(context);
205 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.appear-offline", "true");
206 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ConfigureConnections(onion, false, false, false);
207 : break;
208 0 : case ProfileStatusMenu.editProfile:
209 0 : Navigator.of(context).push(
210 0 : PageRouteBuilder(
211 0 : pageBuilder: (bcontext, a1, a2) {
212 0 : return MultiProvider(
213 0 : providers: [ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context, listen: false))],
214 0 : builder: (context, widget) => AddEditProfileView(key: Key('addprofile')),
215 : );
216 : },
217 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
218 0 : transitionDuration: Duration(milliseconds: 200),
219 : ),
220 : );
221 : break;
222 0 : case ProfileStatusMenu.appearOnline:
223 0 : Provider.of<ProfileInfoState>(context, listen: false).appearOffline = false;
224 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.appear-offline", "false");
225 : // we only need to toggle all connections on here..
226 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ConfigureConnections(onion, true, true, true);
227 : break;
228 0 : case ProfileStatusMenu.allowUnknownContacts:
229 0 : settings.blockUnknownConnections = false;
230 0 : saveSettings(context);
231 : break;
232 0 : case ProfileStatusMenu.blockUnknownContacts:
233 0 : settings.blockUnknownConnections = true;
234 0 : saveSettings(context);
235 : break;
236 0 : case ProfileStatusMenu.enableProfile:
237 0 : Provider.of<ProfileInfoState>(context, listen: false).enabled = true;
238 0 : if (Provider.of<ProfileInfoState>(context, listen: false).appearOffline) {
239 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ConfigureConnections(onion, false, false, false);
240 : } else {
241 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ConfigureConnections(onion, true, true, true);
242 : }
243 : break;
244 0 : case ProfileStatusMenu.disableProfile:
245 0 : Provider.of<ProfileInfoState>(context, listen: false).enabled = false;
246 0 : Provider.of<ProfileInfoState>(context, listen: false).deactivatePeerEngine(context);
247 : break;
248 : }
249 : },
250 0 : itemBuilder: (BuildContext context) => <PopupMenuEntry<ProfileStatusMenu>>[
251 0 : PopupMenuItem<ProfileStatusMenu>(
252 : value: ProfileStatusMenu.available,
253 : enabled: enabled,
254 0 : child: Row(
255 0 : children: [
256 0 : Icon(CwtchIcons.account_circle_24px, color: Colors.white),
257 0 : Expanded(
258 0 : child: Text(
259 0 : AppLocalizations.of(context)!.availabilityStatusAvailable,
260 : textAlign: TextAlign.right,
261 0 : style: Provider.of<Settings>(context, listen: false).scaleFonts(defaultTextButtonStyle),
262 : ),
263 : ),
264 : ],
265 : ),
266 : ),
267 0 : PopupMenuItem<ProfileStatusMenu>(
268 : value: ProfileStatusMenu.away,
269 : enabled: enabled,
270 0 : child: Row(
271 0 : children: [
272 0 : Icon(CwtchIcons.account_circle_24px, color: Colors.yellowAccent),
273 0 : Expanded(
274 0 : child: Text(
275 0 : AppLocalizations.of(context)!.availabilityStatusAway,
276 : textAlign: TextAlign.right,
277 0 : style: Provider.of<Settings>(context, listen: false).scaleFonts(defaultTextButtonStyle),
278 : ),
279 : ),
280 : ],
281 : ),
282 : ),
283 0 : PopupMenuItem<ProfileStatusMenu>(
284 : value: ProfileStatusMenu.busy,
285 : enabled: enabled,
286 0 : child: Row(
287 0 : children: [
288 0 : Icon(CwtchIcons.account_circle_24px, color: Colors.redAccent),
289 0 : Expanded(
290 0 : child: Text(
291 0 : AppLocalizations.of(context)!.availabilityStatusBusy,
292 : textAlign: TextAlign.right,
293 0 : style: Provider.of<Settings>(context, listen: false).scaleFonts(defaultTextButtonStyle),
294 : ),
295 : ),
296 : ],
297 : ),
298 : ),
299 0 : PopupMenuDivider(),
300 0 : PopupMenuItem<ProfileStatusMenu>(
301 : value: ProfileStatusMenu.appearOffline,
302 : enabled: enabled && !appearOffline,
303 0 : child: Row(
304 0 : children: [
305 0 : Icon(CwtchIcons.disconnect_from_contact),
306 0 : Expanded(
307 0 : child: Text(
308 0 : AppLocalizations.of(context)!.profileAppearOffline,
309 : textAlign: TextAlign.right,
310 0 : style: Provider.of<Settings>(context, listen: false).scaleFonts(defaultTextButtonStyle),
311 : ),
312 : ),
313 : ],
314 : ),
315 : ),
316 0 : PopupMenuItem<ProfileStatusMenu>(
317 : value: ProfileStatusMenu.appearOnline,
318 : enabled: enabled && appearOffline,
319 0 : child: Row(
320 0 : children: [
321 0 : Icon(CwtchIcons.disconnect_from_contact),
322 0 : Expanded(
323 0 : child: Text(
324 0 : AppLocalizations.of(context)!.profileAppearOnline,
325 : textAlign: TextAlign.right,
326 0 : style: Provider.of<Settings>(context, listen: false).scaleFonts(defaultTextButtonStyle),
327 : ),
328 : ),
329 : ],
330 : ),
331 : ),
332 0 : PopupMenuDivider(),
333 0 : PopupMenuItem<ProfileStatusMenu>(
334 0 : value: !settings.blockUnknownConnections ? ProfileStatusMenu.blockUnknownContacts : ProfileStatusMenu.allowUnknownContacts,
335 0 : child: Row(
336 0 : children: [
337 0 : Icon(CwtchIcons.block_unknown, color: settings.theme.mainTextColor),
338 0 : Expanded(
339 0 : child: Text(
340 0 : (settings.blockUnknownConnections ? AppLocalizations.of(context)!.profileAllowUnknownContacts : AppLocalizations.of(context)!.profileBlockUnknownContacts),
341 : textAlign: TextAlign.right,
342 0 : style: Provider.of<Settings>(context, listen: false).scaleFonts(defaultTextButtonStyle),
343 : ),
344 : ),
345 : ],
346 : ),
347 : ),
348 0 : PopupMenuDivider(),
349 0 : PopupMenuItem<ProfileStatusMenu>(
350 : value: enabled ? ProfileStatusMenu.disableProfile : ProfileStatusMenu.enableProfile,
351 0 : child: Row(
352 0 : children: [
353 0 : Icon(CwtchIcons.favorite_24px, color: settings.theme.mainTextColor),
354 0 : Expanded(
355 0 : child: Text(
356 0 : (enabled ? AppLocalizations.of(context)!.profileDisableProfile : AppLocalizations.of(context)!.profileEnableProfile),
357 : textAlign: TextAlign.right,
358 0 : style: Provider.of<Settings>(context, listen: false).scaleFonts(defaultTextButtonStyle),
359 : ),
360 : ),
361 : ],
362 : ),
363 : ),
364 0 : PopupMenuItem<ProfileStatusMenu>(
365 : value: ProfileStatusMenu.reconnectProfile,
366 : enabled: enabled && !appearOffline,
367 0 : child: Row(
368 0 : children: [
369 0 : Icon(Icons.cached),
370 0 : Expanded(
371 0 : child: Text(
372 0 : AppLocalizations.of(context)!.restartProfile,
373 : textAlign: TextAlign.right,
374 0 : style: Provider.of<Settings>(context, listen: false).scaleFonts(defaultTextButtonStyle),
375 : ),
376 : ),
377 : ],
378 : ),
379 : ),
380 0 : PopupMenuItem<ProfileStatusMenu>(
381 : value: ProfileStatusMenu.editProfile,
382 : enabled: true,
383 0 : child: Row(
384 0 : children: [
385 0 : Icon(CwtchIcons.edit_24px, color: settings.theme.mainTextColor),
386 0 : Expanded(
387 0 : child: Text(AppLocalizations.of(context)!.editProfile, textAlign: TextAlign.right, style: Provider.of<Settings>(context, listen: false).scaleFonts(defaultTextButtonStyle)),
388 : ),
389 : ],
390 : ),
391 : ),
392 : ],
393 : ),
394 0 : SizedBox(width: 10),
395 0 : Expanded(
396 0 : child: Text(
397 : "%1 ยป %2"
398 0 : .replaceAll("%1", redactedNick(context, Provider.of<ProfileInfoState>(context).onion, Provider.of<ProfileInfoState>(context).nickname))
399 0 : .replaceAll("%2", AppLocalizations.of(context)!.titleManageContacts),
400 : overflow: TextOverflow.ellipsis,
401 0 : style: TextStyle(
402 0 : color: Provider.of<Settings>(context).current().mainTextColor,
403 : fontFamily: "Inter",
404 : fontWeight: FontWeight.bold,
405 0 : fontSize: 14.0 * Provider.of<Settings>(context).fontScaling,
406 : ),
407 : ),
408 : ),
409 : ],
410 : ),
411 0 : actions: getActions(context),
412 : ),
413 0 : floatingActionButton: FloatingActionButton(
414 0 : onPressed: _modalAddImportChoice,
415 0 : tooltip: AppLocalizations.of(context)!.tooltipAddContact,
416 0 : child: Icon(CwtchIcons.person_add_alt_1_24px, color: Provider.of<Settings>(context).theme.defaultButtonTextColor),
417 : ),
418 0 : body: Provider.of<SearchState>(context).searchOrFiltered ? _buildFilterable() : _buildContactList(),
419 : ),
420 : );
421 : }
422 :
423 0 : List<Widget> getActions(context) {
424 0 : var actions = List<Widget>.empty(growable: true);
425 0 : if (Provider.of<Settings>(context).blockUnknownConnections) {
426 0 : actions.add(Tooltip(message: AppLocalizations.of(context)!.blockUnknownConnectionsEnabledDescription, child: Icon(CwtchIcons.block_unknown)));
427 : }
428 :
429 0 : if (Provider.of<Settings>(context, listen: false).isExperimentEnabled(QRCodeExperiment)) {
430 0 : actions.add(
431 0 : PopupMenuButton<ShareMenu>(
432 0 : icon: Icon(CwtchIcons.copy_address),
433 0 : tooltip: AppLocalizations.of(context)!.shareProfileMenuTooltop,
434 0 : splashRadius: Material.defaultSplashRadius / 2,
435 0 : onSelected: (ShareMenu item) {
436 : switch (item) {
437 0 : case ShareMenu.copyCode:
438 : {
439 0 : Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
440 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedToClipboardNotification));
441 0 : scaffoldKey.currentState?.showSnackBar(snackBar);
442 : }
443 : break;
444 0 : case ShareMenu.qrcode:
445 : {
446 0 : _showQRCode("cwtch:" + Provider.of<ProfileInfoState>(context, listen: false).onion);
447 : }
448 : break;
449 : }
450 : },
451 0 : itemBuilder: (BuildContext context) => <PopupMenuEntry<ShareMenu>>[
452 0 : PopupMenuItem<ShareMenu>(
453 : value: ShareMenu.copyCode,
454 0 : child: Text(AppLocalizations.of(context)!.copyAddress, style: Provider.of<Settings>(context, listen: false).scaleFonts(defaultTextButtonStyle)),
455 : ),
456 0 : PopupMenuItem<ShareMenu>(
457 : value: ShareMenu.qrcode,
458 0 : child: Text(AppLocalizations.of(context)!.shareMenuQRCode, style: Provider.of<Settings>(context, listen: false).scaleFonts(defaultTextButtonStyle)),
459 : ),
460 : ],
461 : ),
462 : );
463 : } else {
464 0 : actions.add(
465 0 : IconButton(
466 0 : icon: Icon(CwtchIcons.copy_address),
467 0 : tooltip: AppLocalizations.of(context)!.copyAddress,
468 0 : splashRadius: Material.defaultSplashRadius / 2,
469 0 : onPressed: () {
470 0 : Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
471 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedToClipboardNotification));
472 0 : scaffoldKey.currentState?.showSnackBar(snackBar);
473 : },
474 : ),
475 : );
476 : }
477 :
478 : // Manage known Servers
479 0 : if (Provider.of<Settings>(context, listen: false).isExperimentEnabled(TapirGroupsExperiment) || Provider.of<Settings>(context, listen: false).isExperimentEnabled(ServerManagementExperiment)) {
480 0 : actions.add(
481 0 : IconButton(
482 0 : icon: Icon(CwtchIcons.dns_24px),
483 0 : tooltip: AppLocalizations.of(context)!.manageKnownServersButton,
484 0 : splashRadius: Material.defaultSplashRadius / 2,
485 0 : onPressed: () {
486 0 : _pushServers();
487 : },
488 : ),
489 : );
490 : }
491 :
492 : // Search contacts
493 0 : actions.add(
494 0 : IconButton(
495 : // need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset
496 0 : icon: Icon(Provider.of<SearchState>(context).searchOrFiltered ? Icons.search_off : Icons.search),
497 0 : splashRadius: Material.defaultSplashRadius / 2,
498 0 : onPressed: () {
499 0 : Provider.of<SearchState>(context, listen: false).toggleSearch();
500 : },
501 : ),
502 : );
503 : return actions;
504 : }
505 :
506 0 : Widget _buildFilterable() {
507 0 : Widget txtfield = CwtchTextField(
508 0 : controller: ctrlrFilter,
509 0 : hintText: AppLocalizations.of(context)!.search,
510 : autofocus: true,
511 0 : onChanged: (newVal) {
512 0 : String profileHandle = Provider.of<ProfileInfoState>(context, listen: false).onion;
513 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SearchConversations(profileHandle, newVal).then((value) {
514 0 : Provider.of<SearchState>(context, listen: false).newSearch(value);
515 : });
516 0 : Provider.of<SearchState>(context, listen: false).filter = newVal;
517 : },
518 : );
519 0 : return Column(
520 0 : children: [
521 0 : Padding(padding: EdgeInsets.all(8), child: txtfield),
522 0 : Expanded(child: _buildContactList()),
523 : ],
524 : );
525 : }
526 :
527 0 : Widget _buildContactList() {
528 0 : var tilesFilteredContacts = Provider.of<ProfileInfoState>(context).filteredList().map((ContactInfoState contact) {
529 0 : return MultiProvider(
530 0 : providers: [
531 0 : ChangeNotifierProvider.value(value: contact),
532 0 : ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).serverList),
533 : ],
534 0 : builder: (context, child) => ContactRow(),
535 : );
536 : });
537 :
538 0 : var initialScroll = Provider.of<ProfileInfoState>(context, listen: false).filteredList().indexWhere((element) => element.identifier == Provider.of<AppState>(context).selectedConversation);
539 0 : if (initialScroll < 0) {
540 : initialScroll = 0;
541 : }
542 :
543 0 : if (Provider.of<SearchState>(context).searchActive) {
544 0 : List<SearchResult> searchResults = Provider.of<SearchState>(context).activeSearchResults;
545 0 : var tilesSearchResult = searchResults.map((SearchResult searchResult) {
546 0 : return MultiProvider(
547 0 : providers: [
548 0 : ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).contactList.getContact(searchResult.conversationIdentifier)),
549 0 : ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).serverList),
550 : ],
551 0 : builder: (context, child) => ContactRow(messageIndex: searchResult.messageIndex),
552 : );
553 : });
554 0 : tilesFilteredContacts = tilesFilteredContacts.followedBy(tilesSearchResult);
555 : } else {}
556 :
557 0 : var contactList = ScrollablePositionedList.separated(
558 0 : itemScrollController: Provider.of<ProfileInfoState>(context).contactListScrollController,
559 0 : itemCount: tilesFilteredContacts.length,
560 : initialScrollIndex: initialScroll,
561 : shrinkWrap: true,
562 0 : physics: BouncingScrollPhysics(),
563 0 : semanticChildCount: tilesFilteredContacts.length,
564 0 : itemBuilder: (context, index) {
565 0 : if (tilesFilteredContacts.length > index) {
566 0 : return tilesFilteredContacts.elementAt(index);
567 : }
568 0 : return Container();
569 : },
570 0 : separatorBuilder: (BuildContext context, int index) {
571 0 : return Divider(height: 1);
572 : },
573 : );
574 :
575 : return contactList;
576 : }
577 :
578 0 : void _pushAddContact(bool newGroup) {
579 : // close modal
580 0 : Navigator.popUntil(context, (route) => route.settings.name == "conversations");
581 :
582 0 : Navigator.of(context).push(
583 0 : PageRouteBuilder(
584 0 : settings: RouteSettings(name: "addcontact"),
585 0 : pageBuilder: (builderContext, a1, a2) {
586 0 : return MultiProvider(
587 0 : providers: [ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context))],
588 0 : child: AddContactView(newGroup: newGroup),
589 : );
590 : },
591 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
592 0 : transitionDuration: Duration(milliseconds: 200),
593 : ),
594 : );
595 : }
596 :
597 0 : void _pushServers() {
598 0 : var profileInfoState = Provider.of<ProfileInfoState>(context, listen: false);
599 :
600 0 : Navigator.of(context).push(
601 0 : PageRouteBuilder(
602 0 : settings: RouteSettings(name: "profileremoteservers"),
603 0 : pageBuilder: (bcontext, a1, a2) {
604 0 : return MultiProvider(
605 0 : providers: [
606 0 : ChangeNotifierProvider.value(value: profileInfoState),
607 0 : Provider.value(value: Provider.of<FlwtchState>(context)),
608 : ],
609 0 : child: ProfileServersView(),
610 : );
611 : },
612 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
613 0 : transitionDuration: Duration(milliseconds: 200),
614 : ),
615 : );
616 : }
617 :
618 0 : void _modalAddImportChoice() {
619 0 : bool groupsEnabled = Provider.of<Settings>(context, listen: false).isExperimentEnabled(TapirGroupsExperiment);
620 :
621 0 : showModalBottomSheet<void>(
622 0 : context: context,
623 : isScrollControlled: true,
624 0 : builder: (BuildContext context) {
625 0 : return Padding(
626 0 : padding: MediaQuery.of(context).viewInsets,
627 0 : child: RepaintBoundary(
628 0 : child: Container(
629 0 : height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
630 0 : child: Center(
631 0 : child: Padding(
632 0 : padding: EdgeInsets.all(2.0),
633 0 : child: Column(
634 : mainAxisAlignment: MainAxisAlignment.center,
635 : crossAxisAlignment: CrossAxisAlignment.center,
636 : mainAxisSize: MainAxisSize.max,
637 0 : children: <Widget>[
638 0 : SizedBox(height: 20),
639 0 : Expanded(
640 0 : child: Tooltip(
641 0 : message: AppLocalizations.of(context)!.tooltipAddContact,
642 0 : child: ElevatedButton(
643 0 : style: ElevatedButton.styleFrom(
644 0 : minimumSize: Size.fromWidth(399),
645 0 : maximumSize: Size.fromWidth(400),
646 0 : shape: RoundedRectangleBorder(
647 0 : borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
648 : ),
649 : ),
650 0 : child: Text(
651 0 : AppLocalizations.of(context)!.addContact,
652 0 : semanticsLabel: AppLocalizations.of(context)!.addContact,
653 : textAlign: TextAlign.center,
654 0 : style: TextStyle(fontFamily: "Inter", fontSize: 10.0 * Provider.of<Settings>(context).fontScaling, fontWeight: FontWeight.bold),
655 : ),
656 0 : onPressed: () {
657 0 : _pushAddContact(false);
658 : },
659 : ),
660 : ),
661 : ),
662 0 : SizedBox(height: 20),
663 0 : Expanded(
664 0 : child: Tooltip(
665 0 : message: groupsEnabled ? AppLocalizations.of(context)!.addServerTooltip : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
666 0 : child: ElevatedButton(
667 0 : style: ElevatedButton.styleFrom(
668 0 : minimumSize: Size.fromWidth(399),
669 0 : maximumSize: Size.fromWidth(400),
670 0 : shape: RoundedRectangleBorder(
671 0 : borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
672 : ),
673 : ),
674 0 : child: Text(
675 0 : AppLocalizations.of(context)!.addServerTitle,
676 0 : semanticsLabel: AppLocalizations.of(context)!.addServerTitle,
677 : textAlign: TextAlign.center,
678 0 : style: TextStyle(fontFamily: "Inter", fontSize: 10.0 * Provider.of<Settings>(context).fontScaling, fontWeight: FontWeight.bold),
679 : ),
680 : onPressed: groupsEnabled
681 0 : ? () {
682 0 : _pushAddContact(false);
683 : }
684 : : null,
685 : ),
686 : ),
687 : ),
688 0 : SizedBox(height: 20),
689 0 : Expanded(
690 0 : child: Tooltip(
691 0 : message: groupsEnabled ? AppLocalizations.of(context)!.createGroupTitle : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
692 0 : child: ElevatedButton(
693 0 : style: ElevatedButton.styleFrom(
694 0 : minimumSize: Size.fromWidth(399),
695 0 : maximumSize: Size.fromWidth(400),
696 0 : shape: RoundedRectangleBorder(
697 0 : borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
698 : ),
699 : ),
700 0 : child: Text(
701 0 : AppLocalizations.of(context)!.createGroupTitle,
702 0 : semanticsLabel: AppLocalizations.of(context)!.createGroupTitle,
703 : textAlign: TextAlign.center,
704 0 : style: TextStyle(fontFamily: "Inter", fontSize: 10.0 * Provider.of<Settings>(context).fontScaling, fontWeight: FontWeight.bold),
705 : ),
706 : onPressed: groupsEnabled
707 0 : ? () {
708 0 : _pushAddContact(true);
709 : }
710 : : null,
711 : ),
712 : ),
713 : ),
714 0 : SizedBox(height: 20),
715 : ],
716 : ),
717 : ),
718 : ),
719 : ),
720 : ),
721 : );
722 : },
723 : );
724 : }
725 :
726 0 : void _showQRCode(String profileCode) {
727 0 : showModalBottomSheet<dynamic>(
728 0 : context: context,
729 0 : builder: (BuildContext context) {
730 0 : return Wrap(
731 0 : children: <Widget>[
732 0 : Center(
733 0 : child: QrImageView(
734 : data: profileCode,
735 : version: QrVersions.auto,
736 : size: 400.0,
737 0 : backgroundColor: Provider.of<Settings>(context).theme.backgroundPaneColor,
738 0 : foregroundColor: Provider.of<Settings>(context).theme.mainTextColor,
739 : gapless: false,
740 : ),
741 : ),
742 : ],
743 : );
744 : },
745 : );
746 : }
747 : }
|