alterações em telas
This commit is contained in:
@@ -32,6 +32,8 @@ abstract class AppConfig {
|
||||
static const String appId = 'im.tatuchat.TatuChat';
|
||||
static const String appOpenUrlScheme = 'im.tatuchat';
|
||||
static const String appSsoUrlScheme = 'im.tatuchat.auth';
|
||||
static const String canalTag = 'tatuchat_canal';
|
||||
static const String muralTag = 'tatuchat_mural';
|
||||
|
||||
static const String sourceCodeUrl =
|
||||
'https://github.com/krille-chan/fluffychat';
|
||||
|
||||
@@ -217,6 +217,30 @@ abstract class AppRoutes {
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
),
|
||||
GoRoute(
|
||||
path: 'newcanal',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
NewGroup(
|
||||
createGroupType: CreateGroupType.canal,
|
||||
spaceId: state.uri.queryParameters['space_id'],
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
),
|
||||
GoRoute(
|
||||
path: 'newmural',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
NewGroup(
|
||||
createGroupType: CreateGroupType.mural,
|
||||
spaceId: state.uri.queryParameters['space_id'],
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
),
|
||||
ShellRoute(
|
||||
pageBuilder: (context, state, child) => defaultPageBuilder(
|
||||
context,
|
||||
|
||||
@@ -679,6 +679,15 @@
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"canal": "Canal",
|
||||
"canalIsPublic": "Canal is public",
|
||||
"canalName": "Canal name",
|
||||
"canalOnlyAdminCanPost": "Only canal admins are allowed to post in this room",
|
||||
"createCanalAndInviteUsers": "Create a canal and invite users",
|
||||
"createMuralAndInviteUsers": "Create a mural and invite users",
|
||||
"mural": "Mural",
|
||||
"muralIsPublic": "Mural is public",
|
||||
"muralName": "Mural name",
|
||||
"currentlyActive": "Currently active",
|
||||
"@currentlyActive": {
|
||||
"type": "String",
|
||||
@@ -2025,6 +2034,8 @@
|
||||
"whyIsThisMessageEncrypted": "Why is this message unreadable?",
|
||||
"noKeyForThisMessage": "This can happen if the message was sent before you have signed in to your account at this device.\n\nIt is also possible that the sender has blocked your device or something went wrong with the internet connection.\n\nAre you able to read the message on another session? Then you can transfer the message from it! Go to Settings > Devices and make sure that your devices have verified each other. When you open the room the next time and both sessions are in the foreground, the keys will be transmitted automatically.\n\nDo you not want to lose the keys when logging out or switching devices? Make sure that you have enabled the chat backup in the settings.",
|
||||
"newSpace": "New space",
|
||||
"newCanal": "New canal",
|
||||
"newMural": "New mural",
|
||||
"doNotShowAgain": "Do not show again",
|
||||
"wasDirectChatDisplayName": "Empty chat (was {oldDisplayName})",
|
||||
"@wasDirectChatDisplayName": {
|
||||
@@ -2036,6 +2047,8 @@
|
||||
}
|
||||
},
|
||||
"newSpaceDescription": "Spaces allows you to consolidate your chats and build private or public communities.",
|
||||
"newCanalDescription": "A canal is a room where only admins can publish. Use it to broadcast official announcements.",
|
||||
"newMuralDescription": "A mural is a shared bulletin board where everyone can post. Use it to leave notes for your community.",
|
||||
"encryptThisChat": "Encrypt this chat",
|
||||
"disableEncryptionWarning": "For security reasons you can not disable encryption in a chat, where it has been enabled before.",
|
||||
"sorryThatsNotPossible": "Sorry... that is not possible",
|
||||
@@ -2845,5 +2858,15 @@
|
||||
"@noSpacesFound": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"noMuralsFound": "No murals here",
|
||||
"@noMuralsFound": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"selectASpace": "Select a space",
|
||||
"@selectASpace": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
@@ -1845,6 +1845,8 @@
|
||||
"user": "Usuário",
|
||||
"custom": "Personalizado",
|
||||
"newSpace": "Novo espaço",
|
||||
"newCanal": "Novo canal",
|
||||
"newMural": "Novo mural",
|
||||
"countFiles": "{count} arquivos",
|
||||
"@countFiles": {
|
||||
"placeholders": {
|
||||
@@ -1874,6 +1876,8 @@
|
||||
"unbanUserDescription": "O usuário poderá entrar novamente na conversa, caso tente.",
|
||||
"messagesStyle": "Mensagens:",
|
||||
"newSpaceDescription": "Os espaços permitem que você consolide suas conversas e construa comunidades públicas ou privadas.",
|
||||
"newCanalDescription": "O canal é uma sala onde somente administradores podem publicar. Use-o para transmitir comunicados oficiais.",
|
||||
"newMuralDescription": "O mural é um quadro de avisos onde todos podem publicar. Use-o para deixar recados para a sua comunidade.",
|
||||
"chatDescription": "Descrição da conversa",
|
||||
"encryptThisChat": "Criptografar esta conversa",
|
||||
"reopenChat": "Reabrir conversa",
|
||||
@@ -1963,6 +1967,15 @@
|
||||
"placeholders": {}
|
||||
},
|
||||
"createGroup": "Criar grupo",
|
||||
"canal": "Canal",
|
||||
"canalIsPublic": "O canal é público",
|
||||
"canalName": "Nome do canal",
|
||||
"canalOnlyAdminCanPost": "Somente administradores do canal podem publicar nesta sala",
|
||||
"createCanalAndInviteUsers": "Criar um canal e convidar pessoas",
|
||||
"createMuralAndInviteUsers": "Criar um mural e convidar pessoas",
|
||||
"mural": "Mural",
|
||||
"muralIsPublic": "O mural é público",
|
||||
"muralName": "Nome do mural",
|
||||
"noBackupWarning": "Atenção! Se não ativar o backup de conversas, você perderá acesso a suas mensagens criptografadas. É altamente recomendável ativar o backup antes de sair.",
|
||||
"kickUserDescription": "O usuário foi expulso da conversa, mas não banido. Em conversas públicas, o usuário pode entrar novamente a qualquer momento.",
|
||||
"invite": "Convidar",
|
||||
@@ -2497,5 +2510,15 @@
|
||||
"@noSpacesFound": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"noMuralsFound": "Nenhum mural por aqui",
|
||||
"@noMuralsFound": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
},
|
||||
"selectASpace": "Selecione um espaço",
|
||||
"@selectASpace": {
|
||||
"type": "String",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +119,8 @@ class IntroPage extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(128),
|
||||
child: Image.asset(
|
||||
'./assets/logo/mini/logo_mini.png',
|
||||
width: 128,
|
||||
height: 128,
|
||||
width: 228,
|
||||
height: 228,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:tatchat/config/app_config.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/new_group/new_group_view.dart';
|
||||
import 'package:tatchat/utils/file_selector.dart';
|
||||
@@ -127,6 +128,75 @@ class NewGroupController extends State<NewGroup> {
|
||||
context.pop<String>(spaceId);
|
||||
}
|
||||
|
||||
Future<void> _createCanal() async {
|
||||
if (!mounted) return;
|
||||
final client = Matrix.of(context).client;
|
||||
|
||||
final roomId = await client.createGroupChat(
|
||||
visibility: groupCanBeFound
|
||||
? sdk.Visibility.public
|
||||
: sdk.Visibility.private,
|
||||
preset: publicGroup
|
||||
? sdk.CreateRoomPreset.publicChat
|
||||
: sdk.CreateRoomPreset.privateChat,
|
||||
groupName: nameController.text.isNotEmpty ? nameController.text : null,
|
||||
powerLevelContentOverride: {
|
||||
// Only admins (power level 100) are allowed to post in a canal.
|
||||
'events_default': 100,
|
||||
},
|
||||
initialState: [
|
||||
if (avatar != null)
|
||||
sdk.StateEvent(
|
||||
type: sdk.EventTypes.RoomAvatar,
|
||||
content: {'url': avatarUrl.toString()},
|
||||
),
|
||||
],
|
||||
);
|
||||
final canal = client.getRoomById(roomId);
|
||||
if (canal != null) {
|
||||
await canal.addTag(AppConfig.canalTag);
|
||||
}
|
||||
await _addToSpace(roomId);
|
||||
if (!mounted) return;
|
||||
|
||||
context.go('/rooms/$roomId/invite');
|
||||
}
|
||||
|
||||
Future<void> _createMural() async {
|
||||
if (!mounted) return;
|
||||
final client = Matrix.of(context).client;
|
||||
|
||||
final roomId = await client.createGroupChat(
|
||||
visibility: groupCanBeFound
|
||||
? sdk.Visibility.public
|
||||
: sdk.Visibility.private,
|
||||
preset: publicGroup
|
||||
? sdk.CreateRoomPreset.publicChat
|
||||
: sdk.CreateRoomPreset.privateChat,
|
||||
groupName: nameController.text.isNotEmpty ? nameController.text : null,
|
||||
powerLevelContentOverride: publicGroup
|
||||
? null
|
||||
: {
|
||||
'events': {MatrixRtcCallMember.eventType: 0},
|
||||
},
|
||||
initialState: [
|
||||
if (avatar != null)
|
||||
sdk.StateEvent(
|
||||
type: sdk.EventTypes.RoomAvatar,
|
||||
content: {'url': avatarUrl.toString()},
|
||||
),
|
||||
],
|
||||
);
|
||||
final mural = client.getRoomById(roomId);
|
||||
if (mural != null) {
|
||||
await mural.addTag(AppConfig.muralTag);
|
||||
}
|
||||
await _addToSpace(roomId);
|
||||
if (!mounted) return;
|
||||
|
||||
context.go('/rooms/$roomId/invite');
|
||||
}
|
||||
|
||||
Future<void> _addToSpace(String roomId) async {
|
||||
final spaceId = widget.spaceId;
|
||||
if (spaceId != null) {
|
||||
@@ -164,6 +234,10 @@ class NewGroupController extends State<NewGroup> {
|
||||
await _createGroup();
|
||||
case CreateGroupType.space:
|
||||
await _createSpace();
|
||||
case CreateGroupType.canal:
|
||||
await _createCanal();
|
||||
case CreateGroupType.mural:
|
||||
await _createMural();
|
||||
}
|
||||
} catch (e, s) {
|
||||
sdk.Logs().d('Unable to create group', e, s);
|
||||
@@ -194,4 +268,4 @@ class NewGroupController extends State<NewGroup> {
|
||||
Widget build(BuildContext context) => NewGroupView(this);
|
||||
}
|
||||
|
||||
enum CreateGroupType { group, space }
|
||||
enum CreateGroupType { group, space, canal, mural }
|
||||
|
||||
@@ -16,6 +16,14 @@ class NewGroupView extends StatelessWidget {
|
||||
|
||||
const NewGroupView(this.controller, {super.key});
|
||||
|
||||
bool get isGroup => controller.createGroupType == CreateGroupType.group;
|
||||
|
||||
bool get isSpace => controller.createGroupType == CreateGroupType.space;
|
||||
|
||||
bool get isCanal => controller.createGroupType == CreateGroupType.canal;
|
||||
|
||||
bool get isMural => controller.createGroupType == CreateGroupType.mural;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -30,9 +38,13 @@ class NewGroupView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
controller.createGroupType == CreateGroupType.space
|
||||
isSpace
|
||||
? L10n.of(context).newSpace
|
||||
: L10n.of(context).createGroup,
|
||||
: isCanal
|
||||
? L10n.of(context).newCanal
|
||||
: isMural
|
||||
? L10n.of(context).newMural
|
||||
: L10n.of(context).createGroup,
|
||||
),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
@@ -41,19 +53,30 @@ class NewGroupView extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SegmentedButton<CreateGroupType>(
|
||||
selected: {controller.createGroupType},
|
||||
onSelectionChanged: controller.setCreateGroupType,
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: CreateGroupType.group,
|
||||
label: Text(L10n.of(context).group),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: CreateGroupType.space,
|
||||
label: Text(L10n.of(context).space),
|
||||
),
|
||||
],
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SegmentedButton<CreateGroupType>(
|
||||
selected: {controller.createGroupType},
|
||||
onSelectionChanged: controller.setCreateGroupType,
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: CreateGroupType.group,
|
||||
label: Text(L10n.of(context).group),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: CreateGroupType.space,
|
||||
label: Text(L10n.of(context).space),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: CreateGroupType.canal,
|
||||
label: Text(L10n.of(context).canal),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: CreateGroupType.mural,
|
||||
label: Text(L10n.of(context).mural),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -85,9 +108,13 @@ class NewGroupView extends StatelessWidget {
|
||||
readOnly: controller.loading,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.people_outlined),
|
||||
labelText: controller.createGroupType == CreateGroupType.space
|
||||
labelText: isSpace
|
||||
? L10n.of(context).spaceName
|
||||
: L10n.of(context).groupName,
|
||||
: isCanal
|
||||
? L10n.of(context).canalName
|
||||
: isMural
|
||||
? L10n.of(context).muralName
|
||||
: L10n.of(context).groupName,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -95,11 +122,15 @@ class NewGroupView extends StatelessWidget {
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
secondary: const Icon(Icons.public_outlined),
|
||||
title: Text(
|
||||
controller.createGroupType == CreateGroupType.space
|
||||
? L10n.of(context).spaceIsPublic
|
||||
: L10n.of(context).groupIsPublic,
|
||||
),
|
||||
title: Text(
|
||||
isSpace
|
||||
? L10n.of(context).spaceIsPublic
|
||||
: isCanal
|
||||
? L10n.of(context).canalIsPublic
|
||||
: isMural
|
||||
? L10n.of(context).muralIsPublic
|
||||
: L10n.of(context).groupIsPublic,
|
||||
),
|
||||
value: controller.publicGroup,
|
||||
onChanged: controller.loading ? null : controller.setPublicGroup,
|
||||
),
|
||||
@@ -144,7 +175,7 @@ class NewGroupView extends StatelessWidget {
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: controller.createGroupType == CreateGroupType.space
|
||||
child: !isGroup
|
||||
? ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
@@ -153,7 +184,29 @@ class NewGroupView extends StatelessWidget {
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Icon(Icons.info_outlined),
|
||||
),
|
||||
subtitle: Text(L10n.of(context).newSpaceDescription),
|
||||
subtitle: Text(
|
||||
isSpace
|
||||
? L10n.of(context).newSpaceDescription
|
||||
: isCanal
|
||||
? L10n.of(context).newCanalDescription
|
||||
: L10n.of(context).newMuralDescription,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: isCanal
|
||||
? ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
leading: Icon(
|
||||
Icons.admin_panel_settings_outlined,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
subtitle: Text(L10n.of(context).canalOnlyAdminCanPost),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
@@ -168,9 +221,15 @@ class NewGroupView extends StatelessWidget {
|
||||
child: controller.loading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(
|
||||
controller.createGroupType == CreateGroupType.space
|
||||
isSpace
|
||||
? L10n.of(context).createNewSpace
|
||||
: L10n.of(context).createGroupAndInviteUsers,
|
||||
: isCanal
|
||||
? L10n.of(context).createCanalAndInviteUsers
|
||||
: isMural
|
||||
? L10n.of(context)
|
||||
.createMuralAndInviteUsers
|
||||
: L10n.of(context)
|
||||
.createGroupAndInviteUsers,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -147,6 +147,39 @@ class NewPrivateChatView extends StatelessWidget {
|
||||
title: Text(L10n.of(context).createGroup),
|
||||
onTap: () => context.go('/rooms/newgroup'),
|
||||
),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.tertiaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onTertiaryContainer,
|
||||
child: const Icon(Icons.campaign_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).newCanal),
|
||||
onTap: () => context.go('/rooms/newcanal'),
|
||||
),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.tertiaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onTertiaryContainer,
|
||||
child: const Icon(Icons.workspaces_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).newSpace),
|
||||
onTap: () => context.go('/rooms/newspace'),
|
||||
),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.tertiaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onTertiaryContainer,
|
||||
child: const Icon(Icons.dashboard_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).newMural),
|
||||
onTap: () => context.go('/rooms/newmural'),
|
||||
),
|
||||
if (PlatformInfos.isMobile)
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
|
||||
Reference in New Issue
Block a user