alterações em telas
This commit is contained in:
@@ -8,7 +8,6 @@ import 'dart:async';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:tatchat/config/app_config.dart';
|
||||
import 'package:tatchat/config/themes.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list_view.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/home_tab.dart';
|
||||
@@ -101,13 +100,10 @@ class ChatListController extends State<ChatList>
|
||||
Future<void> setActiveSpace(String spaceId) async {
|
||||
await Matrix.of(context).client.getRoomById(spaceId)!.postLoad();
|
||||
if (!mounted) return;
|
||||
if (!FluffyThemes.isColumnMode(context) &&
|
||||
!AppSettings.displayNavigationRail.value) {
|
||||
await AppSettings.displayNavigationRail.setItem(true);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_activeSpaceId = spaceId;
|
||||
homeTab = HomeTab.spaces;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -304,14 +300,6 @@ class ChatListController extends State<ChatList>
|
||||
}
|
||||
}
|
||||
|
||||
void openNavrail() {
|
||||
setState(() {
|
||||
AppSettings.displayNavigationRail.setItem(
|
||||
!AppSettings.displayNavigationRail.value,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void startSearch() {
|
||||
setState(() {
|
||||
isSearchMode = true;
|
||||
@@ -355,10 +343,6 @@ class ChatListController extends State<ChatList>
|
||||
context.push('/rooms/$spaceId/details');
|
||||
}
|
||||
|
||||
// Needs to match GroupsSpacesEntry for 'separate group' checking.
|
||||
List<Room> get spaces =>
|
||||
Matrix.of(context).client.rooms.where((r) => r.isSpace).toList();
|
||||
|
||||
String? get activeChat => widget.activeChat;
|
||||
|
||||
void _processIncomingSharedMedia(List<SharedMediaFile> files) {
|
||||
@@ -423,6 +407,7 @@ class ChatListController extends State<ChatList>
|
||||
void initState() {
|
||||
_initReceiveSharingIntent();
|
||||
_activeSpaceId = widget.activeSpace;
|
||||
if (widget.activeSpace != null) homeTab = HomeTab.spaces;
|
||||
|
||||
scrollController.addListener(_onScroll);
|
||||
_waitForFirstSync();
|
||||
@@ -984,6 +969,7 @@ class ChatListController extends State<ChatList>
|
||||
setState(() {
|
||||
activeFilter = ActiveFilter.allChats;
|
||||
_activeSpaceId = null;
|
||||
homeTab = HomeTab.chats;
|
||||
Matrix.of(context).setActiveClient(client);
|
||||
});
|
||||
_clientStream.add(client);
|
||||
@@ -993,6 +979,7 @@ class ChatListController extends State<ChatList>
|
||||
context.go('/rooms');
|
||||
setState(() {
|
||||
_activeSpaceId = null;
|
||||
homeTab = HomeTab.chats;
|
||||
Matrix.of(context).activeBundle = bundle;
|
||||
if (!Matrix.of(
|
||||
context,
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'package:cupertino_ui/cupertino_ui.dart';
|
||||
import 'package:tatchat/config/setting_keys.dart';
|
||||
import 'package:tatchat/config/app_config.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:tatchat/pages/chat_list/dummy_chat_list_item.dart';
|
||||
import 'package:tatchat/pages/chat_list/search_title.dart';
|
||||
import 'package:tatchat/pages/chat_list/space_view.dart';
|
||||
import 'package:tatchat/utils/stream_extension.dart';
|
||||
import 'package:tatchat/widgets/adaptive_dialogs/public_room_dialog.dart';
|
||||
import 'package:tatchat/widgets/avatar.dart';
|
||||
@@ -20,7 +19,6 @@ 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;
|
||||
@@ -32,16 +30,6 @@ class ChatListViewBody extends StatelessWidget {
|
||||
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: controller.onChatTap,
|
||||
activeChat: controller.activeChat,
|
||||
);
|
||||
}
|
||||
final spaces = client.rooms.where((r) => r.isSpace);
|
||||
final spaceDelegateCandidates = <String, Room>{};
|
||||
for (final space in spaces) {
|
||||
@@ -70,7 +58,9 @@ class ChatListViewBody extends StatelessWidget {
|
||||
final rooms = controller.filteredRooms
|
||||
.where(
|
||||
(room) =>
|
||||
!AppSettings.hideRoomsInSpaces.value ||
|
||||
!room.isSpace &&
|
||||
!room.tags.keys.contains(AppConfig.canalTag) &&
|
||||
!room.tags.keys.contains(AppConfig.muralTag) &&
|
||||
spaceDelegateCandidates[room.id] == null,
|
||||
)
|
||||
.toList();
|
||||
@@ -78,7 +68,6 @@ class ChatListViewBody extends StatelessWidget {
|
||||
return CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
slivers: [
|
||||
ChatListHeader(controller: controller),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
if (controller.isSearchMode) ...[
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
|
||||
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'package:tatchat/config/setting_keys.dart';
|
||||
import 'package:tatchat/config/themes.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list.dart';
|
||||
import 'package:tatchat/pages/chat_list/client_chooser_button.dart';
|
||||
import 'package:tatchat/utils/sync_status_localization.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../widgets/matrix.dart';
|
||||
|
||||
class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
final ChatListController controller;
|
||||
final bool globalSearch;
|
||||
|
||||
const ChatListHeader({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.globalSearch = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final client = Matrix.of(context).client;
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
return SliverAppBar(
|
||||
floating: true,
|
||||
toolbarHeight: 72,
|
||||
pinned: isColumnMode,
|
||||
scrolledUnderElevation: 0,
|
||||
shape: isColumnMode
|
||||
? Border(bottom: BorderSide(color: theme.dividerColor, width: 1))
|
||||
: null,
|
||||
backgroundColor: isColumnMode
|
||||
? theme.colorScheme.surface.withAlpha(240)
|
||||
: Colors.transparent,
|
||||
automaticallyImplyLeading: false,
|
||||
title: StreamBuilder(
|
||||
stream: client.onSyncStatus.stream,
|
||||
builder: (context, snapshot) {
|
||||
final status =
|
||||
client.onSyncStatus.value ??
|
||||
const SyncStatusUpdate(SyncStatus.waitingForResponse);
|
||||
final hide =
|
||||
client.onSync.value != null &&
|
||||
status.status != SyncStatus.error &&
|
||||
client.prevBatch != null;
|
||||
return TextField(
|
||||
controller: controller.searchController,
|
||||
focusNode: controller.searchFocusNode,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: (text) =>
|
||||
controller.onSearchEnter(text, globalSearch: globalSearch),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
hintText: hide
|
||||
? L10n.of(context).searchChatsRooms
|
||||
: status.calcLocalizedString(context),
|
||||
hintStyle: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
|
||||
prefixIcon: hide
|
||||
? controller.isSearchMode
|
||||
? IconButton(
|
||||
tooltip: L10n.of(context).cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: controller.cancelSearch,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
: isColumnMode ||
|
||||
(controller.spaces.isEmpty &&
|
||||
!AppSettings.displayNavigationRail.value)
|
||||
? IconButton(
|
||||
tooltip: L10n.of(context).search,
|
||||
onPressed: controller.startSearch,
|
||||
icon: Icon(
|
||||
Icons.search_outlined,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
)
|
||||
: IconButton(
|
||||
tooltip: L10n.of(context).displayNavigationRail,
|
||||
onPressed: controller.openNavrail,
|
||||
icon: Icon(
|
||||
Icons.menu,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: 8,
|
||||
height: 8,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
value: status.progress,
|
||||
),
|
||||
),
|
||||
),
|
||||
suffixIcon: controller.isSearchMode && globalSearch
|
||||
? controller.isSearching
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10.0,
|
||||
horizontal: 12,
|
||||
),
|
||||
child: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
: TextButton.icon(
|
||||
onPressed: controller.setServer,
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
icon: const Icon(Icons.edit_outlined, size: 16),
|
||||
label: Text(
|
||||
controller.searchServer ??
|
||||
Matrix.of(context).client.homeserver!.host,
|
||||
maxLines: 2,
|
||||
),
|
||||
)
|
||||
: SizedBox(width: 0, child: ClientChooserButton(controller)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(56);
|
||||
}
|
||||
@@ -4,14 +4,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:tatchat/config/app_config.dart';
|
||||
import 'package:tatchat/config/setting_keys.dart';
|
||||
import 'package:tatchat/config/themes.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list.dart';
|
||||
import 'package:tatchat/pages/chat_list/navigation_rail.dart';
|
||||
import 'package:tatchat/pages/chat_list/start_chat_fab.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/channels_tab.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/chat_tab_bar.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/chat_tab_header.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/home_tab.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/murals_tab.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/spaces_tab.dart';
|
||||
@@ -25,23 +23,12 @@ class ChatListView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final oneColumnSpacesMode =
|
||||
!FluffyThemes.isColumnMode(context) &&
|
||||
AppSettings.displayNavigationRail.value;
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
final homeTab = controller.homeTab;
|
||||
final showSpacesMode = homeTab == HomeTab.chats;
|
||||
final showNavigationRail =
|
||||
showSpacesMode &&
|
||||
(isColumnMode || AppSettings.displayNavigationRail.value);
|
||||
return PopScope(
|
||||
canPop: !controller.isSearchMode && controller.activeSpaceId == null,
|
||||
canPop: !controller.isSearchMode,
|
||||
onPopInvokedWithResult: (pop, _) {
|
||||
if (pop) return;
|
||||
if (controller.activeSpaceId != null) {
|
||||
controller.clearActiveSpace();
|
||||
return;
|
||||
}
|
||||
if (controller.isSearchMode) {
|
||||
controller.cancelSearch();
|
||||
return;
|
||||
@@ -56,91 +43,46 @@ class ChatListView extends StatelessWidget {
|
||||
onTabSelected: controller.setHomeTab,
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: showNavigationRail
|
||||
? SpacesNavigationRail(
|
||||
activeSpaceId: controller.activeSpaceId,
|
||||
onGoToChats: controller.clearActiveSpace,
|
||||
onGoToSpaceId: controller.setActiveSpace,
|
||||
)
|
||||
: SizedBox(
|
||||
width: 0,
|
||||
height: MediaQuery.sizeOf(context).height,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showNavigationRail && isColumnMode)
|
||||
Container(width: 1, color: Theme.of(context).dividerColor),
|
||||
|
||||
Expanded(
|
||||
child: Scaffold(
|
||||
backgroundColor: showSpacesMode && oneColumnSpacesMode
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: null,
|
||||
body: SafeArea(
|
||||
top: showSpacesMode && oneColumnSpacesMode,
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
child: IndexedStack(
|
||||
index: homeTab.index,
|
||||
children: [
|
||||
Material(
|
||||
clipBehavior: showSpacesMode && oneColumnSpacesMode
|
||||
? Clip.hardEdge
|
||||
: Clip.none,
|
||||
borderRadius:
|
||||
showSpacesMode && oneColumnSpacesMode
|
||||
? BorderRadius.only(
|
||||
topLeft: Radius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
color: showSpacesMode && oneColumnSpacesMode
|
||||
? Theme.of(context).colorScheme.surface
|
||||
: null,
|
||||
child: ChatListViewBody(controller),
|
||||
),
|
||||
ChannelsTab(controller),
|
||||
SpacesTab(controller),
|
||||
MuralsTab(),
|
||||
],
|
||||
),
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
child: IndexedStack(
|
||||
index: homeTab.index,
|
||||
children: [
|
||||
ChatTabScaffold(
|
||||
tab: HomeTab.chats,
|
||||
isSearchMode: controller.isSearchMode,
|
||||
onSearchPressed: controller.startSearch,
|
||||
onSearchCancelled: () => controller.cancelSearch(),
|
||||
searchController: controller.searchController,
|
||||
searchFocusNode: controller.searchFocusNode,
|
||||
onSearchChanged: (text) =>
|
||||
controller.onSearchEnter(text, globalSearch: true),
|
||||
controller: controller,
|
||||
child: ChatListViewBody(controller),
|
||||
),
|
||||
floatingActionButton:
|
||||
showSpacesMode &&
|
||||
!controller.isSearchMode &&
|
||||
controller.activeSpaceId == null &&
|
||||
!isColumnMode
|
||||
? ValueListenableBuilder(
|
||||
valueListenable: controller.scrolledToTop,
|
||||
builder: (context, scrolledToTop, _) => StartChatFab(
|
||||
extended:
|
||||
scrolledToTop &&
|
||||
!AppSettings.displayNavigationRail.value,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
bottomNavigationBar: isColumnMode
|
||||
? null
|
||||
: ChatBottomTabBar(
|
||||
currentTab: homeTab,
|
||||
onTabSelected: controller.setHomeTab,
|
||||
),
|
||||
),
|
||||
ChannelsTab(controller),
|
||||
SpacesTab(controller),
|
||||
MuralsTab(controller),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: !controller.isSearchMode && !isColumnMode
|
||||
? const StartChatFab()
|
||||
: const SizedBox.shrink(),
|
||||
bottomNavigationBar: isColumnMode
|
||||
? null
|
||||
: ChatBottomTabBar(
|
||||
currentTab: homeTab,
|
||||
onTabSelected: controller.setHomeTab,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ class NaviRailItem extends StatelessWidget {
|
||||
borderRadius: borderRadius,
|
||||
color: isSelected
|
||||
? theme.colorScheme.secondaryContainer
|
||||
: theme.colorScheme.surfaceContainerHighest,
|
||||
: Colors.transparent,
|
||||
child: Tooltip(
|
||||
message: toolTip,
|
||||
child: InkWell(
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
|
||||
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'package:tatchat/config/app_config.dart';
|
||||
import 'package:tatchat/config/themes.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/chat_list/navi_rail_item.dart';
|
||||
import 'package:tatchat/pages/chat_list/start_chat_fab.dart';
|
||||
import 'package:tatchat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:tatchat/utils/stream_extension.dart';
|
||||
import 'package:tatchat/widgets/avatar.dart';
|
||||
import 'package:tatchat/widgets/matrix.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class SpacesNavigationRail extends StatelessWidget {
|
||||
final String? activeSpaceId;
|
||||
final void Function() onGoToChats;
|
||||
final void Function(String) onGoToSpaceId;
|
||||
|
||||
const SpacesNavigationRail({
|
||||
required this.activeSpaceId,
|
||||
required this.onGoToChats,
|
||||
required this.onGoToSpaceId,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
final coloredMode = !FluffyThemes.isColumnMode(context);
|
||||
final theme = Theme.of(context);
|
||||
return Material(
|
||||
color: coloredMode ? theme.colorScheme.surfaceContainer : null,
|
||||
child: SafeArea(
|
||||
child: StreamBuilder(
|
||||
key: ValueKey(client.userID.toString()),
|
||||
stream: client.onSync.stream
|
||||
.where((s) => s.hasRoomUpdate)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, _) {
|
||||
final allSpaces = client.rooms
|
||||
.where((room) => room.isSpace)
|
||||
.toList();
|
||||
|
||||
return SizedBox(
|
||||
width: FluffyThemes.isColumnMode(context)
|
||||
? FluffyThemes.navRailWidth
|
||||
: FluffyThemes.navRailWidth - 8,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.symmetric(vertical: 4),
|
||||
scrollDirection: Axis.vertical,
|
||||
itemCount: allSpaces.length + 2,
|
||||
itemBuilder: (context, i) {
|
||||
if (i == 0) {
|
||||
return NaviRailItem(
|
||||
isSelected: activeSpaceId == null,
|
||||
onTap: onGoToChats,
|
||||
icon: const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Icon(Icons.forum_outlined),
|
||||
),
|
||||
selectedIcon: const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Icon(Icons.forum),
|
||||
),
|
||||
toolTip: L10n.of(context).chats,
|
||||
unreadBadgeFilter: (room) => true,
|
||||
);
|
||||
}
|
||||
i--;
|
||||
if (i == allSpaces.length) {
|
||||
return NaviRailItem(
|
||||
isSelected: false,
|
||||
onTap: () => context.go('/rooms/newspace'),
|
||||
icon: const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
toolTip: L10n.of(context).createNewSpace,
|
||||
);
|
||||
}
|
||||
final space = allSpaces[i];
|
||||
final displayname = allSpaces[i]
|
||||
.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
);
|
||||
final spaceChildrenIds = space.spaceChildren
|
||||
.map((c) => c.roomId)
|
||||
.toSet();
|
||||
return NaviRailItem(
|
||||
toolTip: displayname,
|
||||
isSelected: activeSpaceId == space.id,
|
||||
onTap: () => onGoToSpaceId(allSpaces[i].id),
|
||||
unreadBadgeFilter: (room) =>
|
||||
spaceChildrenIds.contains(room.id),
|
||||
icon: Avatar(
|
||||
mxContent: allSpaces[i].avatar,
|
||||
name: displayname,
|
||||
//size: 36,
|
||||
shapeBorder: RoundedSuperellipseBorder(
|
||||
side: BorderSide(
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.spaceBorderRadius,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.spaceBorderRadius,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (FluffyThemes.isColumnMode(context))
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: StartChatFab(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import 'package:tatchat/widgets/avatar.dart';
|
||||
import 'package:tatchat/widgets/future_loading_dialog.dart';
|
||||
import 'package:tatchat/widgets/hover_builder.dart';
|
||||
import 'package:tatchat/widgets/matrix.dart';
|
||||
import 'package:tatchat/widgets/mxc_image.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:matrix/matrix.dart' as sdk;
|
||||
@@ -385,84 +386,113 @@ class _SpaceViewState extends State<SpaceView> {
|
||||
final filter = _filterController.text.trim().toLowerCase();
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
toolbarHeight: 72,
|
||||
floating: true,
|
||||
scrolledUnderElevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
automaticallyImplyLeading: false,
|
||||
title: TextField(
|
||||
controller: _filterController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
textInputAction: TextInputAction.search,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: L10n.of(context).search,
|
||||
hintStyle: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
prefixIcon: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
Icons.search_outlined,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
suffixIcon: PopupMenuButton<SpaceActions>(
|
||||
icon: Avatar(
|
||||
size: avatarSize,
|
||||
mxContent: room.avatar,
|
||||
name: displayname,
|
||||
shapeBorder: RoundedSuperellipseBorder(
|
||||
side: BorderSide(
|
||||
width: 1,
|
||||
color: theme.dividerColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.spaceBorderRadius,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.spaceBorderRadius,
|
||||
),
|
||||
),
|
||||
useRootNavigator: true,
|
||||
onSelected: _onSpaceAction,
|
||||
itemBuilder: (context) => [
|
||||
if (isAdmin)
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.addChild,
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.edit_square),
|
||||
title: Text(
|
||||
L10n.of(context).addChatOrSubSpace,
|
||||
SliverToBoxAdapter(
|
||||
child: _spaceCover(
|
||||
theme: theme,
|
||||
room: room,
|
||||
displayname: displayname,
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _filterController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
textInputAction: TextInputAction.search,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor:
|
||||
theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: L10n.of(context).search,
|
||||
hintStyle: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
floatingLabelBehavior:
|
||||
FloatingLabelBehavior.never,
|
||||
prefixIcon: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
Icons.search_outlined,
|
||||
color:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.settings,
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.settings_outlined),
|
||||
title: Text(L10n.of(context).settings),
|
||||
suffixIcon: PopupMenuButton<SpaceActions>(
|
||||
icon: Avatar(
|
||||
size: avatarSize,
|
||||
mxContent: room.avatar,
|
||||
name: displayname,
|
||||
shapeBorder: RoundedSuperellipseBorder(
|
||||
side: BorderSide(
|
||||
width: 1,
|
||||
color: theme.dividerColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.spaceBorderRadius,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.spaceBorderRadius,
|
||||
),
|
||||
),
|
||||
useRootNavigator: true,
|
||||
onSelected: _onSpaceAction,
|
||||
itemBuilder: (context) => [
|
||||
if (isAdmin)
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.addChild,
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.edit_square),
|
||||
title: Text(
|
||||
L10n.of(
|
||||
context,
|
||||
).addChatOrSubSpace,
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.settings,
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
Icons.settings_outlined,
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context).settings,
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.leave,
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete_outlined),
|
||||
title: Text(L10n.of(context).leave),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.leave,
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete_outlined),
|
||||
title: Text(L10n.of(context).leave),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (isAdmin)
|
||||
IconButton.filledTonal(
|
||||
tooltip: L10n.of(context).addChatOrSubSpace,
|
||||
onPressed: () => context.go(
|
||||
'/rooms/newgroup?space_id=${widget.spaceId}',
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -641,4 +671,90 @@ class _SpaceViewState extends State<SpaceView> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _spaceCover({
|
||||
required ThemeData theme,
|
||||
required sdk.Room room,
|
||||
required String displayname,
|
||||
}) {
|
||||
const coverHeight = 160.0;
|
||||
final avatar = room.avatar;
|
||||
return SizedBox(
|
||||
height: coverHeight,
|
||||
width: double.infinity,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
theme.colorScheme.primaryContainer,
|
||||
theme.colorScheme.secondaryContainer,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (avatar != null)
|
||||
MxcImage(
|
||||
uri: avatar,
|
||||
client: room.client,
|
||||
fit: BoxFit.cover,
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
height: coverHeight,
|
||||
),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withValues(alpha: 0.55),
|
||||
],
|
||||
stops: const [0.4, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 12,
|
||||
child: Row(
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: avatar,
|
||||
name: displayname,
|
||||
size: 56,
|
||||
shapeBorder: RoundedSuperellipseBorder(
|
||||
side: BorderSide(
|
||||
width: 2,
|
||||
color: theme.colorScheme.surface,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.spaceBorderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
displayname,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,33 +3,21 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'package:tatchat/config/themes.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
class StartChatFab extends StatelessWidget {
|
||||
final bool extended;
|
||||
const StartChatFab({this.extended = false, super.key});
|
||||
const StartChatFab({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FloatingActionButton.extended(
|
||||
return FloatingActionButton(
|
||||
heroTag: 'start_chat_fab',
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: () => context.go('/rooms/newprivatechat'),
|
||||
extendedIconLabelSpacing: extended ? 10 : 0,
|
||||
extendedPadding: extended
|
||||
? null
|
||||
: const EdgeInsets.symmetric(horizontal: 16),
|
||||
label: AnimatedSize(
|
||||
alignment: Alignment.centerLeft,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: extended ? Text(L10n.of(context).newChat) : SizedBox.shrink(),
|
||||
),
|
||||
icon: const Icon(Icons.edit_square),
|
||||
shape: const CircleBorder(),
|
||||
child: const Icon(Icons.add),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,75 +5,104 @@
|
||||
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:tatchat/config/app_config.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/chat_tab_header.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/empty_tab_view.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/home_tab.dart';
|
||||
import 'package:tatchat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:tatchat/utils/stream_extension.dart';
|
||||
import 'package:tatchat/widgets/matrix.dart';
|
||||
|
||||
class ChannelsTab extends StatelessWidget {
|
||||
class ChannelsTab extends StatefulWidget {
|
||||
final ChatListController controller;
|
||||
|
||||
const ChannelsTab(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
State<ChannelsTab> createState() => _ChannelsTabState();
|
||||
}
|
||||
|
||||
class _ChannelsTabState extends State<ChannelsTab> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
bool _isSearchMode = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
final theme = Theme.of(context);
|
||||
return StreamBuilder(
|
||||
key: ValueKey(client.userID.toString()),
|
||||
stream: client.onSync.stream
|
||||
.where((s) => s.hasRoomUpdate)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, _) {
|
||||
final channels = client.rooms
|
||||
.where((room) => !room.isSpace && !room.isDirectChat)
|
||||
.toList();
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.tag_outlined,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
L10n.of(context).channels,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (channels.isEmpty)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: EmptyTabView(
|
||||
icon: Icons.tag_outlined,
|
||||
title: L10n.of(context).noChannelsFound,
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverList.builder(
|
||||
itemCount: channels.length,
|
||||
itemBuilder: (context, i) {
|
||||
final room = channels[i];
|
||||
return ChatListItem(
|
||||
room,
|
||||
key: Key('channel_list_item_${room.id}'),
|
||||
onTap: () => controller.onChatTap(room),
|
||||
activeChat: controller.activeChat == room.id,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
final searchText = _searchController.text.trim().toLowerCase();
|
||||
return ChatTabScaffold(
|
||||
tab: HomeTab.channels,
|
||||
isSearchMode: _isSearchMode,
|
||||
onSearchPressed: () => setState(() => _isSearchMode = true),
|
||||
onSearchCancelled: () {
|
||||
setState(() {
|
||||
_isSearchMode = false;
|
||||
_searchController.clear();
|
||||
});
|
||||
},
|
||||
searchController: _searchController,
|
||||
searchFocusNode: _searchFocusNode,
|
||||
onSearchChanged: (_) => setState(() {}),
|
||||
controller: widget.controller,
|
||||
child: StreamBuilder(
|
||||
key: ValueKey(client.userID.toString()),
|
||||
stream: client.onSync.stream
|
||||
.where((s) => s.hasRoomUpdate)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, _) {
|
||||
final channels = client.rooms
|
||||
.where((room) => room.tags.keys.contains(AppConfig.canalTag))
|
||||
.where(
|
||||
(room) =>
|
||||
searchText.isEmpty ||
|
||||
room
|
||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)))
|
||||
.toLowerCase()
|
||||
.contains(searchText),
|
||||
)
|
||||
.toList();
|
||||
return ColoredBox(
|
||||
color: theme.colorScheme.surface,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (channels.isEmpty)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: EmptyTabView(
|
||||
icon: Icons.tag_outlined,
|
||||
title: L10n.of(context).noChannelsFound,
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverList.builder(
|
||||
itemCount: channels.length,
|
||||
itemBuilder: (context, i) {
|
||||
final room = channels[i];
|
||||
return ChatListItem(
|
||||
room,
|
||||
key: Key('channel_list_item_${room.id}'),
|
||||
onTap: () => widget.controller.onChatTap(room),
|
||||
activeChat: widget.controller.activeChat == room.id,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
139
lib/pages/chat_list/tabs/chat_tab_header.dart
Normal file
139
lib/pages/chat_list/tabs/chat_tab_header.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
|
||||
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list.dart';
|
||||
import 'package:tatchat/pages/chat_list/client_chooser_button.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/home_tab.dart';
|
||||
|
||||
class ChatTabHeader extends StatelessWidget {
|
||||
final HomeTab tab;
|
||||
final bool isSearchMode;
|
||||
final VoidCallback onSearchPressed;
|
||||
final VoidCallback onSearchCancelled;
|
||||
final TextEditingController searchController;
|
||||
final FocusNode searchFocusNode;
|
||||
final ValueChanged<String> onSearchChanged;
|
||||
final ChatListController controller;
|
||||
|
||||
const ChatTabHeader({
|
||||
super.key,
|
||||
required this.tab,
|
||||
required this.isSearchMode,
|
||||
required this.onSearchPressed,
|
||||
required this.onSearchCancelled,
|
||||
required this.searchController,
|
||||
required this.searchFocusNode,
|
||||
required this.onSearchChanged,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = L10n.of(context);
|
||||
return Material(
|
||||
color: theme.colorScheme.surface,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: theme.dividerColor)),
|
||||
),
|
||||
child: isSearchMode
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
focusNode: searchFocusNode,
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: onSearchChanged,
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.search,
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
prefixIcon: IconButton(
|
||||
tooltip: l10n.cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: onSearchCancelled,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
height: 56,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 12),
|
||||
Icon(tab.icon, size: 22, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(tab.label(context), style: theme.textTheme.titleLarge),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: l10n.search,
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: onSearchPressed,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ClientChooserButton(controller),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatTabScaffold extends StatelessWidget {
|
||||
final HomeTab tab;
|
||||
final bool isSearchMode;
|
||||
final VoidCallback onSearchPressed;
|
||||
final VoidCallback onSearchCancelled;
|
||||
final TextEditingController searchController;
|
||||
final FocusNode searchFocusNode;
|
||||
final ValueChanged<String> onSearchChanged;
|
||||
final ChatListController controller;
|
||||
final Widget child;
|
||||
|
||||
const ChatTabScaffold({
|
||||
super.key,
|
||||
required this.tab,
|
||||
required this.isSearchMode,
|
||||
required this.onSearchPressed,
|
||||
required this.onSearchCancelled,
|
||||
required this.searchController,
|
||||
required this.searchFocusNode,
|
||||
required this.onSearchChanged,
|
||||
required this.controller,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ChatTabHeader(
|
||||
tab: tab,
|
||||
isSearchMode: isSearchMode,
|
||||
onSearchPressed: onSearchPressed,
|
||||
onSearchCancelled: onSearchCancelled,
|
||||
searchController: searchController,
|
||||
searchFocusNode: searchFocusNode,
|
||||
onSearchChanged: onSearchChanged,
|
||||
controller: controller,
|
||||
),
|
||||
Expanded(child: child),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,43 +4,105 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:tatchat/config/app_config.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/chat_tab_header.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/empty_tab_view.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/home_tab.dart';
|
||||
import 'package:tatchat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:tatchat/utils/stream_extension.dart';
|
||||
import 'package:tatchat/widgets/matrix.dart';
|
||||
|
||||
class MuralsTab extends StatelessWidget {
|
||||
const MuralsTab({super.key});
|
||||
class MuralsTab extends StatefulWidget {
|
||||
final ChatListController controller;
|
||||
|
||||
const MuralsTab(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
State<MuralsTab> createState() => _MuralsTabState();
|
||||
}
|
||||
|
||||
class _MuralsTabState extends State<MuralsTab> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
bool _isSearchMode = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
return ColoredBox(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.dashboard_outlined,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
final client = Matrix.of(context).client;
|
||||
final theme = Theme.of(context);
|
||||
final searchText = _searchController.text.trim().toLowerCase();
|
||||
return ChatTabScaffold(
|
||||
tab: HomeTab.murals,
|
||||
isSearchMode: _isSearchMode,
|
||||
onSearchPressed: () => setState(() => _isSearchMode = true),
|
||||
onSearchCancelled: () {
|
||||
setState(() {
|
||||
_isSearchMode = false;
|
||||
_searchController.clear();
|
||||
});
|
||||
},
|
||||
searchController: _searchController,
|
||||
searchFocusNode: _searchFocusNode,
|
||||
onSearchChanged: (_) => setState(() {}),
|
||||
controller: widget.controller,
|
||||
child: StreamBuilder(
|
||||
key: ValueKey(client.userID.toString()),
|
||||
stream: client.onSync.stream
|
||||
.where((s) => s.hasRoomUpdate)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, _) {
|
||||
final murals = client.rooms
|
||||
.where((room) => room.tags.keys.contains(AppConfig.muralTag))
|
||||
.where(
|
||||
(room) =>
|
||||
searchText.isEmpty ||
|
||||
room
|
||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)))
|
||||
.toLowerCase()
|
||||
.contains(searchText),
|
||||
)
|
||||
.toList();
|
||||
return ColoredBox(
|
||||
color: theme.colorScheme.surface,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (murals.isEmpty)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: EmptyTabView(
|
||||
icon: Icons.dashboard_outlined,
|
||||
title: L10n.of(context).noMuralsFound,
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverList.builder(
|
||||
itemCount: murals.length,
|
||||
itemBuilder: (context, i) {
|
||||
final room = murals[i];
|
||||
return ChatListItem(
|
||||
room,
|
||||
key: Key('mural_list_item_${room.id}'),
|
||||
onTap: () => widget.controller.onChatTap(room),
|
||||
activeChat: widget.controller.activeChat == room.id,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(l10n.murals, style: Theme.of(context).textTheme.titleLarge),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: EmptyTabView(
|
||||
icon: Icons.dashboard_outlined,
|
||||
title: l10n.muralsComingSoon,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,84 +6,181 @@
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:tatchat/config/app_config.dart';
|
||||
import 'package:tatchat/config/themes.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list.dart';
|
||||
import 'package:tatchat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/empty_tab_view.dart';
|
||||
import 'package:tatchat/pages/chat_list/navi_rail_item.dart';
|
||||
import 'package:tatchat/pages/chat_list/space_view.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/chat_tab_header.dart';
|
||||
import 'package:tatchat/pages/chat_list/tabs/home_tab.dart';
|
||||
import 'package:tatchat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:tatchat/utils/stream_extension.dart';
|
||||
import 'package:tatchat/widgets/avatar.dart';
|
||||
import 'package:tatchat/widgets/matrix.dart';
|
||||
|
||||
class SpacesTab extends StatelessWidget {
|
||||
class SpacesTab extends StatefulWidget {
|
||||
final ChatListController controller;
|
||||
|
||||
const SpacesTab(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
State<SpacesTab> createState() => _SpacesTabState();
|
||||
}
|
||||
|
||||
class _SpacesTabState extends State<SpacesTab> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
bool _isSearchMode = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
final theme = Theme.of(context);
|
||||
return StreamBuilder(
|
||||
key: ValueKey(client.userID.toString()),
|
||||
stream: client.onSync.stream
|
||||
.where((s) => s.hasRoomUpdate)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, _) {
|
||||
final spaces = client.rooms.where((room) => room.isSpace).toList();
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.workspaces_outlined,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
L10n.of(context).spaces,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (spaces.isEmpty)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: EmptyTabView(
|
||||
icon: Icons.workspaces_outlined,
|
||||
title: L10n.of(context).noSpacesFound,
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverList.builder(
|
||||
itemCount: spaces.length + 1,
|
||||
itemBuilder: (context, i) {
|
||||
if (i == spaces.length) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.add),
|
||||
title: Text(L10n.of(context).createNewSpace),
|
||||
onTap: () => context.go('/rooms/newspace'),
|
||||
);
|
||||
}
|
||||
final space = spaces[i];
|
||||
return ChatListItem(
|
||||
space,
|
||||
key: Key('spaces_tab_item_${space.id}'),
|
||||
activeChat: controller.activeSpaceId == space.id,
|
||||
onTap: () {
|
||||
controller.setActiveSpace(space.id);
|
||||
controller.setHomeTab(HomeTab.chats);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
final searchText = _searchController.text.trim().toLowerCase();
|
||||
return ChatTabScaffold(
|
||||
tab: HomeTab.spaces,
|
||||
isSearchMode: _isSearchMode,
|
||||
onSearchPressed: () => setState(() => _isSearchMode = true),
|
||||
onSearchCancelled: () {
|
||||
setState(() {
|
||||
_isSearchMode = false;
|
||||
_searchController.clear();
|
||||
});
|
||||
},
|
||||
searchController: _searchController,
|
||||
searchFocusNode: _searchFocusNode,
|
||||
onSearchChanged: (_) => setState(() {}),
|
||||
controller: widget.controller,
|
||||
child: StreamBuilder(
|
||||
key: ValueKey(client.userID.toString()),
|
||||
stream: client.onSync.stream
|
||||
.where((s) => s.hasRoomUpdate)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, _) {
|
||||
final spaces = client.rooms
|
||||
.where((room) => room.isSpace)
|
||||
.where(
|
||||
(room) =>
|
||||
searchText.isEmpty ||
|
||||
room
|
||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)))
|
||||
.toLowerCase()
|
||||
.contains(searchText),
|
||||
)
|
||||
.toList();
|
||||
final activeSpaceId = widget.controller.activeSpaceId;
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: FluffyThemes.navRailWidth,
|
||||
child: ColoredBox(
|
||||
color: theme.colorScheme.surfaceContainerHigh,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
if (spaces.isEmpty)
|
||||
const Expanded(child: SizedBox.shrink())
|
||||
else
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
children: [
|
||||
for (final space in spaces)
|
||||
NaviRailItem(
|
||||
isSelected: activeSpaceId == space.id,
|
||||
onTap: () => widget.controller.setActiveSpace(
|
||||
space.id,
|
||||
),
|
||||
toolTip: space.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
),
|
||||
unreadBadgeFilter: (room) => space
|
||||
.spaceChildren
|
||||
.map((c) => c.roomId)
|
||||
.contains(room.id),
|
||||
icon: Avatar(
|
||||
mxContent: space.avatar,
|
||||
name: space.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
),
|
||||
shapeBorder: RoundedSuperellipseBorder(
|
||||
side: BorderSide(
|
||||
width: 1,
|
||||
color: theme.dividerColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.spaceBorderRadius,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.spaceBorderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
NaviRailItem(
|
||||
isSelected: false,
|
||||
onTap: () => context.go('/rooms/newspace'),
|
||||
icon: const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
toolTip: L10n.of(context).createNewSpace,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (FluffyThemes.isColumnMode(context))
|
||||
Container(width: 1, color: theme.dividerColor),
|
||||
Expanded(
|
||||
child: activeSpaceId == null
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.workspaces_outlined,
|
||||
size: 80,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
L10n.of(context).selectASpace,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SpaceView(
|
||||
key: ValueKey(activeSpaceId),
|
||||
spaceId: activeSpaceId,
|
||||
onBack: widget.controller.clearActiveSpace,
|
||||
onChatTab: widget.controller.onChatTap,
|
||||
activeChat: widget.controller.activeChat,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user