mirror of
				https://gitlab.com/famedly/fluffychat.git
				synced 2025-11-03 22:07:23 +01:00 
			
		
		
		
	feat: implement space hierarchy
- implement spaces hierarchy - create expandable navigation drawer tiles - display suggested rooms below joined rooms when in space - everything works nested - fix shared preferenced conflic with debug builds on Linux - add [`package:async`](https://pub.dev/packages/async) Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
This commit is contained in:
		
							parent
							
								
									9f3754fc2f
								
							
						
					
					
						commit
						80f6505671
					
				@ -174,6 +174,13 @@
 | 
			
		||||
    "type": "text",
 | 
			
		||||
    "placeholders": {}
 | 
			
		||||
  },
 | 
			
		||||
  "numberRoomMembers": "{number} members",
 | 
			
		||||
  "@numberRoomMembers": {
 | 
			
		||||
    "type": "number",
 | 
			
		||||
    "placeholders": {
 | 
			
		||||
      "number": {}
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "badServerLoginTypesException": "The homeserver supports the login types:\n{serverVersions}\nBut this app supports only:\n{supportedVersions}",
 | 
			
		||||
  "@badServerLoginTypesException": {
 | 
			
		||||
    "type": "text",
 | 
			
		||||
@ -210,6 +217,7 @@
 | 
			
		||||
      "targetName": {}
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "suggestedRooms": "Discover groups in this space",
 | 
			
		||||
  "blockDevice": "Block Device",
 | 
			
		||||
  "@blockDevice": {
 | 
			
		||||
    "type": "text",
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import 'package:matrix/matrix.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/search_title.dart';
 | 
			
		||||
import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
 | 
			
		||||
import 'package:fluffychat/pages/chat_list/stories_header.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/avatar.dart';
 | 
			
		||||
@ -16,6 +17,7 @@ import 'package:fluffychat/widgets/profile_bottom_sheet.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
 | 
			
		||||
import '../../utils/stream_extension.dart';
 | 
			
		||||
import '../../widgets/matrix.dart';
 | 
			
		||||
import 'spaces_hierarchy_proposal.dart';
 | 
			
		||||
 | 
			
		||||
class ChatListViewBody extends StatefulWidget {
 | 
			
		||||
  final ChatListController controller;
 | 
			
		||||
@ -55,201 +57,206 @@ class _ChatListViewBodyState extends State<ChatListViewBody> {
 | 
			
		||||
    if (widget.controller.waitForFirstSync &&
 | 
			
		||||
        Matrix.of(context).client.prevBatch != null) {
 | 
			
		||||
      final rooms = widget.controller.activeSpacesEntry.getRooms(context);
 | 
			
		||||
      if (rooms.isEmpty) {
 | 
			
		||||
        child = Column(
 | 
			
		||||
          key: const ValueKey(null),
 | 
			
		||||
          mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
          children: <Widget>[
 | 
			
		||||
            Image.asset(
 | 
			
		||||
              'assets/private_chat_wallpaper.png',
 | 
			
		||||
              width: 160,
 | 
			
		||||
              height: 160,
 | 
			
		||||
            ),
 | 
			
		||||
            Center(
 | 
			
		||||
              child: Text(
 | 
			
		||||
                L10n.of(context)!.startYourFirstChat,
 | 
			
		||||
                textAlign: TextAlign.start,
 | 
			
		||||
                style: const TextStyle(
 | 
			
		||||
                  color: Colors.grey,
 | 
			
		||||
                  fontSize: 16,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            const SizedBox(height: 160),
 | 
			
		||||
          ],
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        final displayStoriesHeader = widget.controller.activeSpacesEntry
 | 
			
		||||
            .shouldShowStoriesHeader(context);
 | 
			
		||||
        child = ListView.builder(
 | 
			
		||||
          key: ValueKey(Matrix.of(context).client.userID.toString() +
 | 
			
		||||
              widget.controller.activeSpaceId.toString() +
 | 
			
		||||
              widget.controller.activeSpacesEntry.runtimeType.toString()),
 | 
			
		||||
          controller: widget.controller.scrollController,
 | 
			
		||||
          // add +1 space below in order to properly scroll below the spaces bar
 | 
			
		||||
          itemCount: rooms.length + (displayStoriesHeader ? 2 : 1),
 | 
			
		||||
          itemBuilder: (BuildContext context, int i) {
 | 
			
		||||
            if (displayStoriesHeader) {
 | 
			
		||||
              if (i == 0) {
 | 
			
		||||
                return Column(
 | 
			
		||||
                  mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    const ConnectionStatusHeader(),
 | 
			
		||||
                    if (roomSearchResult != null) ...[
 | 
			
		||||
                      _SearchTitle(
 | 
			
		||||
                        title: L10n.of(context)!.publicRooms,
 | 
			
		||||
                        icon: Icons.explore_outlined,
 | 
			
		||||
                      ),
 | 
			
		||||
                      AnimatedContainer(
 | 
			
		||||
                        height: roomSearchResult.chunk.isEmpty ? 0 : 106,
 | 
			
		||||
                        duration: const Duration(milliseconds: 250),
 | 
			
		||||
                        clipBehavior: Clip.hardEdge,
 | 
			
		||||
                        decoration: const BoxDecoration(),
 | 
			
		||||
                        child: ListView.builder(
 | 
			
		||||
                          scrollDirection: Axis.horizontal,
 | 
			
		||||
                          itemCount: roomSearchResult.chunk.length,
 | 
			
		||||
                          itemBuilder: (context, i) => _SearchItem(
 | 
			
		||||
                            title: roomSearchResult.chunk[i].name ??
 | 
			
		||||
                                roomSearchResult
 | 
			
		||||
                                    .chunk[i].canonicalAlias?.localpart ??
 | 
			
		||||
                                L10n.of(context)!.group,
 | 
			
		||||
                            avatar: roomSearchResult.chunk[i].avatarUrl,
 | 
			
		||||
                            onPressed: () => showModalBottomSheet(
 | 
			
		||||
                              context: context,
 | 
			
		||||
                              builder: (c) => PublicRoomBottomSheet(
 | 
			
		||||
                                roomAlias:
 | 
			
		||||
                                    roomSearchResult.chunk[i].canonicalAlias ??
 | 
			
		||||
                                        roomSearchResult.chunk[i].roomId,
 | 
			
		||||
                                outerContext: context,
 | 
			
		||||
                                chunk: roomSearchResult.chunk[i],
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                    if (userSearchResult != null) ...[
 | 
			
		||||
                      _SearchTitle(
 | 
			
		||||
                        title: L10n.of(context)!.users,
 | 
			
		||||
                        icon: Icons.group_outlined,
 | 
			
		||||
                      ),
 | 
			
		||||
                      AnimatedContainer(
 | 
			
		||||
                        height: userSearchResult.results.isEmpty ? 0 : 106,
 | 
			
		||||
                        duration: const Duration(milliseconds: 250),
 | 
			
		||||
                        clipBehavior: Clip.hardEdge,
 | 
			
		||||
                        decoration: const BoxDecoration(),
 | 
			
		||||
                        child: 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: () => showModalBottomSheet(
 | 
			
		||||
                              context: context,
 | 
			
		||||
                              builder: (c) => ProfileBottomSheet(
 | 
			
		||||
                                userId: userSearchResult.results[i].userId,
 | 
			
		||||
                                outerContext: context,
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                    if (widget.controller.isSearchMode)
 | 
			
		||||
                      _SearchTitle(
 | 
			
		||||
                        title: L10n.of(context)!.stories,
 | 
			
		||||
                        icon: Icons.camera_alt_outlined,
 | 
			
		||||
                      ),
 | 
			
		||||
                    StoriesHeader(
 | 
			
		||||
                      filter: widget.controller.searchController.text,
 | 
			
		||||
 | 
			
		||||
      final displayStoriesHeader = widget.controller.activeSpacesEntry
 | 
			
		||||
              .shouldShowStoriesHeader(context) ||
 | 
			
		||||
          rooms.isEmpty;
 | 
			
		||||
      child = ListView.builder(
 | 
			
		||||
        key: ValueKey(Matrix.of(context).client.userID.toString() +
 | 
			
		||||
            widget.controller.activeSpaceId.toString() +
 | 
			
		||||
            widget.controller.activeSpacesEntry.runtimeType.toString()),
 | 
			
		||||
        controller: widget.controller.scrollController,
 | 
			
		||||
        // add +1 space below in order to properly scroll below the spaces bar
 | 
			
		||||
        itemCount: rooms.length + (displayStoriesHeader ? 2 : 1),
 | 
			
		||||
        itemBuilder: (BuildContext context, int i) {
 | 
			
		||||
          if (displayStoriesHeader) {
 | 
			
		||||
            if (i == 0) {
 | 
			
		||||
              return Column(
 | 
			
		||||
                mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                children: [
 | 
			
		||||
                  const ConnectionStatusHeader(),
 | 
			
		||||
                  if (roomSearchResult != null) ...[
 | 
			
		||||
                    SearchTitle(
 | 
			
		||||
                      title: L10n.of(context)!.publicRooms,
 | 
			
		||||
                      icon: const Icon(Icons.explore_outlined),
 | 
			
		||||
                    ),
 | 
			
		||||
                    AnimatedContainer(
 | 
			
		||||
                      height: !widget.controller.isSearchMode &&
 | 
			
		||||
                              widget.controller.showChatBackupBanner
 | 
			
		||||
                          ? 54
 | 
			
		||||
                          : 0,
 | 
			
		||||
                      duration: const Duration(milliseconds: 300),
 | 
			
		||||
                      height: roomSearchResult.chunk.isEmpty ? 0 : 106,
 | 
			
		||||
                      duration: const Duration(milliseconds: 250),
 | 
			
		||||
                      clipBehavior: Clip.hardEdge,
 | 
			
		||||
                      curve: Curves.bounceInOut,
 | 
			
		||||
                      decoration: const BoxDecoration(),
 | 
			
		||||
                      child: Material(
 | 
			
		||||
                        color: Theme.of(context).colorScheme.surface,
 | 
			
		||||
                        child: ListTile(
 | 
			
		||||
                          leading: CircleAvatar(
 | 
			
		||||
                            radius: Avatar.defaultSize / 2,
 | 
			
		||||
                            child:
 | 
			
		||||
                                const Icon(Icons.enhanced_encryption_outlined),
 | 
			
		||||
                            backgroundColor:
 | 
			
		||||
                                Theme.of(context).colorScheme.surfaceVariant,
 | 
			
		||||
                            foregroundColor:
 | 
			
		||||
                                Theme.of(context).colorScheme.onSurfaceVariant,
 | 
			
		||||
                          ),
 | 
			
		||||
                          title: Text(
 | 
			
		||||
                            (Matrix.of(context)
 | 
			
		||||
                                        .client
 | 
			
		||||
                                        .encryption
 | 
			
		||||
                                        ?.keyManager
 | 
			
		||||
                                        .enabled ==
 | 
			
		||||
                                    true)
 | 
			
		||||
                                ? L10n.of(context)!.unlockOldMessages
 | 
			
		||||
                                : L10n.of(context)!.enableAutoBackups,
 | 
			
		||||
                            style: TextStyle(
 | 
			
		||||
                              color: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                      child: ListView.builder(
 | 
			
		||||
                        scrollDirection: Axis.horizontal,
 | 
			
		||||
                        itemCount: roomSearchResult.chunk.length,
 | 
			
		||||
                        itemBuilder: (context, i) => _SearchItem(
 | 
			
		||||
                          title: roomSearchResult.chunk[i].name ??
 | 
			
		||||
                              roomSearchResult
 | 
			
		||||
                                  .chunk[i].canonicalAlias?.localpart ??
 | 
			
		||||
                              L10n.of(context)!.group,
 | 
			
		||||
                          avatar: roomSearchResult.chunk[i].avatarUrl,
 | 
			
		||||
                          onPressed: () => showModalBottomSheet(
 | 
			
		||||
                            context: context,
 | 
			
		||||
                            builder: (c) => PublicRoomBottomSheet(
 | 
			
		||||
                              roomAlias:
 | 
			
		||||
                                  roomSearchResult.chunk[i].canonicalAlias ??
 | 
			
		||||
                                      roomSearchResult.chunk[i].roomId,
 | 
			
		||||
                              outerContext: context,
 | 
			
		||||
                              chunk: roomSearchResult.chunk[i],
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                          trailing: const Icon(Icons.chevron_right_outlined),
 | 
			
		||||
                          onTap: widget.controller.firstRunBootstrapAction,
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    AnimatedContainer(
 | 
			
		||||
                      height: widget.controller.isTorBrowser ? 64 : 0,
 | 
			
		||||
                      duration: const Duration(milliseconds: 300),
 | 
			
		||||
                      clipBehavior: Clip.hardEdge,
 | 
			
		||||
                      curve: Curves.bounceInOut,
 | 
			
		||||
                      decoration: const BoxDecoration(),
 | 
			
		||||
                      child: Material(
 | 
			
		||||
                        color: Theme.of(context).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: widget.controller.dehydrate,
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (widget.controller.isSearchMode)
 | 
			
		||||
                      _SearchTitle(
 | 
			
		||||
                        title: L10n.of(context)!.chats,
 | 
			
		||||
                        icon: Icons.chat_outlined,
 | 
			
		||||
                      ),
 | 
			
		||||
                  ],
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
              i--;
 | 
			
		||||
                  if (userSearchResult != null) ...[
 | 
			
		||||
                    SearchTitle(
 | 
			
		||||
                      title: L10n.of(context)!.users,
 | 
			
		||||
                      icon: const Icon(Icons.group_outlined),
 | 
			
		||||
                    ),
 | 
			
		||||
                    AnimatedContainer(
 | 
			
		||||
                      height: userSearchResult.results.isEmpty ? 0 : 106,
 | 
			
		||||
                      duration: const Duration(milliseconds: 250),
 | 
			
		||||
                      clipBehavior: Clip.hardEdge,
 | 
			
		||||
                      decoration: const BoxDecoration(),
 | 
			
		||||
                      child: 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: () => showModalBottomSheet(
 | 
			
		||||
                            context: context,
 | 
			
		||||
                            builder: (c) => ProfileBottomSheet(
 | 
			
		||||
                              userId: userSearchResult.results[i].userId,
 | 
			
		||||
                              outerContext: context,
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
                  if (widget.controller.isSearchMode)
 | 
			
		||||
                    SearchTitle(
 | 
			
		||||
                      title: L10n.of(context)!.stories,
 | 
			
		||||
                      icon: const Icon(Icons.camera_alt_outlined),
 | 
			
		||||
                    ),
 | 
			
		||||
                  StoriesHeader(
 | 
			
		||||
                    filter: widget.controller.searchController.text,
 | 
			
		||||
                  ),
 | 
			
		||||
                  AnimatedContainer(
 | 
			
		||||
                    height: !widget.controller.isSearchMode &&
 | 
			
		||||
                            widget.controller.showChatBackupBanner
 | 
			
		||||
                        ? 54
 | 
			
		||||
                        : 0,
 | 
			
		||||
                    duration: const Duration(milliseconds: 300),
 | 
			
		||||
                    clipBehavior: Clip.hardEdge,
 | 
			
		||||
                    curve: Curves.bounceInOut,
 | 
			
		||||
                    decoration: const BoxDecoration(),
 | 
			
		||||
                    child: Material(
 | 
			
		||||
                      color: Theme.of(context).colorScheme.surface,
 | 
			
		||||
                      child: ListTile(
 | 
			
		||||
                        leading: CircleAvatar(
 | 
			
		||||
                          radius: Avatar.defaultSize / 2,
 | 
			
		||||
                          child: const Icon(Icons.enhanced_encryption_outlined),
 | 
			
		||||
                          backgroundColor:
 | 
			
		||||
                              Theme.of(context).colorScheme.surfaceVariant,
 | 
			
		||||
                          foregroundColor:
 | 
			
		||||
                              Theme.of(context).colorScheme.onSurfaceVariant,
 | 
			
		||||
                        ),
 | 
			
		||||
                        title: Text(
 | 
			
		||||
                          (Matrix.of(context)
 | 
			
		||||
                                      .client
 | 
			
		||||
                                      .encryption
 | 
			
		||||
                                      ?.keyManager
 | 
			
		||||
                                      .enabled ==
 | 
			
		||||
                                  true)
 | 
			
		||||
                              ? L10n.of(context)!.unlockOldMessages
 | 
			
		||||
                              : L10n.of(context)!.enableAutoBackups,
 | 
			
		||||
                          style: TextStyle(
 | 
			
		||||
                            color: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        trailing: const Icon(Icons.chevron_right_outlined),
 | 
			
		||||
                        onTap: widget.controller.firstRunBootstrapAction,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  AnimatedContainer(
 | 
			
		||||
                    height: widget.controller.isTorBrowser ? 64 : 0,
 | 
			
		||||
                    duration: const Duration(milliseconds: 300),
 | 
			
		||||
                    clipBehavior: Clip.hardEdge,
 | 
			
		||||
                    curve: Curves.bounceInOut,
 | 
			
		||||
                    decoration: const BoxDecoration(),
 | 
			
		||||
                    child: Material(
 | 
			
		||||
                      color: Theme.of(context).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: widget.controller.dehydrate,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  if (widget.controller.isSearchMode)
 | 
			
		||||
                    SearchTitle(
 | 
			
		||||
                      title: L10n.of(context)!.chats,
 | 
			
		||||
                      icon: const Icon(Icons.chat_outlined),
 | 
			
		||||
                    ),
 | 
			
		||||
                  if (rooms.isEmpty)
 | 
			
		||||
                    Column(
 | 
			
		||||
                      key: const ValueKey(null),
 | 
			
		||||
                      mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                      mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                      children: <Widget>[
 | 
			
		||||
                        Image.asset(
 | 
			
		||||
                          'assets/private_chat_wallpaper.png',
 | 
			
		||||
                          width: 160,
 | 
			
		||||
                          height: 160,
 | 
			
		||||
                        ),
 | 
			
		||||
                        Center(
 | 
			
		||||
                          child: Text(
 | 
			
		||||
                            L10n.of(context)!.startYourFirstChat,
 | 
			
		||||
                            textAlign: TextAlign.start,
 | 
			
		||||
                            style: const TextStyle(
 | 
			
		||||
                              color: Colors.grey,
 | 
			
		||||
                              fontSize: 16,
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        const SizedBox(height: 16),
 | 
			
		||||
                      ],
 | 
			
		||||
                    ),
 | 
			
		||||
                ],
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
            if (i >= rooms.length) {
 | 
			
		||||
              return const ListTile();
 | 
			
		||||
            }
 | 
			
		||||
            if (!rooms[i].displayname.toLowerCase().contains(
 | 
			
		||||
                widget.controller.searchController.text.toLowerCase())) {
 | 
			
		||||
              return Container();
 | 
			
		||||
            }
 | 
			
		||||
            return ChatListItem(
 | 
			
		||||
              rooms[i],
 | 
			
		||||
              selected: widget.controller.selectedRoomIds.contains(rooms[i].id),
 | 
			
		||||
              onTap: widget.controller.selectMode == SelectMode.select
 | 
			
		||||
                  ? () => widget.controller.toggleSelection(rooms[i].id)
 | 
			
		||||
            i--;
 | 
			
		||||
          }
 | 
			
		||||
          if (i >= rooms.length) {
 | 
			
		||||
            return SpacesHierarchyProposals(
 | 
			
		||||
              space: widget.controller.activeSpacesEntry.getSpace(context)?.id,
 | 
			
		||||
              query: widget.controller.isSearchMode
 | 
			
		||||
                  ? widget.controller.searchController.text
 | 
			
		||||
                  : null,
 | 
			
		||||
              onLongPress: () => widget.controller.toggleSelection(rooms[i].id),
 | 
			
		||||
              activeChat: widget.controller.activeChat == rooms[i].id,
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
          }
 | 
			
		||||
          if (!rooms[i].displayname.toLowerCase().contains(
 | 
			
		||||
              widget.controller.searchController.text.toLowerCase())) {
 | 
			
		||||
            return Container();
 | 
			
		||||
          }
 | 
			
		||||
          return ChatListItem(
 | 
			
		||||
            rooms[i],
 | 
			
		||||
            selected: widget.controller.selectedRoomIds.contains(rooms[i].id),
 | 
			
		||||
            onTap: widget.controller.selectMode == SelectMode.select
 | 
			
		||||
                ? () => widget.controller.toggleSelection(rooms[i].id)
 | 
			
		||||
                : null,
 | 
			
		||||
            onLongPress: () => widget.controller.toggleSelection(rooms[i].id),
 | 
			
		||||
            activeChat: widget.controller.activeChat == rooms[i].id,
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      const dummyChatCount = 5;
 | 
			
		||||
      final titleColor =
 | 
			
		||||
@ -361,54 +368,11 @@ class _ChatListViewBodyState extends State<ChatListViewBody> {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _SearchTitle extends StatelessWidget {
 | 
			
		||||
  final String title;
 | 
			
		||||
  final IconData icon;
 | 
			
		||||
  const _SearchTitle({
 | 
			
		||||
    required this.title,
 | 
			
		||||
    required this.icon,
 | 
			
		||||
    Key? key,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) => Container(
 | 
			
		||||
        decoration: BoxDecoration(
 | 
			
		||||
          border: Border.symmetric(
 | 
			
		||||
              horizontal: BorderSide(
 | 
			
		||||
            color: Theme.of(context).dividerColor,
 | 
			
		||||
            width: 1,
 | 
			
		||||
          )),
 | 
			
		||||
          color: Theme.of(context).colorScheme.surface,
 | 
			
		||||
        ),
 | 
			
		||||
        child: Align(
 | 
			
		||||
          alignment: Alignment.centerLeft,
 | 
			
		||||
          child: Padding(
 | 
			
		||||
            padding: const EdgeInsets.symmetric(
 | 
			
		||||
              horizontal: 16,
 | 
			
		||||
              vertical: 8,
 | 
			
		||||
            ),
 | 
			
		||||
            child: Row(
 | 
			
		||||
              children: [
 | 
			
		||||
                Icon(icon, size: 16),
 | 
			
		||||
                const SizedBox(width: 16),
 | 
			
		||||
                Text(title,
 | 
			
		||||
                    textAlign: TextAlign.left,
 | 
			
		||||
                    style: TextStyle(
 | 
			
		||||
                      color: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                      fontSize: 12,
 | 
			
		||||
                      fontWeight: FontWeight.bold,
 | 
			
		||||
                    )),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _SearchItem extends StatelessWidget {
 | 
			
		||||
  final String title;
 | 
			
		||||
  final Uri? avatar;
 | 
			
		||||
  final void Function() onPressed;
 | 
			
		||||
 | 
			
		||||
  const _SearchItem({
 | 
			
		||||
    required this.title,
 | 
			
		||||
    this.avatar,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										113
									
								
								lib/pages/chat_list/recommended_room_list_item.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								lib/pages/chat_list/recommended_room_list_item.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,113 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
import 'package:matrix/matrix.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/pages/chat_list/spaces_hierarchy_proposal.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/avatar.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
 | 
			
		||||
 | 
			
		||||
class RecommendedRoomListItem extends StatelessWidget {
 | 
			
		||||
  final SpaceRoomsChunk room;
 | 
			
		||||
 | 
			
		||||
  const RecommendedRoomListItem({Key? key, required this.room})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final leading = Avatar(
 | 
			
		||||
      mxContent: room.avatarUrl,
 | 
			
		||||
      name: room.name,
 | 
			
		||||
    );
 | 
			
		||||
    final title = Row(
 | 
			
		||||
      children: <Widget>[
 | 
			
		||||
        Expanded(
 | 
			
		||||
          child: Text(
 | 
			
		||||
            room.name ?? '',
 | 
			
		||||
            maxLines: 1,
 | 
			
		||||
            overflow: TextOverflow.ellipsis,
 | 
			
		||||
            softWrap: false,
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
              fontWeight: FontWeight.bold,
 | 
			
		||||
              color: Theme.of(context).textTheme.bodyText1!.color,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        // number of joined users
 | 
			
		||||
        Padding(
 | 
			
		||||
          padding: const EdgeInsets.only(left: 4.0),
 | 
			
		||||
          child: Text.rich(
 | 
			
		||||
            TextSpan(children: [
 | 
			
		||||
              WidgetSpan(
 | 
			
		||||
                  child: Tooltip(
 | 
			
		||||
                    child: const Icon(
 | 
			
		||||
                      Icons.people_outlined,
 | 
			
		||||
                      size: 20,
 | 
			
		||||
                    ),
 | 
			
		||||
                    message: L10n.of(context)!
 | 
			
		||||
                        .numberRoomMembers(room.numJoinedMembers),
 | 
			
		||||
                  ),
 | 
			
		||||
                  alignment: PlaceholderAlignment.middle,
 | 
			
		||||
                  baseline: TextBaseline.alphabetic),
 | 
			
		||||
              TextSpan(text: ' ${room.numJoinedMembers}')
 | 
			
		||||
            ]),
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
              fontSize: 13,
 | 
			
		||||
              color: Theme.of(context).textTheme.bodyText2!.color,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
    final subtitle = Row(
 | 
			
		||||
      mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
      children: <Widget>[
 | 
			
		||||
        Expanded(
 | 
			
		||||
          child: Text(
 | 
			
		||||
            room.topic ?? 'topic',
 | 
			
		||||
            softWrap: false,
 | 
			
		||||
            maxLines: 1,
 | 
			
		||||
            overflow: TextOverflow.ellipsis,
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
              color: Theme.of(context).textTheme.bodyText2!.color,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
    void handler() => showModalBottomSheet(
 | 
			
		||||
          context: context,
 | 
			
		||||
          builder: (c) => PublicRoomBottomSheet(
 | 
			
		||||
            roomAlias: room.canonicalAlias!,
 | 
			
		||||
            outerContext: context,
 | 
			
		||||
            chunk: room,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
    if (room.roomType == 'm.space') {
 | 
			
		||||
      return Material(
 | 
			
		||||
        color: Colors.transparent,
 | 
			
		||||
        child: ExpansionTile(
 | 
			
		||||
          leading: leading,
 | 
			
		||||
          title: title,
 | 
			
		||||
          subtitle: subtitle,
 | 
			
		||||
          onExpansionChanged: (open) {
 | 
			
		||||
            if (!open) handler();
 | 
			
		||||
          },
 | 
			
		||||
          children: [
 | 
			
		||||
            SpacesHierarchyProposals(space: room.roomId),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return Material(
 | 
			
		||||
        color: Colors.transparent,
 | 
			
		||||
        child: ListTile(
 | 
			
		||||
          leading: leading,
 | 
			
		||||
          title: title,
 | 
			
		||||
          subtitle: subtitle,
 | 
			
		||||
          onTap: handler,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										67
									
								
								lib/pages/chat_list/search_title.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								lib/pages/chat_list/search_title.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
class SearchTitle extends StatelessWidget {
 | 
			
		||||
  final String title;
 | 
			
		||||
  final Widget icon;
 | 
			
		||||
  final Widget? trailing;
 | 
			
		||||
  final void Function()? onTap;
 | 
			
		||||
 | 
			
		||||
  const SearchTitle({
 | 
			
		||||
    required this.title,
 | 
			
		||||
    required this.icon,
 | 
			
		||||
    this.trailing,
 | 
			
		||||
    this.onTap,
 | 
			
		||||
    Key? key,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) => Material(
 | 
			
		||||
        shape: Border(
 | 
			
		||||
          top: BorderSide(
 | 
			
		||||
            color: Theme.of(context).dividerColor,
 | 
			
		||||
            width: 1,
 | 
			
		||||
          ),
 | 
			
		||||
          bottom: BorderSide(
 | 
			
		||||
            color: Theme.of(context).dividerColor,
 | 
			
		||||
            width: 1,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        color: Theme.of(context).colorScheme.surface,
 | 
			
		||||
        child: InkWell(
 | 
			
		||||
          onTap: onTap,
 | 
			
		||||
          splashColor: Theme.of(context).colorScheme.surface,
 | 
			
		||||
          child: Align(
 | 
			
		||||
            alignment: Alignment.centerLeft,
 | 
			
		||||
            child: Padding(
 | 
			
		||||
              padding: const EdgeInsets.symmetric(
 | 
			
		||||
                horizontal: 16,
 | 
			
		||||
                vertical: 8,
 | 
			
		||||
              ),
 | 
			
		||||
              child: IconTheme(
 | 
			
		||||
                data: Theme.of(context).iconTheme.copyWith(size: 16),
 | 
			
		||||
                child: Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    icon,
 | 
			
		||||
                    const SizedBox(width: 16),
 | 
			
		||||
                    Text(title,
 | 
			
		||||
                        textAlign: TextAlign.left,
 | 
			
		||||
                        style: TextStyle(
 | 
			
		||||
                          color: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                          fontSize: 12,
 | 
			
		||||
                          fontWeight: FontWeight.bold,
 | 
			
		||||
                        )),
 | 
			
		||||
                    if (trailing != null)
 | 
			
		||||
                      Expanded(
 | 
			
		||||
                        child: Align(
 | 
			
		||||
                          alignment: Alignment.centerRight,
 | 
			
		||||
                          child: trailing!,
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +1,16 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
import 'package:vrouter/vrouter.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/avatar.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/matrix.dart';
 | 
			
		||||
import 'chat_list.dart';
 | 
			
		||||
import 'spaces_drawer_entry.dart';
 | 
			
		||||
 | 
			
		||||
class SpacesDrawer extends StatelessWidget {
 | 
			
		||||
  final ChatListController controller;
 | 
			
		||||
@ -14,19 +19,62 @@ class SpacesDrawer extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final currentIndex = controller.spacesEntries.indexWhere((space) =>
 | 
			
		||||
        controller.activeSpacesEntry.runtimeType == space.runtimeType &&
 | 
			
		||||
        (controller.activeSpaceId == space.getSpace(context)?.id));
 | 
			
		||||
    final spaceEntries = controller.spacesEntries
 | 
			
		||||
        .map((e) => SpacesEntryMaybeChildren.buildIfTopLevel(
 | 
			
		||||
            e, controller.spacesEntries))
 | 
			
		||||
        .whereNotNull()
 | 
			
		||||
        .toList();
 | 
			
		||||
 | 
			
		||||
    final Map<SpacesEntry, dynamic> spaceHierarchy =
 | 
			
		||||
        Map.fromEntries(controller.spacesEntries.map((e) => MapEntry(e, null)));
 | 
			
		||||
    final childSpaceIds = <String>{};
 | 
			
		||||
 | 
			
		||||
    final spacesHierarchy = <SpacesEntryMaybeChildren>[];
 | 
			
		||||
 | 
			
		||||
    final matrix = Matrix.of(context);
 | 
			
		||||
    for (final entry in spaceEntries) {
 | 
			
		||||
      if (entry.spacesEntry is SpaceSpacesEntry) {
 | 
			
		||||
        final space = entry.spacesEntry.getSpace(context);
 | 
			
		||||
        if (space != null && space.spaceChildren.isNotEmpty) {
 | 
			
		||||
          final children = space.spaceChildren;
 | 
			
		||||
          // computing the children space entries
 | 
			
		||||
          final childrenSpaceEntries = spaceEntries.where((element) {
 | 
			
		||||
            // current ID
 | 
			
		||||
            final id = element.spacesEntry.getSpace(context)?.id;
 | 
			
		||||
 | 
			
		||||
            // comparing against the supposed IDs of the children and checking
 | 
			
		||||
            // whether the room is already joined
 | 
			
		||||
            return children.any(
 | 
			
		||||
              (child) =>
 | 
			
		||||
                  child.roomId == id &&
 | 
			
		||||
                  matrix.client.rooms
 | 
			
		||||
                      .any((joinedRoom) => child.roomId == joinedRoom.id),
 | 
			
		||||
            );
 | 
			
		||||
          });
 | 
			
		||||
          childSpaceIds.addAll(childrenSpaceEntries
 | 
			
		||||
              .map((e) => e.spacesEntry.getSpace(context)?.id)
 | 
			
		||||
              .whereNotNull());
 | 
			
		||||
          entry.children.addAll(childrenSpaceEntries);
 | 
			
		||||
          spacesHierarchy.add(entry);
 | 
			
		||||
        } else {
 | 
			
		||||
          if (space?.spaceParents.isEmpty ?? false) {
 | 
			
		||||
            spacesHierarchy.add(entry);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        spacesHierarchy.add(entry);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    spacesHierarchy.removeWhere((element) =>
 | 
			
		||||
        childSpaceIds.contains(element.spacesEntry.getSpace(context)?.id));
 | 
			
		||||
 | 
			
		||||
    // final spacesHierarchy = spaceEntries;
 | 
			
		||||
 | 
			
		||||
    // TODO(TheOeWithTheBraid): wait for space hierarchy https://gitlab.com/famedly/company/frontend/libraries/matrix_api_lite/-/merge_requests/58
 | 
			
		||||
 | 
			
		||||
    return ListView.builder(
 | 
			
		||||
      itemCount: spaceHierarchy.length + 1,
 | 
			
		||||
      itemCount: spacesHierarchy.length + 1,
 | 
			
		||||
      itemBuilder: (context, i) {
 | 
			
		||||
        if (i == spaceHierarchy.length) {
 | 
			
		||||
        if (i == spacesHierarchy.length) {
 | 
			
		||||
          return ListTile(
 | 
			
		||||
            leading: CircleAvatar(
 | 
			
		||||
              radius: Avatar.defaultSize / 2,
 | 
			
		||||
@ -43,54 +91,86 @@ class SpacesDrawer extends StatelessWidget {
 | 
			
		||||
            },
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        final space = spaceHierarchy.keys.toList()[i];
 | 
			
		||||
        final room = space.getSpace(context);
 | 
			
		||||
        final active = currentIndex == i;
 | 
			
		||||
        return ListTile(
 | 
			
		||||
          selected: active,
 | 
			
		||||
          leading: room == null
 | 
			
		||||
              ? CircleAvatar(
 | 
			
		||||
                  child: space.getIcon(active),
 | 
			
		||||
                  radius: Avatar.defaultSize / 2,
 | 
			
		||||
                  backgroundColor: Theme.of(context).colorScheme.secondary,
 | 
			
		||||
                  foregroundColor: Theme.of(context).colorScheme.onSecondary,
 | 
			
		||||
                )
 | 
			
		||||
              : Avatar(
 | 
			
		||||
                  mxContent: room.avatar,
 | 
			
		||||
                  name: space.getName(context),
 | 
			
		||||
                ),
 | 
			
		||||
          title: Text(
 | 
			
		||||
            space.getName(context),
 | 
			
		||||
            maxLines: 1,
 | 
			
		||||
            overflow: TextOverflow.ellipsis,
 | 
			
		||||
          ),
 | 
			
		||||
          subtitle: room?.topic.isEmpty ?? true
 | 
			
		||||
              ? null
 | 
			
		||||
              : Tooltip(
 | 
			
		||||
                  message: room!.topic,
 | 
			
		||||
                  child: Text(
 | 
			
		||||
                    room.topic.replaceAll('\n', ' '),
 | 
			
		||||
                    softWrap: false,
 | 
			
		||||
                    overflow: TextOverflow.fade,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
          onTap: () => controller.setActiveSpacesEntry(
 | 
			
		||||
            context,
 | 
			
		||||
            space,
 | 
			
		||||
          ),
 | 
			
		||||
          trailing: room != null
 | 
			
		||||
              ? SizedBox(
 | 
			
		||||
                  width: 32,
 | 
			
		||||
                  child: IconButton(
 | 
			
		||||
                    splashRadius: 24,
 | 
			
		||||
                    icon: const Icon(Icons.edit_outlined),
 | 
			
		||||
                    tooltip: L10n.of(context)!.edit,
 | 
			
		||||
                    onPressed: () => controller.editSpace(context, room.id),
 | 
			
		||||
                  ),
 | 
			
		||||
                )
 | 
			
		||||
              : const Icon(Icons.arrow_forward_ios_outlined),
 | 
			
		||||
        final space = spacesHierarchy[i];
 | 
			
		||||
        return SpacesDrawerEntry(
 | 
			
		||||
          entry: space,
 | 
			
		||||
          controller: controller,
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SpacesEntryMaybeChildren {
 | 
			
		||||
  final SpacesEntry spacesEntry;
 | 
			
		||||
 | 
			
		||||
  final Set<SpacesEntryMaybeChildren> children;
 | 
			
		||||
 | 
			
		||||
  const SpacesEntryMaybeChildren(this.spacesEntry, [this.children = const {}]);
 | 
			
		||||
 | 
			
		||||
  static SpacesEntryMaybeChildren? buildIfTopLevel(
 | 
			
		||||
      SpacesEntry entry, List<SpacesEntry> allEntries,
 | 
			
		||||
      [String? parent]) {
 | 
			
		||||
    if (entry is SpaceSpacesEntry) {
 | 
			
		||||
      final room = entry.space;
 | 
			
		||||
      if ((parent == null && room.spaceParents.isNotEmpty) ||
 | 
			
		||||
          (parent != null &&
 | 
			
		||||
              !room.spaceParents.any((element) => element.roomId == parent))) {
 | 
			
		||||
        return null;
 | 
			
		||||
      } else {
 | 
			
		||||
        final children = allEntries
 | 
			
		||||
            .where((element) =>
 | 
			
		||||
                element is SpaceSpacesEntry &&
 | 
			
		||||
                element.space.spaceParents.any((parent) =>
 | 
			
		||||
                    parent.roomId == room.id /*&& (parent.canonical ?? true)*/))
 | 
			
		||||
            .toList();
 | 
			
		||||
        return SpacesEntryMaybeChildren(
 | 
			
		||||
            entry,
 | 
			
		||||
            children
 | 
			
		||||
                .map((e) => buildIfTopLevel(e, allEntries, room.id))
 | 
			
		||||
                .whereNotNull()
 | 
			
		||||
                .toSet());
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      return SpacesEntryMaybeChildren(entry);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool isActiveOfChild(ChatListController controller) =>
 | 
			
		||||
      spacesEntry == controller.activeSpacesEntry ||
 | 
			
		||||
      children.any(
 | 
			
		||||
        (element) => element.isActiveOfChild(controller),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() => {
 | 
			
		||||
        'entry': spacesEntry is SpaceSpacesEntry
 | 
			
		||||
            ? (spacesEntry as SpaceSpacesEntry).space.id
 | 
			
		||||
            : spacesEntry.runtimeType.toString(),
 | 
			
		||||
        if (spacesEntry is SpaceSpacesEntry)
 | 
			
		||||
          'rawSpaceParents': (spacesEntry as SpaceSpacesEntry)
 | 
			
		||||
              .space
 | 
			
		||||
              .spaceParents
 | 
			
		||||
              .map((e) =>
 | 
			
		||||
                  {'roomId': e.roomId, 'canonical': e.canonical, 'via': e.via})
 | 
			
		||||
              .toList(),
 | 
			
		||||
        if (spacesEntry is SpaceSpacesEntry)
 | 
			
		||||
          'rawSpaceChildren': (spacesEntry as SpaceSpacesEntry)
 | 
			
		||||
              .space
 | 
			
		||||
              .spaceChildren
 | 
			
		||||
              .map(
 | 
			
		||||
                (e) => {
 | 
			
		||||
                  'roomId': e.roomId,
 | 
			
		||||
                  'suggested': e.suggested,
 | 
			
		||||
                  'via': e.via,
 | 
			
		||||
                  'order': e.order
 | 
			
		||||
                },
 | 
			
		||||
              )
 | 
			
		||||
              .toList(),
 | 
			
		||||
        'children': children.map((e) => e.toJson()).toList(),
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return jsonEncode(toJson());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										96
									
								
								lib/pages/chat_list/spaces_drawer_entry.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								lib/pages/chat_list/spaces_drawer_entry.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,96 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
 | 
			
		||||
import 'package:fluffychat/pages/chat_list/spaces_drawer.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/avatar.dart';
 | 
			
		||||
 | 
			
		||||
class SpacesDrawerEntry extends StatelessWidget {
 | 
			
		||||
  final SpacesEntryMaybeChildren entry;
 | 
			
		||||
  final ChatListController controller;
 | 
			
		||||
 | 
			
		||||
  const SpacesDrawerEntry(
 | 
			
		||||
      {Key? key, required this.entry, required this.controller})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final space = entry.spacesEntry;
 | 
			
		||||
    final room = space.getSpace(context);
 | 
			
		||||
 | 
			
		||||
    final active = controller.activeSpacesEntry == entry.spacesEntry;
 | 
			
		||||
    final leading = room == null
 | 
			
		||||
        ? CircleAvatar(
 | 
			
		||||
            child: space.getIcon(active),
 | 
			
		||||
            radius: Avatar.defaultSize / 2,
 | 
			
		||||
            backgroundColor: Theme.of(context).colorScheme.secondary,
 | 
			
		||||
            foregroundColor: Theme.of(context).colorScheme.onSecondary,
 | 
			
		||||
          )
 | 
			
		||||
        : Avatar(
 | 
			
		||||
            mxContent: room.avatar,
 | 
			
		||||
            name: space.getName(context),
 | 
			
		||||
          );
 | 
			
		||||
    final title = Text(
 | 
			
		||||
      space.getName(context),
 | 
			
		||||
      maxLines: 1,
 | 
			
		||||
      overflow: TextOverflow.ellipsis,
 | 
			
		||||
    );
 | 
			
		||||
    final subtitle = room?.topic.isEmpty ?? true
 | 
			
		||||
        ? null
 | 
			
		||||
        : Tooltip(
 | 
			
		||||
            message: room!.topic,
 | 
			
		||||
            child: Text(
 | 
			
		||||
              room.topic.replaceAll('\n', ' '),
 | 
			
		||||
              softWrap: false,
 | 
			
		||||
              overflow: TextOverflow.fade,
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
    void onTap() => controller.setActiveSpacesEntry(
 | 
			
		||||
          context,
 | 
			
		||||
          space,
 | 
			
		||||
        );
 | 
			
		||||
    final trailing = room != null
 | 
			
		||||
        ? SizedBox(
 | 
			
		||||
            width: 32,
 | 
			
		||||
            child: IconButton(
 | 
			
		||||
              splashRadius: 24,
 | 
			
		||||
              icon: const Icon(Icons.edit_outlined),
 | 
			
		||||
              tooltip: L10n.of(context)!.edit,
 | 
			
		||||
              onPressed: () => controller.editSpace(context, room.id),
 | 
			
		||||
            ),
 | 
			
		||||
          )
 | 
			
		||||
        : const Icon(Icons.arrow_forward_ios_outlined);
 | 
			
		||||
 | 
			
		||||
    if (entry.children.isEmpty) {
 | 
			
		||||
      return ListTile(
 | 
			
		||||
        selected: active,
 | 
			
		||||
        leading: leading,
 | 
			
		||||
        title: title,
 | 
			
		||||
        subtitle: subtitle,
 | 
			
		||||
        onTap: onTap,
 | 
			
		||||
        trailing: trailing,
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return ExpansionTile(
 | 
			
		||||
        leading: leading,
 | 
			
		||||
        initiallyExpanded:
 | 
			
		||||
            entry.children.any((element) => entry.isActiveOfChild(controller)),
 | 
			
		||||
        title: GestureDetector(
 | 
			
		||||
          onTap: onTap,
 | 
			
		||||
          child: Row(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
              mainAxisSize: MainAxisSize.max,
 | 
			
		||||
              children: [
 | 
			
		||||
                Expanded(child: title),
 | 
			
		||||
                const SizedBox(width: 8),
 | 
			
		||||
                trailing
 | 
			
		||||
              ]),
 | 
			
		||||
        ),
 | 
			
		||||
        children: entry.children
 | 
			
		||||
            .map((e) => SpacesDrawerEntry(entry: e, controller: controller))
 | 
			
		||||
            .toList(),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										146
									
								
								lib/pages/chat_list/spaces_hierarchy_proposal.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								lib/pages/chat_list/spaces_hierarchy_proposal.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,146 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:animations/animations.dart';
 | 
			
		||||
import 'package:async/async.dart';
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
import 'package:matrix/matrix.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/pages/chat_list/search_title.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/matrix.dart';
 | 
			
		||||
import 'recommended_room_list_item.dart';
 | 
			
		||||
 | 
			
		||||
class SpacesHierarchyProposals extends StatefulWidget {
 | 
			
		||||
  static final Map<String, AsyncCache<GetSpaceHierarchyResponse?>> _cache = {};
 | 
			
		||||
 | 
			
		||||
  final String? space;
 | 
			
		||||
  final String? query;
 | 
			
		||||
 | 
			
		||||
  const SpacesHierarchyProposals({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    required this.space,
 | 
			
		||||
    this.query,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<SpacesHierarchyProposals> createState() =>
 | 
			
		||||
      _SpacesHierarchyProposalsState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _SpacesHierarchyProposalsState extends State<SpacesHierarchyProposals> {
 | 
			
		||||
  @override
 | 
			
		||||
  void didUpdateWidget(covariant SpacesHierarchyProposals oldWidget) {
 | 
			
		||||
    if (oldWidget.space != widget.space || oldWidget.query != widget.query) {
 | 
			
		||||
      setState(() {});
 | 
			
		||||
    }
 | 
			
		||||
    super.didUpdateWidget(oldWidget);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    // check for recommended rooms in case the active space is a [SpaceSpacesEntry]
 | 
			
		||||
    if (widget.space != null) {
 | 
			
		||||
      final client = Matrix.of(context).client;
 | 
			
		||||
 | 
			
		||||
      final cache = SpacesHierarchyProposals._cache[widget.space!] ??=
 | 
			
		||||
          AsyncCache<GetSpaceHierarchyResponse?>(const Duration(minutes: 15));
 | 
			
		||||
 | 
			
		||||
      /// additionally saving the future's state in the completer in order to
 | 
			
		||||
      /// display the loading indicator when refreshing as a [FutureBuilder] is
 | 
			
		||||
      /// a [StatefulWidget].
 | 
			
		||||
      final completer = Completer();
 | 
			
		||||
      final future = cache.fetch(() => client.getSpaceHierarchy(
 | 
			
		||||
            widget.space!,
 | 
			
		||||
            suggestedOnly: true,
 | 
			
		||||
            maxDepth: 1,
 | 
			
		||||
          ));
 | 
			
		||||
      future.then(completer.complete);
 | 
			
		||||
 | 
			
		||||
      return FutureBuilder<GetSpaceHierarchyResponse?>(
 | 
			
		||||
        future: future,
 | 
			
		||||
        builder: (context, snapshot) {
 | 
			
		||||
          Widget child;
 | 
			
		||||
          if (snapshot.hasData) {
 | 
			
		||||
            final rooms = snapshot.data!.rooms.where(
 | 
			
		||||
              (element) =>
 | 
			
		||||
                  element.roomId != widget.space &&
 | 
			
		||||
                  // filtering in case a query is given
 | 
			
		||||
                  (widget.query != null
 | 
			
		||||
                      ? (element.name?.contains(widget.query!) ?? false) ||
 | 
			
		||||
                          (element.topic?.contains(widget.query!) ?? false)
 | 
			
		||||
                      // in case not, just leave it...
 | 
			
		||||
                      : true) &&
 | 
			
		||||
                  client.rooms
 | 
			
		||||
                      .any((knownRoom) => element.roomId != knownRoom.id),
 | 
			
		||||
            );
 | 
			
		||||
            if (rooms.isEmpty) child = const ListTile(key: ValueKey(false));
 | 
			
		||||
            child = Column(
 | 
			
		||||
              key: ValueKey(widget.space),
 | 
			
		||||
              mainAxisSize: MainAxisSize.min,
 | 
			
		||||
              children: [
 | 
			
		||||
                SearchTitle(
 | 
			
		||||
                  title: L10n.of(context)!.suggestedRooms,
 | 
			
		||||
                  icon: const Icon(Icons.auto_awesome_outlined),
 | 
			
		||||
                  trailing: completer.isCompleted
 | 
			
		||||
                      ? const Icon(
 | 
			
		||||
                          Icons.refresh_outlined,
 | 
			
		||||
                          size: 16,
 | 
			
		||||
                        )
 | 
			
		||||
                      : const SizedBox(
 | 
			
		||||
                          width: 16,
 | 
			
		||||
                          height: 16,
 | 
			
		||||
                          child: CircularProgressIndicator.adaptive(
 | 
			
		||||
                            strokeWidth: 1,
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                  onTap: () => setState(
 | 
			
		||||
                    () => SpacesHierarchyProposals._cache[widget.space!]!
 | 
			
		||||
                        .invalidate(),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                ...rooms.map(
 | 
			
		||||
                  (e) => RecommendedRoomListItem(
 | 
			
		||||
                    room: e,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            );
 | 
			
		||||
          } else {
 | 
			
		||||
            child = Column(
 | 
			
		||||
              key: const ValueKey(null),
 | 
			
		||||
              children: const [
 | 
			
		||||
                LinearProgressIndicator(),
 | 
			
		||||
                ListTile(),
 | 
			
		||||
              ],
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
          return PageTransitionSwitcher(
 | 
			
		||||
            // prevent the animation from re-building on dependency change
 | 
			
		||||
            key: ValueKey(widget.space),
 | 
			
		||||
            transitionBuilder: (
 | 
			
		||||
              Widget child,
 | 
			
		||||
              Animation<double> primaryAnimation,
 | 
			
		||||
              Animation<double> secondaryAnimation,
 | 
			
		||||
            ) {
 | 
			
		||||
              return SharedAxisTransition(
 | 
			
		||||
                animation: primaryAnimation,
 | 
			
		||||
                secondaryAnimation: secondaryAnimation,
 | 
			
		||||
                transitionType: SharedAxisTransitionType.scaled,
 | 
			
		||||
                child: child,
 | 
			
		||||
                fillColor: Colors.transparent,
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
            layoutBuilder: (children) => Stack(
 | 
			
		||||
              alignment: Alignment.topCenter,
 | 
			
		||||
              children: children,
 | 
			
		||||
            ),
 | 
			
		||||
            child: child,
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return Container();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -72,7 +72,7 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.1.0"
 | 
			
		||||
  async:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: async
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ dependencies:
 | 
			
		||||
  adaptive_dialog: ^1.5.1
 | 
			
		||||
  adaptive_theme: ^3.0.0
 | 
			
		||||
  animations: ^2.0.2
 | 
			
		||||
  async: ^2.8.2
 | 
			
		||||
  blurhash_dart: ^1.1.0
 | 
			
		||||
  cached_network_image: ^3.2.0
 | 
			
		||||
  callkeep: ^0.3.2
 | 
			
		||||
 | 
			
		||||
@ -157,7 +157,7 @@ diff --git a/pubspec.yaml b/pubspec.yaml
 | 
			
		||||
index 6999d0b8..b2c9144f 100644
 | 
			
		||||
--- a/pubspec.yaml
 | 
			
		||||
+++ b/pubspec.yaml
 | 
			
		||||
@@ -27,7 +27,7 @@ dependencies:
 | 
			
		||||
@@ -28,7 +28,7 @@ dependencies:
 | 
			
		||||
   emoji_proposal: ^0.0.1
 | 
			
		||||
   emojis: ^0.9.0
 | 
			
		||||
   encrypt: ^5.0.1
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user