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