You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			399 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Dart
		
	
			
		
		
	
	
			399 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Dart
		
	
import 'package:fluffychat/config/app_config.dart';
 | 
						|
import 'package:fluffychat/pages/chat_list/chat_list.dart';
 | 
						|
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
 | 
						|
import 'package:fluffychat/pages/chat_list/dummy_chat_list_item.dart';
 | 
						|
import 'package:fluffychat/pages/chat_list/search_title.dart';
 | 
						|
import 'package:fluffychat/pages/chat_list/space_view.dart';
 | 
						|
import 'package:fluffychat/pages/chat_list/status_msg_list.dart';
 | 
						|
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
 | 
						|
import 'package:fluffychat/utils/stream_extension.dart';
 | 
						|
import 'package:fluffychat/widgets/avatar.dart';
 | 
						|
import 'package:fluffychat/widgets/hover_builder.dart';
 | 
						|
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
 | 
						|
import 'package:flutter/cupertino.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
						|
import 'package:matrix/matrix.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: 16.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),
 | 
						|
                                  child: HoverBuilder(
 | 
						|
                                    builder: (context, hovered) =>
 | 
						|
                                        AnimatedScale(
 | 
						|
                                      duration: FluffyThemes.animationDuration,
 | 
						|
                                      curve: FluffyThemes.animationCurve,
 | 
						|
                                      scale: hovered ? 1.1 : 1.0,
 | 
						|
                                      child: InkWell(
 | 
						|
                                        borderRadius: BorderRadius.circular(
 | 
						|
                                          AppConfig.borderRadius,
 | 
						|
                                        ),
 | 
						|
                                        onTap: () =>
 | 
						|
                                            controller.setActiveFilter(filter),
 | 
						|
                                        child: Container(
 | 
						|
                                          padding: const EdgeInsets.symmetric(
 | 
						|
                                            horizontal: 12,
 | 
						|
                                            vertical: 6,
 | 
						|
                                          ),
 | 
						|
                                          decoration: BoxDecoration(
 | 
						|
                                            color: filter ==
 | 
						|
                                                    controller.activeFilter
 | 
						|
                                                ? theme.colorScheme.primary
 | 
						|
                                                : theme.colorScheme
 | 
						|
                                                    .secondaryContainer,
 | 
						|
                                            borderRadius: BorderRadius.circular(
 | 
						|
                                              AppConfig.borderRadius,
 | 
						|
                                            ),
 | 
						|
                                          ),
 | 
						|
                                          alignment: Alignment.center,
 | 
						|
                                          child: Text(
 | 
						|
                                            filter.toLocalizedString(context),
 | 
						|
                                            style: TextStyle(
 | 
						|
                                              fontWeight: filter ==
 | 
						|
                                                      controller.activeFilter
 | 
						|
                                                  ? FontWeight.w500
 | 
						|
                                                  : FontWeight.normal,
 | 
						|
                                              color: filter ==
 | 
						|
                                                      controller.activeFilter
 | 
						|
                                                  ? theme.colorScheme.onPrimary
 | 
						|
                                                  : theme.colorScheme
 | 
						|
                                                      .onSecondaryContainer,
 | 
						|
                                            ),
 | 
						|
                                          ),
 | 
						|
                                        ),
 | 
						|
                                      ),
 | 
						|
                                    ),
 | 
						|
                                  ),
 | 
						|
                                ),
 | 
						|
                              )
 | 
						|
                              .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: () => showAdaptiveBottomSheet(
 | 
						|
                  context: context,
 | 
						|
                  builder: (c) => PublicRoomBottomSheet(
 | 
						|
                    roomAlias:
 | 
						|
                        publicRooms[i].canonicalAlias ?? publicRooms[i].roomId,
 | 
						|
                    outerContext: context,
 | 
						|
                    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,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      );
 | 
						|
}
 |