import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:async/async.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/connection_status_header.dart'; import '../../utils/account_bundles.dart'; import '../../utils/stream_extension.dart'; import '../../widgets/matrix.dart'; class ChatListView extends StatelessWidget { final ChatListController controller; const ChatListView(this.controller, {Key key}) : super(key: key); List getBottomBarItems(BuildContext context) { final displayClients = Matrix.of(context).currentBundle; if (displayClients.isEmpty) { displayClients.addAll(Matrix.of(context).widget.clients); controller.resetActiveBundle(); } final items = displayClients.map((client) { return BottomNavigationBarItem( label: client.userID, icon: FutureBuilder( future: client.ownProfile, builder: (context, snapshot) { return InkWell( borderRadius: BorderRadius.circular(32), onTap: () => controller.setActiveClient(client), onLongPress: () => controller.editBundlesForAccount(client.userID), child: Avatar( snapshot.data?.avatarUrl, snapshot.data?.displayName ?? client.userID.localpart, size: 32, ), ); }), ); }).toList(); if (controller.displayBundles && false) { items.insert( 0, BottomNavigationBarItem( label: 'Bundles', icon: PopupMenuButton( icon: Icon( Icons.menu, color: Theme.of(context).textTheme.bodyText1.color, ), onSelected: controller.setActiveBundle, itemBuilder: (context) => Matrix.of(context) .accountBundles .keys .map( (bundle) => PopupMenuItem( value: bundle, child: Text(bundle), ), ) .toList(), ))); } return items; } @override Widget build(BuildContext context) { return StreamBuilder( stream: Matrix.of(context).onShareContentChanged.stream, builder: (_, __) { final selectMode = controller.selectMode; return VWidgetGuard( onSystemPop: (redirector) async { final selMode = controller.selectMode; if (selMode != SelectMode.normal) controller.cancelAction(); if (selMode == SelectMode.select) redirector.stopRedirection(); }, child: Scaffold( appBar: AppBar( elevation: controller.scrolledToTop ? 0 : null, actionsIconTheme: IconThemeData( color: controller.selectedRoomIds.isEmpty ? null : Theme.of(context).colorScheme.primary, ), leading: selectMode == SelectMode.normal ? controller.spaces.isEmpty ? null : Builder( builder: (context) => IconButton( icon: const Icon(Icons.group_work_outlined), onPressed: Scaffold.of(context).openDrawer, )) : IconButton( tooltip: L10n.of(context).cancel, icon: const Icon(Icons.close_outlined), onPressed: controller.cancelAction, color: Theme.of(context).colorScheme.primary, ), centerTitle: false, titleSpacing: controller.spaces.isEmpty ? null : 0, actions: selectMode == SelectMode.share ? null : selectMode == SelectMode.select ? [ if (controller.selectedRoomIds.length == 1 && controller.spaces.isNotEmpty && !Matrix.of(context) .client .getRoomById( controller.selectedRoomIds.single) .isDirectChat) IconButton( tooltip: L10n.of(context).addToSpace, icon: const Icon(Icons.group_work_outlined), onPressed: controller.addOrRemoveToSpace, ), if (controller.selectedRoomIds.length == 1) IconButton( tooltip: L10n.of(context).toggleUnread, icon: Icon(Matrix.of(context) .client .getRoomById( controller.selectedRoomIds.single) .isUnread ? Icons.mark_chat_read_outlined : Icons.mark_chat_unread_outlined), onPressed: controller.toggleUnread, ), if (controller.selectedRoomIds.length == 1) IconButton( tooltip: L10n.of(context).toggleFavorite, icon: const Icon(Icons.push_pin_outlined), onPressed: controller.toggleFavouriteRoom, ), if (controller.selectedRoomIds.length == 1) IconButton( icon: Icon(Matrix.of(context) .client .getRoomById(controller .selectedRoomIds.single) .pushRuleState == PushRuleState.notify ? Icons.notifications_off_outlined : Icons.notifications_outlined), tooltip: L10n.of(context).toggleMuted, onPressed: controller.toggleMuted, ), IconButton( icon: const Icon(Icons.delete_outlined), tooltip: L10n.of(context).archive, onPressed: controller.archiveAction, ), ] : [ IconButton( icon: const Icon(Icons.search_outlined), tooltip: L10n.of(context).search, onPressed: () => VRouter.of(context).to('/search'), ), PopupMenuButton( onSelected: controller.onPopupMenuSelect, itemBuilder: (_) => [ PopupMenuItem( value: PopupMenuAction.setStatus, child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.edit_outlined), const SizedBox(width: 12), Text(L10n.of(context).setStatus), ], ), ), PopupMenuItem( value: PopupMenuAction.newGroup, child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.group_add_outlined), const SizedBox(width: 12), Text(L10n.of(context).createNewGroup), ], ), ), PopupMenuItem( value: PopupMenuAction.newSpace, child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.group_work_outlined), const SizedBox(width: 12), Text(L10n.of(context).createNewSpace), ], ), ), PopupMenuItem( value: PopupMenuAction.invite, child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.share_outlined), const SizedBox(width: 12), Text(L10n.of(context).inviteContact), ], ), ), PopupMenuItem( value: PopupMenuAction.archive, child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.archive_outlined), const SizedBox(width: 12), Text(L10n.of(context).archive), ], ), ), PopupMenuItem( value: PopupMenuAction.settings, child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.settings_outlined), const SizedBox(width: 12), Text(L10n.of(context).settings), ], ), ), ], ), ], title: Text(selectMode == SelectMode.share ? L10n.of(context).share : selectMode == SelectMode.select ? controller.selectedRoomIds.length.toString() : controller.activeSpaceId == null ? AppConfig.applicationName : Matrix.of(context) .client .getRoomById(controller.activeSpaceId) .displayname), ), body: Column(children: [ const ConnectionStatusHeader(), Expanded(child: _ChatListViewBody(controller)), ]), floatingActionButton: selectMode == SelectMode.normal ? controller.scrolledToTop ? FloatingActionButton.extended( heroTag: 'main_fab', onPressed: () => VRouter.of(context).to('/newprivatechat'), icon: const Icon(CupertinoIcons.chat_bubble), label: Text(L10n.of(context).newChat), ) : FloatingActionButton( heroTag: 'main_fab', onPressed: () => VRouter.of(context).to('/newprivatechat'), child: const Icon(CupertinoIcons.chat_bubble), ) : null, bottomNavigationBar: Matrix.of(context).isMultiAccount ? StreamBuilder( stream: StreamGroup.merge(Matrix.of(context) .widget .clients .map((client) => client.onSync.stream.where((s) => s.accountData != null && s.accountData .any((e) => e.type == accountBundlesType)))), builder: (context, _) => Material( color: Theme.of(context) .bottomNavigationBarTheme .backgroundColor, child: Column( mainAxisSize: MainAxisSize.min, children: [ const Divider(height: 1), Builder(builder: (context) { final items = getBottomBarItems(context); if (items.length == 1) { return Padding( padding: const EdgeInsets.all(7.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ items.single.icon, Text(items.single.label), ], ), ); } return SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( width: max( FluffyThemes.isColumnMode(context) ? FluffyThemes.columnWidth : MediaQuery.of(context).size.width, Matrix.of(context).widget.clients.length * 84.0, ), child: BottomNavigationBar( elevation: 0, onTap: (i) => controller.setActiveClient( Matrix.of(context).currentBundle[i]), currentIndex: Matrix.of(context) .currentBundle .indexWhere( (client) => client == Matrix.of(context).client, ), showUnselectedLabels: false, showSelectedLabels: true, type: BottomNavigationBarType.shifting, selectedItemColor: Theme.of(context).colorScheme.secondary, items: items, ), ), ); }), if (controller.displayBundles) Padding( padding: const EdgeInsets.symmetric( vertical: 4.0, horizontal: 12, ), child: SizedBox( width: double.infinity, child: CupertinoSlidingSegmentedControl( groupValue: controller.secureActiveBundle, onValueChanged: controller.setActiveBundle, children: Map.fromEntries(Matrix.of(context) .accountBundles .keys .map((bundle) => MapEntry(bundle, Text(bundle)))), ), ), ), ], ), ), ) : null, drawer: controller.spaces.isEmpty ? null : Drawer( child: SafeArea( child: ListView.builder( itemCount: controller.spaces.length + 1, itemBuilder: (context, i) { if (i == 0) { return ListTile( selected: controller.activeSpaceId == null, selectedTileColor: Theme.of(context).secondaryHeaderColor, leading: CircleAvatar( foregroundColor: Colors.white, backgroundColor: Theme.of(context).primaryColor, radius: Avatar.defaultSize / 2, child: const Icon(Icons.home_outlined), ), title: Text(L10n.of(context).allChats), onTap: () => controller.setActiveSpaceId(context, null), ); } final space = controller.spaces[i - 1]; return ListTile( selected: controller.activeSpaceId == space.id, selectedTileColor: Theme.of(context).secondaryHeaderColor, leading: Avatar(space.avatar, space.displayname), title: Text(space.displayname, maxLines: 1), subtitle: Text(L10n.of(context).countParticipants( (space.summary.mJoinedMemberCount + space.summary.mInvitedMemberCount) .toString())), onTap: () => controller.setActiveSpaceId( context, space.id), trailing: IconButton( icon: const Icon(Icons.edit_outlined), onPressed: () => controller.editSpace(context, space.id), ), ); }, ), ), ), ), ); }); } } class _ChatListViewBody extends StatelessWidget { final ChatListController controller; const _ChatListViewBody(this.controller, {Key key}) : super(key: key); @override Widget build(BuildContext context) => StreamBuilder( stream: Matrix.of(context) .client .onSync .stream .where((s) => s.hasRoomUpdate) .rateLimit(const Duration(seconds: 1)), builder: (context, snapshot) { return FutureBuilder( future: controller.waitForFirstSync, builder: (BuildContext context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { final rooms = Matrix.of(context) .client .rooms .where(controller.roomCheck) .toList(); if (rooms.isEmpty) { return Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.maps_ugc_outlined, size: 80, color: Colors.grey, ), Center( child: Text( L10n.of(context).startYourFirstChat, textAlign: TextAlign.start, style: const TextStyle( color: Colors.grey, fontSize: 16, ), ), ), ], ); } return ListView.builder( controller: controller.scrollController, itemCount: rooms.length, itemBuilder: (BuildContext context, int i) { return ChatListItem( rooms[i], selected: controller.selectedRoomIds.contains(rooms[i].id), onTap: controller.selectMode == SelectMode.select ? () => controller.toggleSelection(rooms[i].id) : null, onLongPress: () => controller.toggleSelection(rooms[i].id), activeChat: controller.activeChat == rooms[i].id, ); }, ); } else { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Image.asset( 'assets/private_chat_wallpaper.png', width: 100, ), Text( L10n.of(context).yourChatsAreBeingSynced, textAlign: TextAlign.center, ), ], ), ); } }, ); }); } enum ChatListPopupMenuItemActions { createGroup, createSpace, discover, setStatus, inviteContact, settings, }