359 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
			
		
		
	
	
			359 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
| import 'package:flutter/cupertino.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| 
 | |
| import 'package:extera_next/generated/l10n/l10n.dart';
 | |
| import 'package:matrix/matrix.dart';
 | |
| 
 | |
| import 'package:extera_next/config/app_config.dart';
 | |
| import 'package:extera_next/pages/chat_list/chat_list.dart';
 | |
| import 'package:extera_next/pages/chat_list/chat_list_item.dart';
 | |
| import 'package:extera_next/pages/chat_list/dummy_chat_list_item.dart';
 | |
| import 'package:extera_next/pages/chat_list/search_title.dart';
 | |
| import 'package:extera_next/pages/chat_list/space_view.dart';
 | |
| import 'package:extera_next/pages/chat_list/status_msg_list.dart';
 | |
| import 'package:extera_next/utils/stream_extension.dart';
 | |
| import 'package:extera_next/widgets/adaptive_dialogs/public_room_dialog.dart';
 | |
| import 'package:extera_next/widgets/avatar.dart';
 | |
| import '../../config/themes.dart';
 | |
| import '../../widgets/adaptive_dialogs/user_dialog.dart';
 | |
| import '../../widgets/matrix.dart';
 | |
| import 'chat_list_header.dart';
 | |
| 
 | |
| class ChatListViewBody extends StatelessWidget {
 | |
|   final ChatListController controller;
 | |
| 
 | |
|   const ChatListViewBody(this.controller, {super.key});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final theme = Theme.of(context);
 | |
| 
 | |
|     final client = Matrix.of(context).client;
 | |
|     final activeSpace = controller.activeSpaceId;
 | |
|     if (activeSpace != null) {
 | |
|       return SpaceView(
 | |
|         key: ValueKey(activeSpace),
 | |
|         spaceId: activeSpace,
 | |
|         onBack: controller.clearActiveSpace,
 | |
|         onChatTab: (room) => controller.onChatTap(room),
 | |
|         onChatContext: (room, context) =>
 | |
|             controller.chatContextAction(room, context),
 | |
|         activeChat: controller.activeChat,
 | |
|         toParentSpace: controller.setActiveSpace,
 | |
|       );
 | |
|     }
 | |
|     final spaces = client.rooms.where((r) => r.isSpace);
 | |
|     final spaceDelegateCandidates = <String, Room>{};
 | |
|     for (final space in spaces) {
 | |
|       for (final spaceChild in space.spaceChildren) {
 | |
|         final roomId = spaceChild.roomId;
 | |
|         if (roomId == null) continue;
 | |
|         spaceDelegateCandidates[roomId] = space;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     final publicRooms = controller.roomSearchResult?.chunk
 | |
|         .where((room) => room.roomType != 'm.space')
 | |
|         .toList();
 | |
|     final publicSpaces = controller.roomSearchResult?.chunk
 | |
|         .where((room) => room.roomType == 'm.space')
 | |
|         .toList();
 | |
|     final userSearchResult = controller.userSearchResult;
 | |
|     const dummyChatCount = 4;
 | |
|     final filter = controller.searchController.text.toLowerCase();
 | |
|     return StreamBuilder(
 | |
|       key: ValueKey(
 | |
|         client.userID.toString(),
 | |
|       ),
 | |
|       stream: client.onSync.stream
 | |
|           .where((s) => s.hasRoomUpdate)
 | |
|           .rateLimit(const Duration(seconds: 1)),
 | |
|       builder: (context, _) {
 | |
|         final rooms = controller.filteredRooms;
 | |
| 
 | |
|         return SafeArea(
 | |
|           child: CustomScrollView(
 | |
|             controller: controller.scrollController,
 | |
|             slivers: [
 | |
|               ChatListHeader(controller: controller),
 | |
|               SliverList(
 | |
|                 delegate: SliverChildListDelegate(
 | |
|                   [
 | |
|                     if (controller.isSearchMode) ...[
 | |
|                       SearchTitle(
 | |
|                         title: L10n.of(context).publicRooms,
 | |
|                         icon: const Icon(Icons.explore_outlined),
 | |
|                       ),
 | |
|                       PublicRoomsHorizontalList(publicRooms: publicRooms),
 | |
|                       SearchTitle(
 | |
|                         title: L10n.of(context).publicSpaces,
 | |
|                         icon: const Icon(Icons.workspaces_outlined),
 | |
|                       ),
 | |
|                       PublicRoomsHorizontalList(publicRooms: publicSpaces),
 | |
|                       SearchTitle(
 | |
|                         title: L10n.of(context).users,
 | |
|                         icon: const Icon(Icons.group_outlined),
 | |
|                       ),
 | |
|                       AnimatedContainer(
 | |
|                         clipBehavior: Clip.hardEdge,
 | |
|                         decoration: const BoxDecoration(),
 | |
|                         height: userSearchResult == null ||
 | |
|                                 userSearchResult.results.isEmpty
 | |
|                             ? 0
 | |
|                             : 106,
 | |
|                         duration: FluffyThemes.animationDuration,
 | |
|                         curve: FluffyThemes.animationCurve,
 | |
|                         child: userSearchResult == null
 | |
|                             ? null
 | |
|                             : ListView.builder(
 | |
|                                 scrollDirection: Axis.horizontal,
 | |
|                                 itemCount: userSearchResult.results.length,
 | |
|                                 itemBuilder: (context, i) => _SearchItem(
 | |
|                                   title:
 | |
|                                       userSearchResult.results[i].displayName ??
 | |
|                                           userSearchResult
 | |
|                                               .results[i].userId.localpart ??
 | |
|                                           L10n.of(context).unknownDevice,
 | |
|                                   avatar: userSearchResult.results[i].avatarUrl,
 | |
|                                   onPressed: () => UserDialog.show(
 | |
|                                     context: context,
 | |
|                                     profile: userSearchResult.results[i],
 | |
|                                   ),
 | |
|                                 ),
 | |
|                               ),
 | |
|                       ),
 | |
|                     ],
 | |
|                     if (!controller.isSearchMode && AppConfig.showPresences)
 | |
|                       GestureDetector(
 | |
|                         onLongPress: () => controller.dismissStatusList(),
 | |
|                         child: StatusMessageList(
 | |
|                           onStatusEdit: controller.setStatus,
 | |
|                         ),
 | |
|                       ),
 | |
|                     AnimatedContainer(
 | |
|                       height: controller.isTorBrowser ? 64 : 0,
 | |
|                       duration: FluffyThemes.animationDuration,
 | |
|                       curve: FluffyThemes.animationCurve,
 | |
|                       clipBehavior: Clip.hardEdge,
 | |
|                       decoration: const BoxDecoration(),
 | |
|                       child: Material(
 | |
|                         color: theme.colorScheme.surface,
 | |
|                         child: ListTile(
 | |
|                           leading: const Icon(Icons.vpn_key),
 | |
|                           title: Text(L10n.of(context).dehydrateTor),
 | |
|                           subtitle: Text(L10n.of(context).dehydrateTorLong),
 | |
|                           trailing: const Icon(Icons.chevron_right_outlined),
 | |
|                           onTap: controller.dehydrate,
 | |
|                         ),
 | |
|                       ),
 | |
|                     ),
 | |
|                     if (client.rooms.isNotEmpty && !controller.isSearchMode)
 | |
|                       SizedBox(
 | |
|                         height: 64,
 | |
|                         child: ListView(
 | |
|                           padding: const EdgeInsets.symmetric(
 | |
|                             horizontal: 12.0,
 | |
|                             vertical: 12.0,
 | |
|                           ),
 | |
|                           shrinkWrap: true,
 | |
|                           scrollDirection: Axis.horizontal,
 | |
|                           children: [
 | |
|                             if (AppConfig.separateChatTypes)
 | |
|                               ActiveFilter.messages
 | |
|                             else
 | |
|                               ActiveFilter.allChats,
 | |
|                             ActiveFilter.groups,
 | |
|                             ActiveFilter.unread,
 | |
|                             if (spaceDelegateCandidates.isNotEmpty &&
 | |
|                                 !controller.widget.displayNavigationRail)
 | |
|                               ActiveFilter.spaces,
 | |
|                           ]
 | |
|                               .map(
 | |
|                                 (filter) => Padding(
 | |
|                                   padding: const EdgeInsets.symmetric(
 | |
|                                     horizontal: 4.0,
 | |
|                                   ),
 | |
|                                   child: FilterChip(
 | |
|                                     selected: filter == controller.activeFilter,
 | |
|                                     onSelected: (_) =>
 | |
|                                         controller.setActiveFilter(filter),
 | |
|                                     label:
 | |
|                                         Text(filter.toLocalizedString(context)),
 | |
|                                   ),
 | |
|                                 ),
 | |
|                               )
 | |
|                               .toList(),
 | |
|                         ),
 | |
|                       ),
 | |
|                     if (controller.isSearchMode)
 | |
|                       SearchTitle(
 | |
|                         title: L10n.of(context).chats,
 | |
|                         icon: const Icon(Icons.forum_outlined),
 | |
|                       ),
 | |
|                     if (client.prevBatch != null &&
 | |
|                         rooms.isEmpty &&
 | |
|                         !controller.isSearchMode) ...[
 | |
|                       Column(
 | |
|                         mainAxisAlignment: MainAxisAlignment.center,
 | |
|                         children: [
 | |
|                           Stack(
 | |
|                             alignment: Alignment.center,
 | |
|                             children: [
 | |
|                               const Column(
 | |
|                                 mainAxisSize: MainAxisSize.min,
 | |
|                                 children: [
 | |
|                                   DummyChatListItem(
 | |
|                                     opacity: 0.5,
 | |
|                                     animate: false,
 | |
|                                   ),
 | |
|                                   DummyChatListItem(
 | |
|                                     opacity: 0.3,
 | |
|                                     animate: false,
 | |
|                                   ),
 | |
|                                 ],
 | |
|                               ),
 | |
|                               Icon(
 | |
|                                 CupertinoIcons.chat_bubble_text_fill,
 | |
|                                 size: 128,
 | |
|                                 color: theme.colorScheme.secondary,
 | |
|                               ),
 | |
|                             ],
 | |
|                           ),
 | |
|                           Padding(
 | |
|                             padding: const EdgeInsets.all(16.0),
 | |
|                             child: Text(
 | |
|                               client.rooms.isEmpty
 | |
|                                   ? L10n.of(context).noChatsFoundHere
 | |
|                                   : L10n.of(context).noMoreChatsFound,
 | |
|                               textAlign: TextAlign.center,
 | |
|                               style: TextStyle(
 | |
|                                 fontSize: 18,
 | |
|                                 color: theme.colorScheme.secondary,
 | |
|                               ),
 | |
|                             ),
 | |
|                           ),
 | |
|                         ],
 | |
|                       ),
 | |
|                     ],
 | |
|                   ],
 | |
|                 ),
 | |
|               ),
 | |
|               if (client.prevBatch == null)
 | |
|                 SliverList(
 | |
|                   delegate: SliverChildBuilderDelegate(
 | |
|                     (context, i) => DummyChatListItem(
 | |
|                       opacity: (dummyChatCount - i) / dummyChatCount,
 | |
|                       animate: true,
 | |
|                     ),
 | |
|                     childCount: dummyChatCount,
 | |
|                   ),
 | |
|                 ),
 | |
|               if (client.prevBatch != null)
 | |
|                 SliverList.builder(
 | |
|                   itemCount: rooms.length,
 | |
|                   itemBuilder: (BuildContext context, int i) {
 | |
|                     final room = rooms[i];
 | |
|                     final space = spaceDelegateCandidates[room.id];
 | |
|                     return ChatListItem(
 | |
|                       room,
 | |
|                       space: space,
 | |
|                       key: Key('chat_list_item_${room.id}'),
 | |
|                       filter: filter,
 | |
|                       onTap: () => controller.onChatTap(room),
 | |
|                       onLongPress: (context) =>
 | |
|                           controller.chatContextAction(room, context, space),
 | |
|                       activeChat: controller.activeChat == room.id,
 | |
|                     );
 | |
|                   },
 | |
|                 ),
 | |
|             ],
 | |
|           ),
 | |
|         );
 | |
|       },
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class PublicRoomsHorizontalList extends StatelessWidget {
 | |
|   const PublicRoomsHorizontalList({
 | |
|     super.key,
 | |
|     required this.publicRooms,
 | |
|   });
 | |
| 
 | |
|   final List<PublicRoomsChunk>? publicRooms;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final publicRooms = this.publicRooms;
 | |
|     return AnimatedContainer(
 | |
|       clipBehavior: Clip.hardEdge,
 | |
|       decoration: const BoxDecoration(),
 | |
|       height: publicRooms == null || publicRooms.isEmpty ? 0 : 106,
 | |
|       duration: FluffyThemes.animationDuration,
 | |
|       curve: FluffyThemes.animationCurve,
 | |
|       child: publicRooms == null
 | |
|           ? null
 | |
|           : ListView.builder(
 | |
|               scrollDirection: Axis.horizontal,
 | |
|               itemCount: publicRooms.length,
 | |
|               itemBuilder: (context, i) => _SearchItem(
 | |
|                 title: publicRooms[i].name ??
 | |
|                     publicRooms[i].canonicalAlias?.localpart ??
 | |
|                     L10n.of(context).group,
 | |
|                 avatar: publicRooms[i].avatarUrl,
 | |
|                 onPressed: () => showAdaptiveDialog(
 | |
|                   context: context,
 | |
|                   builder: (c) => PublicRoomDialog(
 | |
|                     roomAlias:
 | |
|                         publicRooms[i].canonicalAlias ?? publicRooms[i].roomId,
 | |
|                     chunk: publicRooms[i],
 | |
|                   ),
 | |
|                 ),
 | |
|               ),
 | |
|             ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _SearchItem extends StatelessWidget {
 | |
|   final String title;
 | |
|   final Uri? avatar;
 | |
|   final void Function() onPressed;
 | |
| 
 | |
|   const _SearchItem({
 | |
|     required this.title,
 | |
|     this.avatar,
 | |
|     required this.onPressed,
 | |
|   });
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) => InkWell(
 | |
|         onTap: onPressed,
 | |
|         child: SizedBox(
 | |
|           width: 84,
 | |
|           child: Column(
 | |
|             mainAxisSize: MainAxisSize.min,
 | |
|             children: [
 | |
|               const SizedBox(height: 8),
 | |
|               Avatar(
 | |
|                 mxContent: avatar,
 | |
|                 name: title,
 | |
|               ),
 | |
|               Padding(
 | |
|                 padding: const EdgeInsets.all(8.0),
 | |
|                 child: Text(
 | |
|                   title,
 | |
|                   maxLines: 2,
 | |
|                   textAlign: TextAlign.center,
 | |
|                   overflow: TextOverflow.ellipsis,
 | |
|                   style: const TextStyle(
 | |
|                     fontSize: 12,
 | |
|                   ),
 | |
|                 ),
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|         ),
 | |
|       );
 | |
| }
 |