mirror of
https://github.com/krille-chan/fluffychat
synced 2024-09-17 06:15:10 +00:00
feat: New spaces and chat list design
This commit is contained in:
parent
396a89657d
commit
5c23453e66
17 changed files with 1270 additions and 1582 deletions
|
@ -196,6 +196,19 @@
|
||||||
"supportedVersions": {}
|
"supportedVersions": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"countChatsAndCountParticipants": "{chats} chats and {participants} participants",
|
||||||
|
"@countChatsAndCountParticipants": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {
|
||||||
|
"chats": {},
|
||||||
|
"participants": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"noMoreChatsFound": "No more chats found...",
|
||||||
|
"joinedChats": "Joined chats",
|
||||||
|
"unread": "Unread",
|
||||||
|
"space": "Space",
|
||||||
|
"spaces": "Spaces",
|
||||||
"banFromChat": "Ban from chat",
|
"banFromChat": "Ban from chat",
|
||||||
"@banFromChat": {
|
"@banFromChat": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
|
|
@ -44,7 +44,6 @@ abstract class AppConfig {
|
||||||
static bool hideRedactedEvents = false;
|
static bool hideRedactedEvents = false;
|
||||||
static bool hideUnknownEvents = true;
|
static bool hideUnknownEvents = true;
|
||||||
static bool hideUnimportantStateEvents = true;
|
static bool hideUnimportantStateEvents = true;
|
||||||
static bool separateChatTypes = false;
|
|
||||||
static bool autoplayImages = true;
|
static bool autoplayImages = true;
|
||||||
static bool sendTypingNotifications = true;
|
static bool sendTypingNotifications = true;
|
||||||
static bool sendPublicReadReceipts = true;
|
static bool sendPublicReadReceipts = true;
|
||||||
|
|
|
@ -92,12 +92,8 @@ abstract class AppRoutes {
|
||||||
FluffyThemes.isColumnMode(context) &&
|
FluffyThemes.isColumnMode(context) &&
|
||||||
state.fullPath?.startsWith('/rooms/settings') == false
|
state.fullPath?.startsWith('/rooms/settings') == false
|
||||||
? TwoColumnLayout(
|
? TwoColumnLayout(
|
||||||
displayNavigationRail:
|
|
||||||
state.path?.startsWith('/rooms/settings') != true,
|
|
||||||
mainView: ChatList(
|
mainView: ChatList(
|
||||||
activeChat: state.pathParameters['roomid'],
|
activeChat: state.pathParameters['roomid'],
|
||||||
displayNavigationRail:
|
|
||||||
state.path?.startsWith('/rooms/settings') != true,
|
|
||||||
),
|
),
|
||||||
sideView: child,
|
sideView: child,
|
||||||
)
|
)
|
||||||
|
@ -175,7 +171,6 @@ abstract class AppRoutes {
|
||||||
? TwoColumnLayout(
|
? TwoColumnLayout(
|
||||||
mainView: const Settings(),
|
mainView: const Settings(),
|
||||||
sideView: child,
|
sideView: child,
|
||||||
displayNavigationRail: false,
|
|
||||||
)
|
)
|
||||||
: child,
|
: child,
|
||||||
),
|
),
|
||||||
|
|
|
@ -4,7 +4,6 @@ abstract class SettingKeys {
|
||||||
static const String hideUnknownEvents = 'chat.fluffy.hideUnknownEvents';
|
static const String hideUnknownEvents = 'chat.fluffy.hideUnknownEvents';
|
||||||
static const String hideUnimportantStateEvents =
|
static const String hideUnimportantStateEvents =
|
||||||
'chat.fluffy.hideUnimportantStateEvents';
|
'chat.fluffy.hideUnimportantStateEvents';
|
||||||
static const String separateChatTypes = 'chat.fluffy.separateChatTypes';
|
|
||||||
static const String sentry = 'sentry';
|
static const String sentry = 'sentry';
|
||||||
static const String theme = 'theme';
|
static const String theme = 'theme';
|
||||||
static const String amoledEnabled = 'amoled_enabled';
|
static const String amoledEnabled = 'amoled_enabled';
|
||||||
|
|
|
@ -10,12 +10,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:flutter_shortcuts/flutter_shortcuts.dart';
|
import 'package:flutter_shortcuts/flutter_shortcuts.dart';
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:matrix/matrix.dart' as sdk;
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
import 'package:uni_links/uni_links.dart';
|
import 'package:uni_links/uni_links.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/config/themes.dart';
|
import 'package:fluffychat/pages/chat/send_file_dialog.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
|
import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
|
||||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
|
@ -35,7 +36,6 @@ import 'package:fluffychat/utils/tor_stub.dart'
|
||||||
enum SelectMode {
|
enum SelectMode {
|
||||||
normal,
|
normal,
|
||||||
share,
|
share,
|
||||||
select,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PopupMenuAction {
|
enum PopupMenuAction {
|
||||||
|
@ -49,19 +49,32 @@ enum PopupMenuAction {
|
||||||
|
|
||||||
enum ActiveFilter {
|
enum ActiveFilter {
|
||||||
allChats,
|
allChats,
|
||||||
|
unread,
|
||||||
groups,
|
groups,
|
||||||
messages,
|
|
||||||
spaces,
|
spaces,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension LocalizedActiveFilter on ActiveFilter {
|
||||||
|
String toLocalizedString(BuildContext context) {
|
||||||
|
switch (this) {
|
||||||
|
case ActiveFilter.allChats:
|
||||||
|
return L10n.of(context)!.all;
|
||||||
|
case ActiveFilter.unread:
|
||||||
|
return L10n.of(context)!.unread;
|
||||||
|
case ActiveFilter.groups:
|
||||||
|
return L10n.of(context)!.groups;
|
||||||
|
case ActiveFilter.spaces:
|
||||||
|
return L10n.of(context)!.spaces;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ChatList extends StatefulWidget {
|
class ChatList extends StatefulWidget {
|
||||||
static BuildContext? contextForVoip;
|
static BuildContext? contextForVoip;
|
||||||
final bool displayNavigationRail;
|
|
||||||
final String? activeChat;
|
final String? activeChat;
|
||||||
|
|
||||||
const ChatList({
|
const ChatList({
|
||||||
super.key,
|
super.key,
|
||||||
this.displayNavigationRail = false,
|
|
||||||
required this.activeChat,
|
required this.activeChat,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -77,85 +90,240 @@ class ChatListController extends State<ChatList>
|
||||||
|
|
||||||
StreamSubscription? _intentUriStreamSubscription;
|
StreamSubscription? _intentUriStreamSubscription;
|
||||||
|
|
||||||
bool get displayNavigationBar =>
|
void createNewSpace() {
|
||||||
!FluffyThemes.isColumnMode(context) &&
|
context.push<String?>('/rooms/newspace');
|
||||||
(spaces.isNotEmpty || AppConfig.separateChatTypes);
|
|
||||||
|
|
||||||
String? activeSpaceId;
|
|
||||||
|
|
||||||
void resetActiveSpaceId() {
|
|
||||||
setState(() {
|
|
||||||
selectedRoomIds.clear();
|
|
||||||
activeSpaceId = null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setActiveSpace(String? spaceId) {
|
ActiveFilter activeFilter = ActiveFilter.allChats;
|
||||||
setState(() {
|
|
||||||
selectedRoomIds.clear();
|
|
||||||
activeSpaceId = spaceId;
|
|
||||||
activeFilter = ActiveFilter.spaces;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void createNewSpace() async {
|
String? _activeSpaceId;
|
||||||
final spaceId = await context.push<String?>('/rooms/newspace');
|
String? get activeSpaceId => _activeSpaceId;
|
||||||
if (spaceId != null) {
|
|
||||||
setActiveSpace(spaceId);
|
void setActiveSpace(String spaceId) => setState(() {
|
||||||
|
_activeSpaceId = spaceId;
|
||||||
|
});
|
||||||
|
void clearActiveSpace() => setState(() {
|
||||||
|
_activeSpaceId = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
void addChatAction() async {
|
||||||
|
if (activeSpaceId == null) {
|
||||||
|
context.go('/rooms/newprivatechat');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
int get selectedIndex {
|
final roomType = await showConfirmationDialog(
|
||||||
switch (activeFilter) {
|
context: context,
|
||||||
case ActiveFilter.allChats:
|
title: L10n.of(context)!.addChatOrSubSpace,
|
||||||
case ActiveFilter.messages:
|
actions: [
|
||||||
return 0;
|
AlertDialogAction(
|
||||||
case ActiveFilter.groups:
|
key: AddRoomType.subspace,
|
||||||
return 1;
|
label: L10n.of(context)!.createNewSpace,
|
||||||
case ActiveFilter.spaces:
|
),
|
||||||
return AppConfig.separateChatTypes ? 2 : 1;
|
AlertDialogAction(
|
||||||
}
|
key: AddRoomType.chat,
|
||||||
}
|
label: L10n.of(context)!.createGroup,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (roomType == null) return;
|
||||||
|
|
||||||
ActiveFilter getActiveFilterByDestination(int? i) {
|
final names = await showTextInputDialog(
|
||||||
switch (i) {
|
context: context,
|
||||||
case 1:
|
title: roomType == AddRoomType.subspace
|
||||||
if (AppConfig.separateChatTypes) {
|
? L10n.of(context)!.createNewSpace
|
||||||
return ActiveFilter.groups;
|
: L10n.of(context)!.createGroup,
|
||||||
|
textFields: [
|
||||||
|
DialogTextField(
|
||||||
|
hintText: roomType == AddRoomType.subspace
|
||||||
|
? L10n.of(context)!.spaceName
|
||||||
|
: L10n.of(context)!.groupName,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 1,
|
||||||
|
maxLength: 64,
|
||||||
|
validator: (text) {
|
||||||
|
if (text == null || text.isEmpty) {
|
||||||
|
return L10n.of(context)!.pleaseChoose;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DialogTextField(
|
||||||
|
hintText: L10n.of(context)!.chatDescription,
|
||||||
|
minLines: 4,
|
||||||
|
maxLines: 8,
|
||||||
|
maxLength: 255,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
okLabel: L10n.of(context)!.create,
|
||||||
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
|
);
|
||||||
|
if (names == null) return;
|
||||||
|
final client = Matrix.of(context).client;
|
||||||
|
final result = await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () async {
|
||||||
|
late final String roomId;
|
||||||
|
final activeSpace = client.getRoomById(activeSpaceId!)!;
|
||||||
|
await activeSpace.postLoad();
|
||||||
|
|
||||||
|
if (roomType == AddRoomType.subspace) {
|
||||||
|
roomId = await client.createSpace(
|
||||||
|
name: names.first,
|
||||||
|
topic: names.last.isEmpty ? null : names.last,
|
||||||
|
visibility: activeSpace.joinRules == JoinRules.public
|
||||||
|
? sdk.Visibility.public
|
||||||
|
: sdk.Visibility.private,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
roomId = await client.createGroupChat(
|
||||||
|
groupName: names.first,
|
||||||
|
preset: activeSpace.joinRules == JoinRules.public
|
||||||
|
? CreateRoomPreset.publicChat
|
||||||
|
: CreateRoomPreset.privateChat,
|
||||||
|
visibility: activeSpace.joinRules == JoinRules.public
|
||||||
|
? sdk.Visibility.public
|
||||||
|
: sdk.Visibility.private,
|
||||||
|
initialState: names.length > 1 && names.last.isNotEmpty
|
||||||
|
? [
|
||||||
|
sdk.StateEvent(
|
||||||
|
type: sdk.EventTypes.RoomTopic,
|
||||||
|
content: {'topic': names.last},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ActiveFilter.spaces;
|
await activeSpace.setSpaceChild(roomId);
|
||||||
case 2:
|
},
|
||||||
return ActiveFilter.spaces;
|
);
|
||||||
case 0:
|
if (result.error != null) return;
|
||||||
default:
|
}
|
||||||
if (AppConfig.separateChatTypes) {
|
|
||||||
return ActiveFilter.messages;
|
void onChatTap(Room room, BuildContext context) async {
|
||||||
}
|
if (room.isSpace) {
|
||||||
return ActiveFilter.allChats;
|
setActiveSpace(room.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (room.membership == Membership.invite) {
|
||||||
|
final inviterId =
|
||||||
|
room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId;
|
||||||
|
final inviteAction = await showModalActionSheet<InviteActions>(
|
||||||
|
context: context,
|
||||||
|
message: room.isDirectChat
|
||||||
|
? L10n.of(context)!.invitePrivateChat
|
||||||
|
: L10n.of(context)!.inviteGroupChat,
|
||||||
|
title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||||
|
actions: [
|
||||||
|
SheetAction(
|
||||||
|
key: InviteActions.accept,
|
||||||
|
label: L10n.of(context)!.accept,
|
||||||
|
icon: Icons.check_outlined,
|
||||||
|
isDefaultAction: true,
|
||||||
|
),
|
||||||
|
SheetAction(
|
||||||
|
key: InviteActions.decline,
|
||||||
|
label: L10n.of(context)!.decline,
|
||||||
|
icon: Icons.close_outlined,
|
||||||
|
isDestructiveAction: true,
|
||||||
|
),
|
||||||
|
SheetAction(
|
||||||
|
key: InviteActions.block,
|
||||||
|
label: L10n.of(context)!.block,
|
||||||
|
icon: Icons.block_outlined,
|
||||||
|
isDestructiveAction: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (inviteAction == null) return;
|
||||||
|
if (inviteAction == InviteActions.block) {
|
||||||
|
context.go('/rooms/settings/security/ignorelist', extra: inviterId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (inviteAction == InviteActions.decline) {
|
||||||
|
await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: room.leave,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final joinResult = await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () async {
|
||||||
|
final waitForRoom = room.client.waitForRoomInSync(
|
||||||
|
room.id,
|
||||||
|
join: true,
|
||||||
|
);
|
||||||
|
await room.join();
|
||||||
|
await waitForRoom;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (joinResult.error != null) return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void onDestinationSelected(int? i) {
|
if (room.membership == Membership.ban) {
|
||||||
setState(() {
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
selectedRoomIds.clear();
|
SnackBar(
|
||||||
activeFilter = getActiveFilterByDestination(i);
|
content: Text(L10n.of(context)!.youHaveBeenBannedFromThisChat),
|
||||||
});
|
),
|
||||||
}
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ActiveFilter activeFilter = AppConfig.separateChatTypes
|
if (room.membership == Membership.leave) {
|
||||||
? ActiveFilter.messages
|
context.go('/rooms/archive/${room.id}');
|
||||||
: ActiveFilter.allChats;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share content into this room
|
||||||
|
final shareContent = Matrix.of(context).shareContent;
|
||||||
|
if (shareContent != null) {
|
||||||
|
final shareFile = shareContent.tryGet<MatrixFile>('file');
|
||||||
|
if (shareContent.tryGet<String>('msgtype') == 'chat.fluffy.shared_file' &&
|
||||||
|
shareFile != null) {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: false,
|
||||||
|
builder: (c) => SendFileDialog(
|
||||||
|
files: [shareFile],
|
||||||
|
room: room,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Matrix.of(context).shareContent = null;
|
||||||
|
} else {
|
||||||
|
final consent = await showOkCancelAlertDialog(
|
||||||
|
context: context,
|
||||||
|
title: L10n.of(context)!.forward,
|
||||||
|
message: L10n.of(context)!.forwardMessageTo(
|
||||||
|
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||||
|
),
|
||||||
|
okLabel: L10n.of(context)!.forward,
|
||||||
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
|
);
|
||||||
|
if (consent == OkCancelResult.cancel) {
|
||||||
|
Matrix.of(context).shareContent = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (consent == OkCancelResult.ok) {
|
||||||
|
room.sendEvent(shareContent);
|
||||||
|
Matrix.of(context).shareContent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.go('/rooms/${room.id}');
|
||||||
|
}
|
||||||
|
|
||||||
bool Function(Room) getRoomFilterByActiveFilter(ActiveFilter activeFilter) {
|
bool Function(Room) getRoomFilterByActiveFilter(ActiveFilter activeFilter) {
|
||||||
switch (activeFilter) {
|
switch (activeFilter) {
|
||||||
case ActiveFilter.allChats:
|
case ActiveFilter.allChats:
|
||||||
return (room) => !room.isSpace;
|
return (room) => true;
|
||||||
case ActiveFilter.groups:
|
case ActiveFilter.groups:
|
||||||
return (room) => !room.isSpace && !room.isDirectChat;
|
return (room) => !room.isSpace && !room.isDirectChat;
|
||||||
case ActiveFilter.messages:
|
case ActiveFilter.unread:
|
||||||
return (room) => !room.isSpace && room.isDirectChat;
|
return (room) => room.isUnreadOrInvited;
|
||||||
case ActiveFilter.spaces:
|
case ActiveFilter.spaces:
|
||||||
return (r) => r.isSpace;
|
return (room) => room.isSpace;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,15 +499,11 @@ class ChatListController extends State<ChatList>
|
||||||
List<Room> get spaces =>
|
List<Room> get spaces =>
|
||||||
Matrix.of(context).client.rooms.where((r) => r.isSpace).toList();
|
Matrix.of(context).client.rooms.where((r) => r.isSpace).toList();
|
||||||
|
|
||||||
final selectedRoomIds = <String>{};
|
|
||||||
|
|
||||||
String? get activeChat => widget.activeChat;
|
String? get activeChat => widget.activeChat;
|
||||||
|
|
||||||
SelectMode get selectMode => Matrix.of(context).shareContent != null
|
SelectMode get selectMode => Matrix.of(context).shareContent != null
|
||||||
? SelectMode.share
|
? SelectMode.share
|
||||||
: selectedRoomIds.isEmpty
|
: SelectMode.normal;
|
||||||
? SelectMode.normal
|
|
||||||
: SelectMode.select;
|
|
||||||
|
|
||||||
void _processIncomingSharedFiles(List<SharedMediaFile> files) {
|
void _processIncomingSharedFiles(List<SharedMediaFile> files) {
|
||||||
if (files.isEmpty) return;
|
if (files.isEmpty) return;
|
||||||
|
@ -448,80 +612,67 @@ class ChatListController extends State<ChatList>
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleSelection(String roomId) {
|
void chatContextAction(Room room) async {
|
||||||
setState(
|
final action = await showModalActionSheet<ChatContextAction>(
|
||||||
() => selectedRoomIds.contains(roomId)
|
context: context,
|
||||||
? selectedRoomIds.remove(roomId)
|
title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||||
: selectedRoomIds.add(roomId),
|
actions: [
|
||||||
|
SheetAction(
|
||||||
|
key: ChatContextAction.markUnread,
|
||||||
|
icon: room.markedUnread
|
||||||
|
? Icons.mark_as_unread
|
||||||
|
: Icons.mark_as_unread_outlined,
|
||||||
|
label: room.markedUnread
|
||||||
|
? L10n.of(context)!.markAsRead
|
||||||
|
: L10n.of(context)!.unread,
|
||||||
|
),
|
||||||
|
SheetAction(
|
||||||
|
key: ChatContextAction.favorite,
|
||||||
|
icon: room.isFavourite ? Icons.pin : Icons.pin_outlined,
|
||||||
|
label: room.isFavourite
|
||||||
|
? L10n.of(context)!.unpin
|
||||||
|
: L10n.of(context)!.pin,
|
||||||
|
),
|
||||||
|
SheetAction(
|
||||||
|
key: ChatContextAction.mute,
|
||||||
|
icon: room.pushRuleState == PushRuleState.notify
|
||||||
|
? Icons.notifications_off_outlined
|
||||||
|
: Icons.notifications,
|
||||||
|
label: room.pushRuleState == PushRuleState.notify
|
||||||
|
? L10n.of(context)!.muteChat
|
||||||
|
: L10n.of(context)!.unmuteChat,
|
||||||
|
),
|
||||||
|
SheetAction(
|
||||||
|
isDestructiveAction: true,
|
||||||
|
key: ChatContextAction.leave,
|
||||||
|
icon: Icons.delete_outlined,
|
||||||
|
label: L10n.of(context)!.leave,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> toggleUnread() async {
|
if (action == null) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () {
|
||||||
final markUnread = anySelectedRoomNotMarkedUnread;
|
switch (action) {
|
||||||
final client = Matrix.of(context).client;
|
case ChatContextAction.favorite:
|
||||||
for (final roomId in selectedRoomIds) {
|
return room.setFavourite(!room.isFavourite);
|
||||||
final room = client.getRoomById(roomId)!;
|
case ChatContextAction.markUnread:
|
||||||
if (room.markedUnread == markUnread) continue;
|
return room.markUnread(!room.markedUnread);
|
||||||
await client.getRoomById(roomId)!.markUnread(markUnread);
|
case ChatContextAction.mute:
|
||||||
|
return room.setPushRuleState(
|
||||||
|
room.pushRuleState == PushRuleState.notify
|
||||||
|
? PushRuleState.mentionsOnly
|
||||||
|
: PushRuleState.notify,
|
||||||
|
);
|
||||||
|
case ChatContextAction.leave:
|
||||||
|
return room.leave();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
cancelAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> toggleFavouriteRoom() async {
|
|
||||||
await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () async {
|
|
||||||
final makeFavorite = anySelectedRoomNotFavorite;
|
|
||||||
final client = Matrix.of(context).client;
|
|
||||||
for (final roomId in selectedRoomIds) {
|
|
||||||
final room = client.getRoomById(roomId)!;
|
|
||||||
if (room.isFavourite == makeFavorite) continue;
|
|
||||||
await client.getRoomById(roomId)!.setFavourite(makeFavorite);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
cancelAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> toggleMuted() async {
|
|
||||||
await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () async {
|
|
||||||
final newState = anySelectedRoomNotMuted
|
|
||||||
? PushRuleState.mentionsOnly
|
|
||||||
: PushRuleState.notify;
|
|
||||||
final client = Matrix.of(context).client;
|
|
||||||
for (final roomId in selectedRoomIds) {
|
|
||||||
final room = client.getRoomById(roomId)!;
|
|
||||||
if (room.pushRuleState == newState) continue;
|
|
||||||
await client.getRoomById(roomId)!.setPushRuleState(newState);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
cancelAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> archiveAction() async {
|
|
||||||
final confirmed = await showOkCancelAlertDialog(
|
|
||||||
useRootNavigator: false,
|
|
||||||
context: context,
|
|
||||||
title: L10n.of(context)!.areYouSure,
|
|
||||||
okLabel: L10n.of(context)!.yes,
|
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
|
||||||
message: L10n.of(context)!.archiveRoomDescription,
|
|
||||||
) ==
|
|
||||||
OkCancelResult.ok;
|
|
||||||
if (!confirmed) return;
|
|
||||||
await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () => _archiveSelectedRooms(),
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void dismissStatusList() async {
|
void dismissStatusList() async {
|
||||||
|
@ -568,76 +719,6 @@ class ChatListController extends State<ChatList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _archiveSelectedRooms() async {
|
|
||||||
final client = Matrix.of(context).client;
|
|
||||||
while (selectedRoomIds.isNotEmpty) {
|
|
||||||
final roomId = selectedRoomIds.first;
|
|
||||||
try {
|
|
||||||
await client.getRoomById(roomId)!.leave();
|
|
||||||
} finally {
|
|
||||||
toggleSelection(roomId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addToSpace() async {
|
|
||||||
final selectedSpace = await showConfirmationDialog<String>(
|
|
||||||
context: context,
|
|
||||||
title: L10n.of(context)!.addToSpace,
|
|
||||||
message: L10n.of(context)!.addToSpaceDescription,
|
|
||||||
fullyCapitalizedForMaterial: false,
|
|
||||||
actions: Matrix.of(context)
|
|
||||||
.client
|
|
||||||
.rooms
|
|
||||||
.where((r) => r.isSpace)
|
|
||||||
.map(
|
|
||||||
(space) => AlertDialogAction(
|
|
||||||
key: space.id,
|
|
||||||
label: space
|
|
||||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
if (selectedSpace == null) return;
|
|
||||||
final result = await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () async {
|
|
||||||
final space = Matrix.of(context).client.getRoomById(selectedSpace)!;
|
|
||||||
if (space.canSendDefaultStates) {
|
|
||||||
for (final roomId in selectedRoomIds) {
|
|
||||||
await space.setSpaceChild(roomId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (result.error == null) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(L10n.of(context)!.chatHasBeenAddedToThisSpace),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() => selectedRoomIds.clear());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get anySelectedRoomNotMarkedUnread => selectedRoomIds.any(
|
|
||||||
(roomId) =>
|
|
||||||
!Matrix.of(context).client.getRoomById(roomId)!.markedUnread,
|
|
||||||
);
|
|
||||||
|
|
||||||
bool get anySelectedRoomNotFavorite => selectedRoomIds.any(
|
|
||||||
(roomId) => !Matrix.of(context).client.getRoomById(roomId)!.isFavourite,
|
|
||||||
);
|
|
||||||
|
|
||||||
bool get anySelectedRoomNotMuted => selectedRoomIds.any(
|
|
||||||
(roomId) =>
|
|
||||||
Matrix.of(context).client.getRoomById(roomId)!.pushRuleState ==
|
|
||||||
PushRuleState.notify,
|
|
||||||
);
|
|
||||||
|
|
||||||
bool waitForFirstSync = false;
|
bool waitForFirstSync = false;
|
||||||
|
|
||||||
Future<void> _waitForFirstSync() async {
|
Future<void> _waitForFirstSync() async {
|
||||||
|
@ -666,19 +747,20 @@ class ChatListController extends State<ChatList>
|
||||||
void cancelAction() {
|
void cancelAction() {
|
||||||
if (selectMode == SelectMode.share) {
|
if (selectMode == SelectMode.share) {
|
||||||
setState(() => Matrix.of(context).shareContent = null);
|
setState(() => Matrix.of(context).shareContent = null);
|
||||||
} else {
|
|
||||||
setState(() => selectedRoomIds.clear());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setActiveFilter(ActiveFilter filter) {
|
||||||
|
setState(() {
|
||||||
|
activeFilter = filter;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void setActiveClient(Client client) {
|
void setActiveClient(Client client) {
|
||||||
context.go('/rooms');
|
context.go('/rooms');
|
||||||
setState(() {
|
setState(() {
|
||||||
activeFilter = AppConfig.separateChatTypes
|
activeFilter = ActiveFilter.allChats;
|
||||||
? ActiveFilter.messages
|
_activeSpaceId = null;
|
||||||
: ActiveFilter.allChats;
|
|
||||||
activeSpaceId = null;
|
|
||||||
selectedRoomIds.clear();
|
|
||||||
Matrix.of(context).setActiveClient(client);
|
Matrix.of(context).setActiveClient(client);
|
||||||
});
|
});
|
||||||
_clientStream.add(client);
|
_clientStream.add(client);
|
||||||
|
@ -687,7 +769,7 @@ class ChatListController extends State<ChatList>
|
||||||
void setActiveBundle(String bundle) {
|
void setActiveBundle(String bundle) {
|
||||||
context.go('/rooms');
|
context.go('/rooms');
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedRoomIds.clear();
|
_activeSpaceId = null;
|
||||||
Matrix.of(context).activeBundle = bundle;
|
Matrix.of(context).activeBundle = bundle;
|
||||||
if (!Matrix.of(context)
|
if (!Matrix.of(context)
|
||||||
.currentBundle!
|
.currentBundle!
|
||||||
|
@ -780,3 +862,18 @@ class ChatListController extends State<ChatList>
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EditBundleAction { addToBundle, removeFromBundle }
|
enum EditBundleAction { addToBundle, removeFromBundle }
|
||||||
|
|
||||||
|
enum InviteActions {
|
||||||
|
accept,
|
||||||
|
decline,
|
||||||
|
block,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AddRoomType { chat, subspace }
|
||||||
|
|
||||||
|
enum ChatContextAction {
|
||||||
|
favorite,
|
||||||
|
markUnread,
|
||||||
|
mute,
|
||||||
|
leave,
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
@ -11,7 +10,6 @@ import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/space_view.dart';
|
import 'package:fluffychat/pages/chat_list/space_view.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/status_msg_list.dart';
|
import 'package:fluffychat/pages/chat_list/status_msg_list.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart';
|
|
||||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||||
import 'package:fluffychat/utils/stream_extension.dart';
|
import 'package:fluffychat/utils/stream_extension.dart';
|
||||||
|
@ -29,6 +27,17 @@ class ChatListViewBody extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final activeSpace = controller.activeSpaceId;
|
||||||
|
if (activeSpace != null) {
|
||||||
|
return SpaceView(
|
||||||
|
spaceId: activeSpace,
|
||||||
|
onBack: controller.clearActiveSpace,
|
||||||
|
onChatTab: (room) => controller.onChatTap(room, context),
|
||||||
|
onChatContext: (room) => controller.chatContextAction(room),
|
||||||
|
activeChat: controller.activeChat,
|
||||||
|
toParentSpace: controller.setActiveSpace,
|
||||||
|
);
|
||||||
|
}
|
||||||
final publicRooms = controller.roomSearchResult?.chunk
|
final publicRooms = controller.roomSearchResult?.chunk
|
||||||
.where((room) => room.roomType != 'm.space')
|
.where((room) => room.roomType != 'm.space')
|
||||||
.toList();
|
.toList();
|
||||||
|
@ -43,224 +52,281 @@ class ChatListViewBody extends StatelessWidget {
|
||||||
final subtitleColor =
|
final subtitleColor =
|
||||||
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50);
|
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50);
|
||||||
final filter = controller.searchController.text.toLowerCase();
|
final filter = controller.searchController.text.toLowerCase();
|
||||||
return PageTransitionSwitcher(
|
return StreamBuilder(
|
||||||
transitionBuilder: (
|
key: ValueKey(
|
||||||
Widget child,
|
client.userID.toString(),
|
||||||
Animation<double> primaryAnimation,
|
),
|
||||||
Animation<double> secondaryAnimation,
|
stream: client.onSync.stream
|
||||||
) {
|
.where((s) => s.hasRoomUpdate)
|
||||||
return SharedAxisTransition(
|
.rateLimit(const Duration(seconds: 1)),
|
||||||
animation: primaryAnimation,
|
builder: (context, _) {
|
||||||
secondaryAnimation: secondaryAnimation,
|
final rooms = controller.filteredRooms;
|
||||||
transitionType: SharedAxisTransitionType.vertical,
|
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
final spaces = rooms.where((r) => r.isSpace);
|
||||||
child: child,
|
final spaceDelegateCandidates = <String, Room>{};
|
||||||
);
|
for (final space in spaces) {
|
||||||
},
|
spaceDelegateCandidates[space.id] = space;
|
||||||
child: StreamBuilder(
|
for (final spaceChild in space.spaceChildren) {
|
||||||
key: ValueKey(
|
final roomId = spaceChild.roomId;
|
||||||
client.userID.toString() +
|
if (roomId == null) continue;
|
||||||
controller.activeFilter.toString() +
|
spaceDelegateCandidates[roomId] = space;
|
||||||
controller.activeSpaceId.toString(),
|
|
||||||
),
|
|
||||||
stream: client.onSync.stream
|
|
||||||
.where((s) => s.hasRoomUpdate)
|
|
||||||
.rateLimit(const Duration(seconds: 1)),
|
|
||||||
builder: (context, _) {
|
|
||||||
if (controller.activeFilter == ActiveFilter.spaces) {
|
|
||||||
return SpaceView(
|
|
||||||
controller,
|
|
||||||
scrollController: controller.scrollController,
|
|
||||||
key: Key(controller.activeSpaceId ?? 'Spaces'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
final rooms = controller.filteredRooms;
|
}
|
||||||
return SafeArea(
|
final spaceDelegates = <String>{};
|
||||||
child: CustomScrollView(
|
|
||||||
controller: controller.scrollController,
|
return SafeArea(
|
||||||
slivers: [
|
child: CustomScrollView(
|
||||||
ChatListHeader(controller: controller),
|
controller: controller.scrollController,
|
||||||
SliverList(
|
slivers: [
|
||||||
delegate: SliverChildListDelegate(
|
ChatListHeader(controller: controller),
|
||||||
[
|
SliverList(
|
||||||
if (controller.isSearchMode) ...[
|
delegate: SliverChildListDelegate(
|
||||||
SearchTitle(
|
[
|
||||||
title: L10n.of(context)!.publicRooms,
|
if (controller.isSearchMode) ...[
|
||||||
icon: const Icon(Icons.explore_outlined),
|
SearchTitle(
|
||||||
|
title: L10n.of(context)!.publicRooms,
|
||||||
|
icon: const Icon(Icons.explore_outlined),
|
||||||
|
),
|
||||||
|
PublicRoomsHorizontalList(publicRooms: publicRooms),
|
||||||
|
SearchTitle(
|
||||||
|
title: L10n.of(context)!.publicSpaces,
|
||||||
|
icon: const Icon(Icons.workspaces_outlined),
|
||||||
|
),
|
||||||
|
PublicRoomsHorizontalList(publicRooms: publicSpaces),
|
||||||
|
SearchTitle(
|
||||||
|
title: L10n.of(context)!.users,
|
||||||
|
icon: const Icon(Icons.group_outlined),
|
||||||
|
),
|
||||||
|
AnimatedContainer(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
decoration: const BoxDecoration(),
|
||||||
|
height: userSearchResult == null ||
|
||||||
|
userSearchResult.results.isEmpty
|
||||||
|
? 0
|
||||||
|
: 106,
|
||||||
|
duration: FluffyThemes.animationDuration,
|
||||||
|
curve: FluffyThemes.animationCurve,
|
||||||
|
child: userSearchResult == null
|
||||||
|
? null
|
||||||
|
: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: userSearchResult.results.length,
|
||||||
|
itemBuilder: (context, i) => _SearchItem(
|
||||||
|
title:
|
||||||
|
userSearchResult.results[i].displayName ??
|
||||||
|
userSearchResult
|
||||||
|
.results[i].userId.localpart ??
|
||||||
|
L10n.of(context)!.unknownDevice,
|
||||||
|
avatar: userSearchResult.results[i].avatarUrl,
|
||||||
|
onPressed: () => showAdaptiveBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (c) => UserBottomSheet(
|
||||||
|
profile: userSearchResult.results[i],
|
||||||
|
outerContext: context,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (!controller.isSearchMode && AppConfig.showPresences)
|
||||||
|
GestureDetector(
|
||||||
|
onLongPress: () => controller.dismissStatusList(),
|
||||||
|
child: StatusMessageList(
|
||||||
|
onStatusEdit: controller.setStatus,
|
||||||
),
|
),
|
||||||
PublicRoomsHorizontalList(publicRooms: publicRooms),
|
),
|
||||||
SearchTitle(
|
const ConnectionStatusHeader(),
|
||||||
title: L10n.of(context)!.publicSpaces,
|
AnimatedContainer(
|
||||||
icon: const Icon(Icons.workspaces_outlined),
|
height: controller.isTorBrowser ? 64 : 0,
|
||||||
|
duration: FluffyThemes.animationDuration,
|
||||||
|
curve: FluffyThemes.animationCurve,
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
decoration: const BoxDecoration(),
|
||||||
|
child: Material(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.vpn_key),
|
||||||
|
title: Text(L10n.of(context)!.dehydrateTor),
|
||||||
|
subtitle: Text(L10n.of(context)!.dehydrateTorLong),
|
||||||
|
trailing: const Icon(Icons.chevron_right_outlined),
|
||||||
|
onTap: controller.dehydrate,
|
||||||
),
|
),
|
||||||
PublicRoomsHorizontalList(publicRooms: publicSpaces),
|
),
|
||||||
SearchTitle(
|
),
|
||||||
title: L10n.of(context)!.users,
|
if (client.rooms.isNotEmpty && !controller.isSearchMode)
|
||||||
icon: const Icon(Icons.group_outlined),
|
SizedBox(
|
||||||
),
|
height: 44,
|
||||||
AnimatedContainer(
|
child: ListView(
|
||||||
clipBehavior: Clip.hardEdge,
|
padding: const EdgeInsets.symmetric(
|
||||||
decoration: const BoxDecoration(),
|
horizontal: 12.0,
|
||||||
height: userSearchResult == null ||
|
vertical: 6,
|
||||||
userSearchResult.results.isEmpty
|
),
|
||||||
? 0
|
shrinkWrap: true,
|
||||||
: 106,
|
scrollDirection: Axis.horizontal,
|
||||||
duration: FluffyThemes.animationDuration,
|
children: ActiveFilter.values
|
||||||
curve: FluffyThemes.animationCurve,
|
.map(
|
||||||
child: userSearchResult == null
|
(filter) => Padding(
|
||||||
? null
|
padding:
|
||||||
: ListView.builder(
|
const EdgeInsets.symmetric(horizontal: 4),
|
||||||
scrollDirection: Axis.horizontal,
|
child: InkWell(
|
||||||
itemCount: userSearchResult.results.length,
|
borderRadius: BorderRadius.circular(
|
||||||
itemBuilder: (context, i) => _SearchItem(
|
AppConfig.borderRadius,
|
||||||
title: userSearchResult
|
),
|
||||||
.results[i].displayName ??
|
onTap: () =>
|
||||||
userSearchResult
|
controller.setActiveFilter(filter),
|
||||||
.results[i].userId.localpart ??
|
child: Container(
|
||||||
L10n.of(context)!.unknownDevice,
|
padding: const EdgeInsets.symmetric(
|
||||||
avatar:
|
horizontal: 12,
|
||||||
userSearchResult.results[i].avatarUrl,
|
vertical: 6,
|
||||||
onPressed: () => showAdaptiveBottomSheet(
|
),
|
||||||
context: context,
|
decoration: BoxDecoration(
|
||||||
builder: (c) => UserBottomSheet(
|
color: filter == controller.activeFilter
|
||||||
profile: userSearchResult.results[i],
|
? Theme.of(context)
|
||||||
outerContext: context,
|
.colorScheme
|
||||||
|
.primary
|
||||||
|
: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppConfig.borderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
filter.toLocalizedString(context),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight:
|
||||||
|
filter == controller.activeFilter
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
|
color:
|
||||||
|
filter == controller.activeFilter
|
||||||
|
? Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimary
|
||||||
|
: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSecondaryContainer,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
],
|
.toList(),
|
||||||
if (!controller.isSearchMode &&
|
|
||||||
controller.activeFilter != ActiveFilter.groups &&
|
|
||||||
AppConfig.showPresences)
|
|
||||||
GestureDetector(
|
|
||||||
onLongPress: () => controller.dismissStatusList(),
|
|
||||||
child: StatusMessageList(
|
|
||||||
onStatusEdit: controller.setStatus,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const ConnectionStatusHeader(),
|
|
||||||
AnimatedContainer(
|
|
||||||
height: controller.isTorBrowser ? 64 : 0,
|
|
||||||
duration: FluffyThemes.animationDuration,
|
|
||||||
curve: FluffyThemes.animationCurve,
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
decoration: const BoxDecoration(),
|
|
||||||
child: Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: ListTile(
|
|
||||||
leading: const Icon(Icons.vpn_key),
|
|
||||||
title: Text(L10n.of(context)!.dehydrateTor),
|
|
||||||
subtitle: Text(L10n.of(context)!.dehydrateTorLong),
|
|
||||||
trailing: const Icon(Icons.chevron_right_outlined),
|
|
||||||
onTap: controller.dehydrate,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (controller.isSearchMode)
|
if (controller.isSearchMode)
|
||||||
SearchTitle(
|
SearchTitle(
|
||||||
title: L10n.of(context)!.chats,
|
title: L10n.of(context)!.chats,
|
||||||
icon: const Icon(Icons.forum_outlined),
|
icon: const Icon(Icons.forum_outlined),
|
||||||
|
),
|
||||||
|
if (client.prevBatch != null &&
|
||||||
|
rooms.isEmpty &&
|
||||||
|
!controller.isSearchMode) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(32.0),
|
||||||
|
child: Icon(
|
||||||
|
CupertinoIcons.chat_bubble_2,
|
||||||
|
size: 128,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
),
|
),
|
||||||
if (client.prevBatch != null &&
|
),
|
||||||
rooms.isEmpty &&
|
],
|
||||||
!controller.isSearchMode) ...[
|
],
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.all(32.0),
|
),
|
||||||
child: Icon(
|
if (client.prevBatch == null)
|
||||||
CupertinoIcons.chat_bubble_2,
|
SliverList(
|
||||||
size: 128,
|
delegate: SliverChildBuilderDelegate(
|
||||||
color:
|
(context, i) => Opacity(
|
||||||
Theme.of(context).colorScheme.onInverseSurface,
|
opacity: (dummyChatCount - i) / dummyChatCount,
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: titleColor,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 1,
|
||||||
|
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
title: Row(
|
||||||
],
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 14,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: titleColor,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 36),
|
||||||
|
Container(
|
||||||
|
height: 14,
|
||||||
|
width: 14,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: subtitleColor,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Container(
|
||||||
|
height: 14,
|
||||||
|
width: 14,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: subtitleColor,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: subtitleColor,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
height: 12,
|
||||||
|
margin: const EdgeInsets.only(right: 22),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
childCount: dummyChatCount,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (client.prevBatch == null)
|
if (client.prevBatch != null)
|
||||||
SliverList(
|
SliverList.builder(
|
||||||
delegate: SliverChildBuilderDelegate(
|
itemCount: rooms.length,
|
||||||
(context, i) => Opacity(
|
itemBuilder: (BuildContext context, int i) {
|
||||||
opacity: (dummyChatCount - i) / dummyChatCount,
|
var room = rooms[i];
|
||||||
child: ListTile(
|
if (controller.activeFilter != ActiveFilter.groups) {
|
||||||
leading: CircleAvatar(
|
final parent = room.isSpace
|
||||||
backgroundColor: titleColor,
|
? room
|
||||||
child: CircularProgressIndicator(
|
: spaceDelegateCandidates[room.id];
|
||||||
strokeWidth: 1,
|
if (parent != null) {
|
||||||
color:
|
if (spaceDelegates.contains(parent.id)) {
|
||||||
Theme.of(context).textTheme.bodyLarge!.color,
|
return const SizedBox.shrink();
|
||||||
),
|
}
|
||||||
),
|
spaceDelegates.add(parent.id);
|
||||||
title: Row(
|
room = parent;
|
||||||
children: [
|
}
|
||||||
Expanded(
|
}
|
||||||
child: Container(
|
|
||||||
height: 14,
|
return ChatListItem(
|
||||||
decoration: BoxDecoration(
|
room,
|
||||||
color: titleColor,
|
lastEventRoom: rooms[i],
|
||||||
borderRadius: BorderRadius.circular(3),
|
key: Key('chat_list_item_${room.id}'),
|
||||||
),
|
filter: filter,
|
||||||
),
|
onTap: () => controller.onChatTap(room, context),
|
||||||
),
|
onLongPress: () => controller.chatContextAction(room),
|
||||||
const SizedBox(width: 36),
|
activeChat: controller.activeChat == room.id,
|
||||||
Container(
|
);
|
||||||
height: 14,
|
},
|
||||||
width: 14,
|
),
|
||||||
decoration: BoxDecoration(
|
],
|
||||||
color: subtitleColor,
|
),
|
||||||
borderRadius: BorderRadius.circular(14),
|
);
|
||||||
),
|
},
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Container(
|
|
||||||
height: 14,
|
|
||||||
width: 14,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: subtitleColor,
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: subtitleColor,
|
|
||||||
borderRadius: BorderRadius.circular(3),
|
|
||||||
),
|
|
||||||
height: 12,
|
|
||||||
margin: const EdgeInsets.only(right: 22),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
childCount: dummyChatCount,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (client.prevBatch != null)
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: rooms.length,
|
|
||||||
itemBuilder: (BuildContext context, int i) {
|
|
||||||
return ChatListItem(
|
|
||||||
rooms[i],
|
|
||||||
key: Key('chat_list_item_${rooms[i].id}'),
|
|
||||||
filter: filter,
|
|
||||||
selected:
|
|
||||||
controller.selectedRoomIds.contains(rooms[i].id),
|
|
||||||
onTap: controller.selectMode == SelectMode.select
|
|
||||||
? () => controller.toggleSelection(rooms[i].id)
|
|
||||||
: () => onChatTap(rooms[i], context),
|
|
||||||
onLongPress: () =>
|
|
||||||
controller.toggleSelection(rooms[i].id),
|
|
||||||
activeChat: controller.activeChat == rooms[i].id,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,88 +43,77 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||||
L10n.of(context)!.share,
|
L10n.of(context)!.share,
|
||||||
key: const ValueKey(SelectMode.share),
|
key: const ValueKey(SelectMode.share),
|
||||||
)
|
)
|
||||||
: selectMode == SelectMode.select
|
: TextField(
|
||||||
? Text(
|
controller: controller.searchController,
|
||||||
controller.selectedRoomIds.length.toString(),
|
focusNode: controller.searchFocusNode,
|
||||||
key: const ValueKey(SelectMode.select),
|
textInputAction: TextInputAction.search,
|
||||||
)
|
onChanged: (text) => controller.onSearchEnter(
|
||||||
: TextField(
|
text,
|
||||||
controller: controller.searchController,
|
globalSearch: globalSearch,
|
||||||
focusNode: controller.searchFocusNode,
|
),
|
||||||
textInputAction: TextInputAction.search,
|
decoration: InputDecoration(
|
||||||
onChanged: (text) => controller.onSearchEnter(
|
fillColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
text,
|
border: OutlineInputBorder(
|
||||||
globalSearch: globalSearch,
|
borderSide: BorderSide.none,
|
||||||
),
|
borderRadius: BorderRadius.circular(99),
|
||||||
decoration: InputDecoration(
|
|
||||||
fillColor: Theme.of(context).colorScheme.secondaryContainer,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
borderRadius: BorderRadius.circular(99),
|
|
||||||
),
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
hintText: L10n.of(context)!.searchChatsRooms,
|
|
||||||
hintStyle: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
),
|
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
|
||||||
prefixIcon: controller.isSearchMode
|
|
||||||
? IconButton(
|
|
||||||
tooltip: L10n.of(context)!.cancel,
|
|
||||||
icon: const Icon(Icons.close_outlined),
|
|
||||||
onPressed: controller.cancelSearch,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
)
|
|
||||||
: IconButton(
|
|
||||||
onPressed: controller.startSearch,
|
|
||||||
icon: Icon(
|
|
||||||
Icons.search_outlined,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
hintText: L10n.of(context)!.searchChatsRooms,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
|
prefixIcon: controller.isSearchMode
|
||||||
|
? IconButton(
|
||||||
|
tooltip: L10n.of(context)!.cancel,
|
||||||
|
icon: const Icon(Icons.close_outlined),
|
||||||
|
onPressed: controller.cancelSearch,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
: IconButton(
|
||||||
|
onPressed: controller.startSearch,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.search_outlined,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
actions: selectMode == SelectMode.share
|
actions: selectMode == SelectMode.share
|
||||||
? [
|
? [
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -135,48 +124,7 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||||
child: ClientChooserButton(controller),
|
child: ClientChooserButton(controller),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: selectMode == SelectMode.select
|
: null,
|
||||||
? [
|
|
||||||
if (controller.spaces.isNotEmpty)
|
|
||||||
IconButton(
|
|
||||||
tooltip: L10n.of(context)!.addToSpace,
|
|
||||||
icon: const Icon(Icons.workspaces_outlined),
|
|
||||||
onPressed: controller.addToSpace,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip: L10n.of(context)!.toggleUnread,
|
|
||||||
icon: Icon(
|
|
||||||
controller.anySelectedRoomNotMarkedUnread
|
|
||||||
? Icons.mark_chat_unread_outlined
|
|
||||||
: Icons.mark_chat_read_outlined,
|
|
||||||
),
|
|
||||||
onPressed: controller.toggleUnread,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip: L10n.of(context)!.toggleFavorite,
|
|
||||||
icon: Icon(
|
|
||||||
controller.anySelectedRoomNotFavorite
|
|
||||||
? Icons.push_pin
|
|
||||||
: Icons.push_pin_outlined,
|
|
||||||
),
|
|
||||||
onPressed: controller.toggleFavouriteRoom,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
controller.anySelectedRoomNotMuted
|
|
||||||
? Icons.notifications_off_outlined
|
|
||||||
: Icons.notifications_outlined,
|
|
||||||
),
|
|
||||||
tooltip: L10n.of(context)!.toggleMuted,
|
|
||||||
onPressed: controller.toggleMuted,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.delete_outlined),
|
|
||||||
tooltip: L10n.of(context)!.archive,
|
|
||||||
onPressed: controller.archiveAction,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ enum ArchivedRoomAction { delete, rejoin }
|
||||||
|
|
||||||
class ChatListItem extends StatelessWidget {
|
class ChatListItem extends StatelessWidget {
|
||||||
final Room room;
|
final Room room;
|
||||||
|
final Room? lastEventRoom;
|
||||||
final bool activeChat;
|
final bool activeChat;
|
||||||
final bool selected;
|
|
||||||
final void Function()? onLongPress;
|
final void Function()? onLongPress;
|
||||||
final void Function()? onForget;
|
final void Function()? onForget;
|
||||||
final void Function() onTap;
|
final void Function() onTap;
|
||||||
|
@ -27,11 +27,11 @@ class ChatListItem extends StatelessWidget {
|
||||||
const ChatListItem(
|
const ChatListItem(
|
||||||
this.room, {
|
this.room, {
|
||||||
this.activeChat = false,
|
this.activeChat = false,
|
||||||
this.selected = false,
|
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
this.onForget,
|
this.onForget,
|
||||||
this.filter,
|
this.filter,
|
||||||
|
this.lastEventRoom,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -64,24 +64,23 @@ class ChatListItem extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isMuted = room.pushRuleState != PushRuleState.notify;
|
final isMuted = room.pushRuleState != PushRuleState.notify;
|
||||||
final typingText = room.getLocalizedTypingText(context);
|
final lastEventRoom = this.lastEventRoom ?? room;
|
||||||
final lastEvent = room.lastEvent;
|
final typingText = lastEventRoom.getLocalizedTypingText(context);
|
||||||
|
final lastEvent = lastEventRoom.lastEvent;
|
||||||
final ownMessage = lastEvent?.senderId == room.client.userID;
|
final ownMessage = lastEvent?.senderId == room.client.userID;
|
||||||
final unread = room.isUnread || room.membership == Membership.invite;
|
final unread =
|
||||||
|
lastEventRoom.isUnread || lastEventRoom.membership == Membership.invite;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final directChatMatrixId = room.directChatMatrixID;
|
final directChatMatrixId = room.directChatMatrixID;
|
||||||
final isDirectChat = directChatMatrixId != null;
|
final isDirectChat = directChatMatrixId != null;
|
||||||
final unreadBubbleSize = unread || room.hasNewMessages
|
final unreadBubbleSize = unread || lastEventRoom.hasNewMessages
|
||||||
? room.notificationCount > 0
|
? lastEventRoom.notificationCount > 0
|
||||||
? 20.0
|
? 20.0
|
||||||
: 14.0
|
: 14.0
|
||||||
: 0.0;
|
: 0.0;
|
||||||
final hasNotifications = room.notificationCount > 0;
|
final hasNotifications = lastEventRoom.notificationCount > 0;
|
||||||
final backgroundColor = selected
|
final backgroundColor =
|
||||||
? theme.colorScheme.primaryContainer
|
activeChat ? theme.colorScheme.secondaryContainer : null;
|
||||||
: activeChat
|
|
||||||
? theme.colorScheme.secondaryContainer
|
|
||||||
: null;
|
|
||||||
final displayname = room.getLocalizedDisplayname(
|
final displayname = room.getLocalizedDisplayname(
|
||||||
MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
);
|
);
|
||||||
|
@ -119,6 +118,9 @@ class ChatListItem extends StatelessWidget {
|
||||||
curve: FluffyThemes.animationCurve,
|
curve: FluffyThemes.animationCurve,
|
||||||
scale: hovered ? 1.1 : 1.0,
|
scale: hovered ? 1.1 : 1.0,
|
||||||
child: Avatar(
|
child: Avatar(
|
||||||
|
borderRadius: room.isSpace
|
||||||
|
? BorderRadius.circular(AppConfig.borderRadius / 3)
|
||||||
|
: null,
|
||||||
mxContent: room.avatar,
|
mxContent: room.avatar,
|
||||||
name: displayname,
|
name: displayname,
|
||||||
presenceUserId: directChatMatrixId,
|
presenceUserId: directChatMatrixId,
|
||||||
|
@ -133,14 +135,12 @@ class ChatListItem extends StatelessWidget {
|
||||||
child: AnimatedScale(
|
child: AnimatedScale(
|
||||||
duration: FluffyThemes.animationDuration,
|
duration: FluffyThemes.animationDuration,
|
||||||
curve: FluffyThemes.animationCurve,
|
curve: FluffyThemes.animationCurve,
|
||||||
scale: (hovered || selected) ? 1.0 : 0.0,
|
scale: (hovered) ? 1.0 : 0.0,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Icon(
|
child: const Icon(
|
||||||
selected
|
Icons.check_circle_outlined,
|
||||||
? Icons.check_circle
|
|
||||||
: Icons.check_circle_outlined,
|
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -180,7 +180,9 @@ class ChatListItem extends StatelessWidget {
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (lastEvent != null && room.membership != Membership.invite)
|
if (!room.isSpace &&
|
||||||
|
lastEvent != null &&
|
||||||
|
room.membership != Membership.invite)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 4.0),
|
padding: const EdgeInsets.only(left: 4.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
@ -193,11 +195,30 @@ class ChatListItem extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (room.isSpace)
|
||||||
|
const Icon(
|
||||||
|
Icons.arrow_circle_right_outlined,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
if (room.isSpace) ...[
|
||||||
|
room.id != lastEventRoom.id &&
|
||||||
|
lastEventRoom.isUnreadOrInvited
|
||||||
|
? Avatar(
|
||||||
|
mxContent: lastEventRoom.avatar,
|
||||||
|
name: lastEventRoom.name,
|
||||||
|
size: 18,
|
||||||
|
)
|
||||||
|
: const Icon(
|
||||||
|
Icons.workspaces_outlined,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
if (typingText.isEmpty &&
|
if (typingText.isEmpty &&
|
||||||
ownMessage &&
|
ownMessage &&
|
||||||
room.lastEvent!.status.isSending) ...[
|
room.lastEvent!.status.isSending) ...[
|
||||||
|
@ -222,62 +243,71 @@ class ChatListItem extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: typingText.isNotEmpty
|
child: room.isSpace && !lastEventRoom.isUnreadOrInvited
|
||||||
? Text(
|
? Text(
|
||||||
typingText,
|
L10n.of(context)!.countChatsAndCountParticipants(
|
||||||
style: TextStyle(
|
room.spaceChildren.length.toString(),
|
||||||
color: theme.colorScheme.primary,
|
(room.summary.mJoinedMemberCount ?? 1).toString(),
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
|
||||||
softWrap: false,
|
|
||||||
)
|
)
|
||||||
: FutureBuilder(
|
: typingText.isNotEmpty
|
||||||
key: ValueKey(
|
? Text(
|
||||||
'${lastEvent?.eventId}_${lastEvent?.type}',
|
typingText,
|
||||||
),
|
style: TextStyle(
|
||||||
future: needLastEventSender
|
color: theme.colorScheme.primary,
|
||||||
? lastEvent.calcLocalizedBody(
|
),
|
||||||
MatrixLocals(L10n.of(context)!),
|
maxLines: 1,
|
||||||
hideReply: true,
|
softWrap: false,
|
||||||
hideEdit: true,
|
)
|
||||||
plaintextBody: true,
|
: FutureBuilder(
|
||||||
removeMarkdown: true,
|
key: ValueKey(
|
||||||
withSenderNamePrefix: !isDirectChat ||
|
'${lastEvent?.eventId}_${lastEvent?.type}',
|
||||||
directChatMatrixId !=
|
),
|
||||||
room.lastEvent?.senderId,
|
future: needLastEventSender
|
||||||
)
|
? lastEvent.calcLocalizedBody(
|
||||||
: null,
|
MatrixLocals(L10n.of(context)!),
|
||||||
initialData: lastEvent?.calcLocalizedBodyFallback(
|
hideReply: true,
|
||||||
MatrixLocals(L10n.of(context)!),
|
hideEdit: true,
|
||||||
hideReply: true,
|
plaintextBody: true,
|
||||||
hideEdit: true,
|
removeMarkdown: true,
|
||||||
plaintextBody: true,
|
withSenderNamePrefix: (!isDirectChat ||
|
||||||
removeMarkdown: true,
|
directChatMatrixId !=
|
||||||
withSenderNamePrefix: !isDirectChat ||
|
room.lastEvent?.senderId),
|
||||||
directChatMatrixId !=
|
)
|
||||||
room.lastEvent?.senderId,
|
|
||||||
),
|
|
||||||
builder: (context, snapshot) => Text(
|
|
||||||
room.membership == Membership.invite
|
|
||||||
? isDirectChat
|
|
||||||
? L10n.of(context)!.invitePrivateChat
|
|
||||||
: L10n.of(context)!.inviteGroupChat
|
|
||||||
: snapshot.data ??
|
|
||||||
L10n.of(context)!.emptyChat,
|
|
||||||
softWrap: false,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: unread || room.hasNewMessages
|
|
||||||
? FontWeight.bold
|
|
||||||
: null,
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
decoration: room.lastEvent?.redacted == true
|
|
||||||
? TextDecoration.lineThrough
|
|
||||||
: null,
|
: null,
|
||||||
|
initialData:
|
||||||
|
lastEvent?.calcLocalizedBodyFallback(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
hideReply: true,
|
||||||
|
hideEdit: true,
|
||||||
|
plaintextBody: true,
|
||||||
|
removeMarkdown: true,
|
||||||
|
withSenderNamePrefix: (!isDirectChat ||
|
||||||
|
directChatMatrixId !=
|
||||||
|
room.lastEvent?.senderId),
|
||||||
|
),
|
||||||
|
builder: (context, snapshot) => Text(
|
||||||
|
room.membership == Membership.invite
|
||||||
|
? isDirectChat
|
||||||
|
? L10n.of(context)!.invitePrivateChat
|
||||||
|
: L10n.of(context)!.inviteGroupChat
|
||||||
|
: snapshot.data ??
|
||||||
|
L10n.of(context)!.emptyChat,
|
||||||
|
softWrap: false,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight:
|
||||||
|
unread || lastEventRoom.hasNewMessages
|
||||||
|
? FontWeight.bold
|
||||||
|
: null,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
decoration: room.lastEvent?.redacted == true
|
||||||
|
? TextDecoration.lineThrough
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
|
@ -288,7 +318,9 @@ class ChatListItem extends StatelessWidget {
|
||||||
width: !hasNotifications && !unread && !room.hasNewMessages
|
width: !hasNotifications && !unread && !room.hasNewMessages
|
||||||
? 0
|
? 0
|
||||||
: (unreadBubbleSize - 9) *
|
: (unreadBubbleSize - 9) *
|
||||||
room.notificationCount.toString().length +
|
lastEventRoom.notificationCount
|
||||||
|
.toString()
|
||||||
|
.length +
|
||||||
9,
|
9,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: room.highlightCount > 0 ||
|
color: room.highlightCount > 0 ||
|
||||||
|
@ -303,7 +335,7 @@ class ChatListItem extends StatelessWidget {
|
||||||
child: Center(
|
child: Center(
|
||||||
child: hasNotifications
|
child: hasNotifications
|
||||||
? Text(
|
? Text(
|
||||||
room.notificationCount.toString(),
|
lastEventRoom.notificationCount.toString(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: room.highlightCount > 0
|
color: room.highlightCount > 0
|
||||||
? Colors.white
|
? Colors.white
|
||||||
|
|
|
@ -1,87 +1,21 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:badges/badges.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
|
||||||
import 'package:fluffychat/config/themes.dart';
|
|
||||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/navi_rail_item.dart';
|
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
|
||||||
import 'package:fluffychat/widgets/unread_rooms_badge.dart';
|
|
||||||
import '../../widgets/matrix.dart';
|
import '../../widgets/matrix.dart';
|
||||||
import 'chat_list_body.dart';
|
import 'chat_list_body.dart';
|
||||||
import 'start_chat_fab.dart';
|
|
||||||
|
|
||||||
class ChatListView extends StatelessWidget {
|
class ChatListView extends StatelessWidget {
|
||||||
final ChatListController controller;
|
final ChatListController controller;
|
||||||
|
|
||||||
const ChatListView(this.controller, {super.key});
|
const ChatListView(this.controller, {super.key});
|
||||||
|
|
||||||
List<NavigationDestination> getNavigationDestinations(BuildContext context) {
|
|
||||||
final badgePosition = BadgePosition.topEnd(top: -12, end: -8);
|
|
||||||
return [
|
|
||||||
if (AppConfig.separateChatTypes) ...[
|
|
||||||
NavigationDestination(
|
|
||||||
icon: UnreadRoomsBadge(
|
|
||||||
badgePosition: badgePosition,
|
|
||||||
filter:
|
|
||||||
controller.getRoomFilterByActiveFilter(ActiveFilter.messages),
|
|
||||||
child: const Icon(Icons.chat_outlined),
|
|
||||||
),
|
|
||||||
selectedIcon: UnreadRoomsBadge(
|
|
||||||
badgePosition: badgePosition,
|
|
||||||
filter:
|
|
||||||
controller.getRoomFilterByActiveFilter(ActiveFilter.messages),
|
|
||||||
child: const Icon(Icons.chat),
|
|
||||||
),
|
|
||||||
label: L10n.of(context)!.messages,
|
|
||||||
),
|
|
||||||
NavigationDestination(
|
|
||||||
icon: UnreadRoomsBadge(
|
|
||||||
badgePosition: badgePosition,
|
|
||||||
filter: controller.getRoomFilterByActiveFilter(ActiveFilter.groups),
|
|
||||||
child: const Icon(Icons.group_outlined),
|
|
||||||
),
|
|
||||||
selectedIcon: UnreadRoomsBadge(
|
|
||||||
badgePosition: badgePosition,
|
|
||||||
filter: controller.getRoomFilterByActiveFilter(ActiveFilter.groups),
|
|
||||||
child: const Icon(Icons.group),
|
|
||||||
),
|
|
||||||
label: L10n.of(context)!.groups,
|
|
||||||
),
|
|
||||||
] else
|
|
||||||
NavigationDestination(
|
|
||||||
icon: UnreadRoomsBadge(
|
|
||||||
badgePosition: badgePosition,
|
|
||||||
filter:
|
|
||||||
controller.getRoomFilterByActiveFilter(ActiveFilter.allChats),
|
|
||||||
child: const Icon(Icons.chat_outlined),
|
|
||||||
),
|
|
||||||
selectedIcon: UnreadRoomsBadge(
|
|
||||||
badgePosition: badgePosition,
|
|
||||||
filter:
|
|
||||||
controller.getRoomFilterByActiveFilter(ActiveFilter.allChats),
|
|
||||||
child: const Icon(Icons.chat),
|
|
||||||
),
|
|
||||||
label: L10n.of(context)!.chats,
|
|
||||||
),
|
|
||||||
if (controller.spaces.isNotEmpty)
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.workspaces_outlined),
|
|
||||||
selectedIcon: Icon(Icons.workspaces),
|
|
||||||
label: 'Spaces',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final client = Matrix.of(context).client;
|
|
||||||
return StreamBuilder<Object?>(
|
return StreamBuilder<Object?>(
|
||||||
stream: Matrix.of(context).onShareContentChanged.stream,
|
stream: Matrix.of(context).onShareContentChanged.stream,
|
||||||
builder: (_, __) {
|
builder: (_, __) {
|
||||||
|
@ -89,10 +23,7 @@ class ChatListView extends StatelessWidget {
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: controller.selectMode == SelectMode.normal &&
|
canPop: controller.selectMode == SelectMode.normal &&
|
||||||
!controller.isSearchMode &&
|
!controller.isSearchMode &&
|
||||||
controller.activeFilter ==
|
controller.activeFilter == ActiveFilter.allChats,
|
||||||
(AppConfig.separateChatTypes
|
|
||||||
? ActiveFilter.messages
|
|
||||||
: ActiveFilter.allChats),
|
|
||||||
onPopInvoked: (pop) async {
|
onPopInvoked: (pop) async {
|
||||||
if (pop) return;
|
if (pop) return;
|
||||||
final selMode = controller.selectMode;
|
final selMode = controller.selectMode;
|
||||||
|
@ -104,122 +35,33 @@ class ChatListView extends StatelessWidget {
|
||||||
controller.cancelAction();
|
controller.cancelAction();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (controller.activeFilter !=
|
|
||||||
(AppConfig.separateChatTypes
|
|
||||||
? ActiveFilter.messages
|
|
||||||
: ActiveFilter.allChats)) {
|
|
||||||
controller
|
|
||||||
.onDestinationSelected(AppConfig.separateChatTypes ? 1 : 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: Row(
|
child: GestureDetector(
|
||||||
children: [
|
onTap: FocusManager.instance.primaryFocus?.unfocus,
|
||||||
if (FluffyThemes.isColumnMode(context) &&
|
excludeFromSemantics: true,
|
||||||
controller.widget.displayNavigationRail) ...[
|
behavior: HitTestBehavior.translucent,
|
||||||
Builder(
|
child: Scaffold(
|
||||||
builder: (context) {
|
body: ChatListViewBody(controller),
|
||||||
final allSpaces =
|
floatingActionButton: KeyBoardShortcuts(
|
||||||
client.rooms.where((room) => room.isSpace);
|
keysToPress: {
|
||||||
final rootSpaces = allSpaces
|
LogicalKeyboardKey.controlLeft,
|
||||||
.where(
|
LogicalKeyboardKey.keyN,
|
||||||
(space) => !allSpaces.any(
|
},
|
||||||
(parentSpace) => parentSpace.spaceChildren
|
onKeysPressed: () => context.go('/rooms/newprivatechat'),
|
||||||
.any((child) => child.roomId == space.id),
|
helpLabel: L10n.of(context)!.newChat,
|
||||||
),
|
child:
|
||||||
)
|
selectMode == SelectMode.normal && !controller.isSearchMode
|
||||||
.toList();
|
? FloatingActionButton.extended(
|
||||||
final destinations = getNavigationDestinations(context);
|
onPressed: controller.addChatAction,
|
||||||
|
icon: const Icon(Icons.add_outlined),
|
||||||
return SizedBox(
|
label: Text(
|
||||||
width: FluffyThemes.navRailWidth,
|
L10n.of(context)!.chat,
|
||||||
child: ListView.builder(
|
overflow: TextOverflow.fade,
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
itemCount: rootSpaces.length + destinations.length,
|
|
||||||
itemBuilder: (context, i) {
|
|
||||||
if (i < destinations.length) {
|
|
||||||
return NaviRailItem(
|
|
||||||
isSelected: i == controller.selectedIndex,
|
|
||||||
onTap: () => controller.onDestinationSelected(i),
|
|
||||||
icon: destinations[i].icon,
|
|
||||||
selectedIcon: destinations[i].selectedIcon,
|
|
||||||
toolTip: destinations[i].label,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
i -= destinations.length;
|
|
||||||
final isSelected =
|
|
||||||
controller.activeFilter == ActiveFilter.spaces &&
|
|
||||||
rootSpaces[i].id == controller.activeSpaceId;
|
|
||||||
return NaviRailItem(
|
|
||||||
toolTip: rootSpaces[i].getLocalizedDisplayname(
|
|
||||||
MatrixLocals(L10n.of(context)!),
|
|
||||||
),
|
),
|
||||||
isSelected: isSelected,
|
|
||||||
onTap: () =>
|
|
||||||
controller.setActiveSpace(rootSpaces[i].id),
|
|
||||||
icon: Avatar(
|
|
||||||
mxContent: rootSpaces[i].avatar,
|
|
||||||
name: rootSpaces[i].getLocalizedDisplayname(
|
|
||||||
MatrixLocals(L10n.of(context)!),
|
|
||||||
),
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
Expanded(
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: FocusManager.instance.primaryFocus?.unfocus,
|
|
||||||
excludeFromSemantics: true,
|
|
||||||
behavior: HitTestBehavior.translucent,
|
|
||||||
child: Scaffold(
|
|
||||||
body: ChatListViewBody(controller),
|
|
||||||
bottomNavigationBar: controller.displayNavigationBar
|
|
||||||
? NavigationBar(
|
|
||||||
elevation: 4,
|
|
||||||
labelBehavior:
|
|
||||||
NavigationDestinationLabelBehavior.alwaysShow,
|
|
||||||
shadowColor:
|
|
||||||
Theme.of(context).colorScheme.onSurface,
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.surface,
|
|
||||||
surfaceTintColor:
|
|
||||||
Theme.of(context).colorScheme.surface,
|
|
||||||
selectedIndex: controller.selectedIndex,
|
|
||||||
onDestinationSelected:
|
|
||||||
controller.onDestinationSelected,
|
|
||||||
destinations: getNavigationDestinations(context),
|
|
||||||
)
|
)
|
||||||
: null,
|
: const SizedBox.shrink(),
|
||||||
floatingActionButton: KeyBoardShortcuts(
|
|
||||||
keysToPress: {
|
|
||||||
LogicalKeyboardKey.controlLeft,
|
|
||||||
LogicalKeyboardKey.keyN,
|
|
||||||
},
|
|
||||||
onKeysPressed: () => context.go('/rooms/newprivatechat'),
|
|
||||||
helpLabel: L10n.of(context)!.newChat,
|
|
||||||
child: selectMode == SelectMode.normal &&
|
|
||||||
!controller.isSearchMode
|
|
||||||
? StartChatFloatingActionButton(
|
|
||||||
activeFilter: controller.activeFilter,
|
|
||||||
roomsIsEmpty: false,
|
|
||||||
scrolledToTop: controller.scrolledToTop,
|
|
||||||
createNewSpace: controller.createNewSpace,
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,26 +5,32 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:matrix/matrix.dart' as sdk;
|
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart';
|
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
import 'package:fluffychat/utils/stream_extension.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
import '../../utils/localized_exception_extension.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
import '../../widgets/matrix.dart';
|
|
||||||
import 'chat_list_header.dart';
|
|
||||||
|
|
||||||
class SpaceView extends StatefulWidget {
|
class SpaceView extends StatefulWidget {
|
||||||
final ChatListController controller;
|
final String spaceId;
|
||||||
final ScrollController scrollController;
|
final void Function() onBack;
|
||||||
const SpaceView(
|
final void Function(String spaceId) toParentSpace;
|
||||||
this.controller, {
|
final void Function(Room room) onChatTab;
|
||||||
|
final void Function(Room room) onChatContext;
|
||||||
|
final String? activeChat;
|
||||||
|
|
||||||
|
const SpaceView({
|
||||||
|
required this.spaceId,
|
||||||
|
required this.onBack,
|
||||||
|
required this.onChatTab,
|
||||||
|
required this.activeChat,
|
||||||
|
required this.toParentSpace,
|
||||||
|
required this.onChatContext,
|
||||||
super.key,
|
super.key,
|
||||||
required this.scrollController,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -32,543 +38,449 @@ class SpaceView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SpaceViewState extends State<SpaceView> {
|
class _SpaceViewState extends State<SpaceView> {
|
||||||
static final Map<String, GetSpaceHierarchyResponse> _lastResponse = {};
|
final List<SpaceRoomsChunk> _discoveredChildren = [];
|
||||||
|
final TextEditingController _filterController = TextEditingController();
|
||||||
String? prevBatch;
|
String? _nextBatch;
|
||||||
Object? error;
|
bool _noMoreRooms = false;
|
||||||
bool loading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
loadHierarchy();
|
_loadHierarchy();
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _refresh() {
|
void _loadHierarchy() async {
|
||||||
_lastResponse.remove(widget.controller.activeSpaceId);
|
final room = Matrix.of(context).client.getRoomById(widget.spaceId);
|
||||||
loadHierarchy();
|
if (room == null) return;
|
||||||
}
|
|
||||||
|
|
||||||
Future<GetSpaceHierarchyResponse?> loadHierarchy([String? prevBatch]) async {
|
|
||||||
final activeSpaceId = widget.controller.activeSpaceId;
|
|
||||||
if (activeSpaceId == null) return null;
|
|
||||||
final client = Matrix.of(context).client;
|
|
||||||
|
|
||||||
final activeSpace = client.getRoomById(activeSpaceId);
|
|
||||||
await activeSpace?.postLoad();
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
error = null;
|
_isLoading = true;
|
||||||
loading = true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await client.getSpaceHierarchy(
|
final hierarchy = await room.client.getSpaceHierarchy(
|
||||||
activeSpaceId,
|
widget.spaceId,
|
||||||
maxDepth: 1,
|
suggestedOnly: false,
|
||||||
from: prevBatch,
|
maxDepth: 2,
|
||||||
|
from: _nextBatch,
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
if (prevBatch != null) {
|
|
||||||
response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []);
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_lastResponse[activeSpaceId] = response;
|
_nextBatch = hierarchy.nextBatch;
|
||||||
});
|
if (hierarchy.nextBatch == null) {
|
||||||
return _lastResponse[activeSpaceId]!;
|
_noMoreRooms = true;
|
||||||
} catch (e) {
|
}
|
||||||
setState(() {
|
_discoveredChildren.addAll(
|
||||||
error = e;
|
hierarchy.rooms
|
||||||
});
|
.where((c) => room.client.getRoomById(c.roomId) == null),
|
||||||
rethrow;
|
|
||||||
} finally {
|
|
||||||
setState(() {
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onJoinSpaceChild(SpaceRoomsChunk spaceChild) async {
|
|
||||||
final client = Matrix.of(context).client;
|
|
||||||
final space = client.getRoomById(widget.controller.activeSpaceId!);
|
|
||||||
if (client.getRoomById(spaceChild.roomId) == null) {
|
|
||||||
final result = await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () async {
|
|
||||||
await client.joinRoom(
|
|
||||||
spaceChild.roomId,
|
|
||||||
serverName: space?.spaceChildren
|
|
||||||
.firstWhereOrNull(
|
|
||||||
(child) => child.roomId == spaceChild.roomId,
|
|
||||||
)
|
|
||||||
?.via,
|
|
||||||
);
|
|
||||||
if (client.getRoomById(spaceChild.roomId) == null) {
|
|
||||||
// Wait for room actually appears in sync
|
|
||||||
await client.waitForRoomInSync(spaceChild.roomId, join: true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (result.error != null) return;
|
|
||||||
_refresh();
|
|
||||||
}
|
|
||||||
if (spaceChild.roomType == 'm.space') {
|
|
||||||
if (spaceChild.roomId == widget.controller.activeSpaceId) {
|
|
||||||
context.go('/rooms/${spaceChild.roomId}');
|
|
||||||
} else {
|
|
||||||
widget.controller.setActiveSpace(spaceChild.roomId);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
context.go('/rooms/${spaceChild.roomId}');
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSpaceChildContextMenu([
|
|
||||||
SpaceRoomsChunk? spaceChild,
|
|
||||||
Room? room,
|
|
||||||
]) async {
|
|
||||||
final client = Matrix.of(context).client;
|
|
||||||
final activeSpaceId = widget.controller.activeSpaceId;
|
|
||||||
final activeSpace =
|
|
||||||
activeSpaceId == null ? null : client.getRoomById(activeSpaceId);
|
|
||||||
final action = await showModalActionSheet<SpaceChildContextAction>(
|
|
||||||
context: context,
|
|
||||||
title: spaceChild?.name ??
|
|
||||||
room?.getLocalizedDisplayname(
|
|
||||||
MatrixLocals(L10n.of(context)!),
|
|
||||||
),
|
|
||||||
message: spaceChild?.topic ?? room?.topic,
|
|
||||||
actions: [
|
|
||||||
if (room == null)
|
|
||||||
SheetAction(
|
|
||||||
key: SpaceChildContextAction.join,
|
|
||||||
label: L10n.of(context)!.joinRoom,
|
|
||||||
icon: Icons.send_outlined,
|
|
||||||
),
|
|
||||||
if (spaceChild != null &&
|
|
||||||
(activeSpace?.canChangeStateEvent(EventTypes.SpaceChild) ?? false))
|
|
||||||
SheetAction(
|
|
||||||
key: SpaceChildContextAction.removeFromSpace,
|
|
||||||
label: L10n.of(context)!.removeFromSpace,
|
|
||||||
icon: Icons.delete_sweep_outlined,
|
|
||||||
),
|
|
||||||
if (room != null)
|
|
||||||
SheetAction(
|
|
||||||
key: SpaceChildContextAction.leave,
|
|
||||||
label: L10n.of(context)!.leave,
|
|
||||||
icon: Icons.delete_outlined,
|
|
||||||
isDestructiveAction: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (action == null) return;
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case SpaceChildContextAction.join:
|
|
||||||
_onJoinSpaceChild(spaceChild!);
|
|
||||||
break;
|
|
||||||
case SpaceChildContextAction.leave:
|
|
||||||
await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: room!.leave,
|
|
||||||
);
|
);
|
||||||
break;
|
_isLoading = false;
|
||||||
case SpaceChildContextAction.removeFromSpace:
|
});
|
||||||
await showFutureLoadingDialog(
|
} catch (e, s) {
|
||||||
context: context,
|
Logs().w('Unable to load hierarchy', e, s);
|
||||||
future: () => activeSpace!.removeSpaceChild(spaceChild!.roomId),
|
if (!mounted) return;
|
||||||
);
|
ScaffoldMessenger.of(context)
|
||||||
break;
|
.showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addChatOrSubSpace() async {
|
void _joinChildRoom(SpaceRoomsChunk item) async {
|
||||||
final roomType = await showConfirmationDialog(
|
final client = Matrix.of(context).client;
|
||||||
context: context,
|
final space = client.getRoomById(widget.spaceId);
|
||||||
title: L10n.of(context)!.addChatOrSubSpace,
|
|
||||||
actions: [
|
|
||||||
AlertDialogAction(
|
|
||||||
key: AddRoomType.subspace,
|
|
||||||
label: L10n.of(context)!.createNewSpace,
|
|
||||||
),
|
|
||||||
AlertDialogAction(
|
|
||||||
key: AddRoomType.chat,
|
|
||||||
label: L10n.of(context)!.createGroup,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (roomType == null) return;
|
|
||||||
|
|
||||||
final names = await showTextInputDialog(
|
final consent = await showOkCancelAlertDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: roomType == AddRoomType.subspace
|
title: item.name ?? item.canonicalAlias ?? L10n.of(context)!.emptyChat,
|
||||||
? L10n.of(context)!.createNewSpace
|
message: item.topic,
|
||||||
: L10n.of(context)!.createGroup,
|
okLabel: L10n.of(context)!.joinRoom,
|
||||||
textFields: [
|
|
||||||
DialogTextField(
|
|
||||||
hintText: roomType == AddRoomType.subspace
|
|
||||||
? L10n.of(context)!.spaceName
|
|
||||||
: L10n.of(context)!.groupName,
|
|
||||||
minLines: 1,
|
|
||||||
maxLines: 1,
|
|
||||||
maxLength: 64,
|
|
||||||
validator: (text) {
|
|
||||||
if (text == null || text.isEmpty) {
|
|
||||||
return L10n.of(context)!.pleaseChoose;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
DialogTextField(
|
|
||||||
hintText: L10n.of(context)!.chatDescription,
|
|
||||||
minLines: 4,
|
|
||||||
maxLines: 8,
|
|
||||||
maxLength: 255,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
okLabel: L10n.of(context)!.create,
|
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
);
|
);
|
||||||
if (names == null) return;
|
if (consent != OkCancelResult.ok) return;
|
||||||
final client = Matrix.of(context).client;
|
if (!mounted) return;
|
||||||
final result = await showFutureLoadingDialog(
|
|
||||||
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
late final String roomId;
|
await client.joinRoom(
|
||||||
final activeSpace = client.getRoomById(
|
item.roomId,
|
||||||
widget.controller.activeSpaceId!,
|
serverName: space?.spaceChildren
|
||||||
)!;
|
.firstWhereOrNull(
|
||||||
|
(child) => child.roomId == item.roomId,
|
||||||
if (roomType == AddRoomType.subspace) {
|
)
|
||||||
roomId = await client.createSpace(
|
?.via,
|
||||||
name: names.first,
|
);
|
||||||
topic: names.last.isEmpty ? null : names.last,
|
if (client.getRoomById(item.roomId) == null) {
|
||||||
visibility: activeSpace.joinRules == JoinRules.public
|
// Wait for room actually appears in sync
|
||||||
? sdk.Visibility.public
|
await client.waitForRoomInSync(item.roomId, join: true);
|
||||||
: sdk.Visibility.private,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
roomId = await client.createGroupChat(
|
|
||||||
groupName: names.first,
|
|
||||||
initialState: names.length > 1 && names.last.isNotEmpty
|
|
||||||
? [
|
|
||||||
sdk.StateEvent(
|
|
||||||
type: sdk.EventTypes.RoomTopic,
|
|
||||||
content: {'topic': names.last},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
await activeSpace.setSpaceChild(roomId);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (result.error != null) return;
|
if (!mounted) return;
|
||||||
_refresh();
|
|
||||||
|
setState(() {
|
||||||
|
_discoveredChildren.remove(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSpaceAction(SpaceActions action) async {
|
||||||
|
final space = Matrix.of(context).client.getRoomById(widget.spaceId);
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case SpaceActions.settings:
|
||||||
|
await space?.postLoad();
|
||||||
|
context.push('/rooms/${widget.spaceId}/details');
|
||||||
|
break;
|
||||||
|
case SpaceActions.invite:
|
||||||
|
await space?.postLoad();
|
||||||
|
context.push('/rooms/${widget.spaceId}/invite');
|
||||||
|
break;
|
||||||
|
case SpaceActions.leave:
|
||||||
|
final confirmed = await showOkCancelAlertDialog(
|
||||||
|
useRootNavigator: false,
|
||||||
|
context: context,
|
||||||
|
title: L10n.of(context)!.areYouSure,
|
||||||
|
okLabel: L10n.of(context)!.ok,
|
||||||
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
|
message: L10n.of(context)!.archiveRoomDescription,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (confirmed != OkCancelResult.ok) return;
|
||||||
|
|
||||||
|
final success = await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () async => await space?.leave(),
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (success.error != null) return;
|
||||||
|
widget.onBack();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final client = Matrix.of(context).client;
|
final room = Matrix.of(context).client.getRoomById(widget.spaceId);
|
||||||
final activeSpaceId = widget.controller.activeSpaceId;
|
final displayname =
|
||||||
final activeSpace = activeSpaceId == null
|
room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound;
|
||||||
? null
|
return Scaffold(
|
||||||
: client.getRoomById(
|
appBar: AppBar(
|
||||||
activeSpaceId,
|
leading: Center(
|
||||||
);
|
child: CloseButton(
|
||||||
final allSpaces = client.rooms.where((room) => room.isSpace);
|
onPressed: widget.onBack,
|
||||||
if (activeSpaceId == null) {
|
),
|
||||||
final rootSpaces = allSpaces
|
|
||||||
.where(
|
|
||||||
(space) =>
|
|
||||||
!allSpaces.any(
|
|
||||||
(parentSpace) => parentSpace.spaceChildren
|
|
||||||
.any((child) => child.roomId == space.id),
|
|
||||||
) &&
|
|
||||||
space
|
|
||||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!))
|
|
||||||
.toLowerCase()
|
|
||||||
.contains(
|
|
||||||
widget.controller.searchController.text.toLowerCase(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return SafeArea(
|
|
||||||
child: CustomScrollView(
|
|
||||||
controller: widget.scrollController,
|
|
||||||
slivers: [
|
|
||||||
ChatListHeader(controller: widget.controller),
|
|
||||||
SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, i) {
|
|
||||||
final rootSpace = rootSpaces[i];
|
|
||||||
final displayname = rootSpace.getLocalizedDisplayname(
|
|
||||||
MatrixLocals(L10n.of(context)!),
|
|
||||||
);
|
|
||||||
return Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: ListTile(
|
|
||||||
leading: Avatar(
|
|
||||||
mxContent: rootSpace.avatar,
|
|
||||||
name: displayname,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
displayname,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
L10n.of(context)!.numChats(
|
|
||||||
rootSpace.spaceChildren.length.toString(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () =>
|
|
||||||
widget.controller.setActiveSpace(rootSpace.id),
|
|
||||||
onLongPress: () =>
|
|
||||||
_onSpaceChildContextMenu(null, rootSpace),
|
|
||||||
trailing: const Icon(Icons.chevron_right_outlined),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: rootSpaces.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
titleSpacing: 0,
|
||||||
}
|
title: ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
final parentSpace = allSpaces.firstWhereOrNull(
|
leading: Avatar(
|
||||||
(space) =>
|
mxContent: room?.avatar,
|
||||||
space.spaceChildren.any((child) => child.roomId == activeSpaceId),
|
name: displayname,
|
||||||
);
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||||
return PopScope(
|
),
|
||||||
canPop: parentSpace == null,
|
title: Text(
|
||||||
onPopInvoked: (pop) async {
|
displayname,
|
||||||
if (pop) return;
|
maxLines: 1,
|
||||||
if (parentSpace != null) {
|
overflow: TextOverflow.ellipsis,
|
||||||
widget.controller.setActiveSpace(parentSpace.id);
|
),
|
||||||
}
|
subtitle: room == null
|
||||||
},
|
? null
|
||||||
child: SafeArea(
|
: Text(
|
||||||
child: CustomScrollView(
|
L10n.of(context)!.countChatsAndCountParticipants(
|
||||||
controller: widget.scrollController,
|
room.spaceChildren.length,
|
||||||
slivers: [
|
room.summary.mJoinedMemberCount ?? 1,
|
||||||
ChatListHeader(controller: widget.controller, globalSearch: false),
|
),
|
||||||
SliverAppBar(
|
maxLines: 1,
|
||||||
automaticallyImplyLeading: false,
|
overflow: TextOverflow.ellipsis,
|
||||||
primary: false,
|
|
||||||
titleSpacing: 0,
|
|
||||||
title: ListTile(
|
|
||||||
leading: BackButton(
|
|
||||||
onPressed: () =>
|
|
||||||
widget.controller.setActiveSpace(parentSpace?.id),
|
|
||||||
),
|
),
|
||||||
title: Text(
|
),
|
||||||
parentSpace == null
|
actions: [
|
||||||
? L10n.of(context)!.allSpaces
|
PopupMenuButton<SpaceActions>(
|
||||||
: parentSpace.getLocalizedDisplayname(
|
onSelected: _onSpaceAction,
|
||||||
MatrixLocals(L10n.of(context)!),
|
itemBuilder: (context) => [
|
||||||
),
|
PopupMenuItem(
|
||||||
),
|
value: SpaceActions.settings,
|
||||||
trailing: IconButton(
|
child: Row(
|
||||||
icon: loading
|
mainAxisSize: MainAxisSize.min,
|
||||||
? const CircularProgressIndicator.adaptive(strokeWidth: 2)
|
children: [
|
||||||
: const Icon(Icons.refresh_outlined),
|
const Icon(Icons.settings_outlined),
|
||||||
onPressed: loading ? null : _refresh,
|
const SizedBox(width: 12),
|
||||||
|
Text(L10n.of(context)!.settings),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
PopupMenuItem(
|
||||||
Builder(
|
value: SpaceActions.invite,
|
||||||
builder: (context) {
|
child: Row(
|
||||||
final response = _lastResponse[activeSpaceId];
|
mainAxisSize: MainAxisSize.min,
|
||||||
final error = this.error;
|
children: [
|
||||||
if (error != null) {
|
const Icon(Icons.person_add_outlined),
|
||||||
return SliverFillRemaining(
|
const SizedBox(width: 12),
|
||||||
child: Column(
|
Text(L10n.of(context)!.invite),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
],
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
),
|
||||||
children: [
|
),
|
||||||
Padding(
|
PopupMenuItem(
|
||||||
padding: const EdgeInsets.all(16.0),
|
value: SpaceActions.leave,
|
||||||
child: Text(error.toLocalizedString(context)),
|
child: Row(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
IconButton(
|
children: [
|
||||||
onPressed: _refresh,
|
const Icon(Icons.delete_outlined),
|
||||||
icon: const Icon(Icons.refresh_outlined),
|
const SizedBox(width: 12),
|
||||||
),
|
Text(L10n.of(context)!.leave),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
],
|
||||||
if (response == null) {
|
),
|
||||||
return SliverFillRemaining(
|
],
|
||||||
child: Center(
|
),
|
||||||
child: Text(L10n.of(context)!.loadingPleaseWait),
|
body: room == null
|
||||||
),
|
? const Center(
|
||||||
);
|
child: Icon(
|
||||||
}
|
Icons.search_outlined,
|
||||||
final spaceChildren = response.rooms;
|
size: 80,
|
||||||
final canLoadMore = response.nextBatch != null;
|
),
|
||||||
return SliverList(
|
)
|
||||||
delegate: SliverChildBuilderDelegate(
|
: StreamBuilder(
|
||||||
(context, i) {
|
stream: room.client.onSync.stream
|
||||||
if (canLoadMore && i == spaceChildren.length) {
|
.where((s) => s.hasRoomUpdate)
|
||||||
return Padding(
|
.rateLimit(const Duration(seconds: 1)),
|
||||||
padding: const EdgeInsets.all(16.0),
|
builder: (context, snapshot) {
|
||||||
child: OutlinedButton.icon(
|
final joinedRooms = room.spaceChildren
|
||||||
label: loading
|
.map((child) {
|
||||||
? const LinearProgressIndicator()
|
final roomId = child.roomId;
|
||||||
: Text(L10n.of(context)!.loadMore),
|
if (roomId == null) return null;
|
||||||
icon: const Icon(Icons.chevron_right_outlined),
|
return room.client.getRoomById(roomId);
|
||||||
onPressed: loading
|
})
|
||||||
? null
|
.whereType<Room>()
|
||||||
: () {
|
.where((room) => room.membership != Membership.leave)
|
||||||
loadHierarchy(response.nextBatch);
|
.toList();
|
||||||
},
|
|
||||||
|
// Sort rooms by last activity
|
||||||
|
joinedRooms.sort(
|
||||||
|
(b, a) => (a.lastEvent?.originServerTs ??
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(0))
|
||||||
|
.compareTo(
|
||||||
|
b.lastEvent?.originServerTs ??
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final joinedParents = room.spaceParents
|
||||||
|
.map((parent) {
|
||||||
|
final roomId = parent.roomId;
|
||||||
|
if (roomId == null) return null;
|
||||||
|
return room.client.getRoomById(roomId);
|
||||||
|
})
|
||||||
|
.whereType<Room>()
|
||||||
|
.toList();
|
||||||
|
final filter = _filterController.text.trim().toLowerCase();
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
toolbarHeight: 72,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
title: TextField(
|
||||||
|
controller: _filterController,
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
|
textInputAction: TextInputAction.search,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
fillColor:
|
||||||
|
Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
borderRadius: BorderRadius.circular(99),
|
||||||
),
|
),
|
||||||
);
|
contentPadding: EdgeInsets.zero,
|
||||||
}
|
hintText: L10n.of(context)!.search,
|
||||||
final spaceChild = spaceChildren[i];
|
hintStyle: TextStyle(
|
||||||
final room = client.getRoomById(spaceChild.roomId);
|
color: Theme.of(context)
|
||||||
if (room != null && !room.isSpace) {
|
.colorScheme
|
||||||
return ChatListItem(
|
.onPrimaryContainer,
|
||||||
room,
|
fontWeight: FontWeight.normal,
|
||||||
onLongPress: () =>
|
),
|
||||||
_onSpaceChildContextMenu(spaceChild, room),
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
activeChat: widget.controller.activeChat == room.id,
|
prefixIcon: IconButton(
|
||||||
onTap: () => onChatTap(room, context),
|
onPressed: () {},
|
||||||
);
|
icon: Icon(
|
||||||
}
|
Icons.search_outlined,
|
||||||
final isSpace = spaceChild.roomType == 'm.space';
|
|
||||||
final topic = spaceChild.topic?.isEmpty ?? true
|
|
||||||
? null
|
|
||||||
: spaceChild.topic;
|
|
||||||
if (spaceChild.roomId == activeSpaceId) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
SearchTitle(
|
|
||||||
title: spaceChild.name ??
|
|
||||||
spaceChild.canonicalAlias ??
|
|
||||||
'Space',
|
|
||||||
icon: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 10.0,
|
|
||||||
),
|
|
||||||
child: Avatar(
|
|
||||||
size: 24,
|
|
||||||
mxContent: spaceChild.avatarUrl,
|
|
||||||
name: spaceChild.name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.secondaryContainer
|
.onPrimaryContainer,
|
||||||
.withAlpha(128),
|
|
||||||
trailing: const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Icon(Icons.edit_outlined),
|
|
||||||
),
|
|
||||||
onTap: () => _onJoinSpaceChild(spaceChild),
|
|
||||||
),
|
|
||||||
if (activeSpace?.canChangeStateEvent(
|
|
||||||
EventTypes.SpaceChild,
|
|
||||||
) ==
|
|
||||||
true)
|
|
||||||
Material(
|
|
||||||
child: ListTile(
|
|
||||||
leading: const CircleAvatar(
|
|
||||||
child: Icon(Icons.group_add_outlined),
|
|
||||||
),
|
|
||||||
title:
|
|
||||||
Text(L10n.of(context)!.addChatOrSubSpace),
|
|
||||||
trailing:
|
|
||||||
const Icon(Icons.chevron_right_outlined),
|
|
||||||
onTap: _addChatOrSubSpace,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final name = spaceChild.name ??
|
|
||||||
spaceChild.canonicalAlias ??
|
|
||||||
L10n.of(context)!.chat;
|
|
||||||
if (widget.controller.isSearchMode &&
|
|
||||||
!name.toLowerCase().contains(
|
|
||||||
widget.controller.searchController.text
|
|
||||||
.toLowerCase(),
|
|
||||||
)) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return Material(
|
|
||||||
child: ListTile(
|
|
||||||
leading: Avatar(
|
|
||||||
mxContent: spaceChild.avatarUrl,
|
|
||||||
name: spaceChild.name,
|
|
||||||
),
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
name,
|
|
||||||
maxLines: 1,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!isSpace) ...[
|
|
||||||
const Icon(
|
|
||||||
Icons.people_outline,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
spaceChild.numJoinedMembers.toString(),
|
|
||||||
style: const TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () => room?.isSpace == true
|
|
||||||
? widget.controller.setActiveSpace(room!.id)
|
|
||||||
: _onSpaceChildContextMenu(spaceChild, room),
|
|
||||||
onLongPress: () =>
|
|
||||||
_onSpaceChildContextMenu(spaceChild, room),
|
|
||||||
subtitle: Text(
|
|
||||||
topic ??
|
|
||||||
(isSpace
|
|
||||||
? L10n.of(context)!.enterSpace
|
|
||||||
: L10n.of(context)!.enterRoom),
|
|
||||||
maxLines: 1,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: isSpace
|
|
||||||
? const Icon(Icons.chevron_right_outlined)
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
childCount: spaceChildren.length + (canLoadMore ? 1 : 0),
|
SliverList.builder(
|
||||||
),
|
itemCount: joinedParents.length,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
final displayname =
|
||||||
|
joinedParents[i].getLocalizedDisplayname();
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 1,
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppConfig.borderRadius),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: ListTile(
|
||||||
|
minVerticalPadding: 0,
|
||||||
|
leading: Icon(
|
||||||
|
Icons.adaptive.arrow_back_outlined,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
mxContent: joinedParents[i].avatar,
|
||||||
|
name: displayname,
|
||||||
|
size: Avatar.defaultSize / 2,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppConfig.borderRadius / 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(child: Text(displayname)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () =>
|
||||||
|
widget.toParentSpace(joinedParents[i].id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SliverList.builder(
|
||||||
|
itemCount: joinedRooms.length + 1,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
if (i == 0) {
|
||||||
|
return SearchTitle(
|
||||||
|
title: L10n.of(context)!.joinedChats,
|
||||||
|
icon: const Icon(Icons.chat_outlined),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
i--;
|
||||||
|
final room = joinedRooms[i];
|
||||||
|
return ChatListItem(
|
||||||
|
room,
|
||||||
|
filter: filter,
|
||||||
|
onTap: () => widget.onChatTab(room),
|
||||||
|
onLongPress: () => widget.onChatContext(room),
|
||||||
|
activeChat: widget.activeChat == room.id,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SliverList.builder(
|
||||||
|
itemCount: _discoveredChildren.length + 2,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
if (i == 0) {
|
||||||
|
return SearchTitle(
|
||||||
|
title: L10n.of(context)!.discover,
|
||||||
|
icon: const Icon(Icons.explore_outlined),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
i--;
|
||||||
|
if (i == _discoveredChildren.length) {
|
||||||
|
if (_noMoreRooms) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
L10n.of(context)!.noMoreChatsFound,
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12.0,
|
||||||
|
vertical: 2.0,
|
||||||
|
),
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: _isLoading ? null : _loadHierarchy,
|
||||||
|
child: _isLoading
|
||||||
|
? LinearProgressIndicator(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppConfig.borderRadius,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(L10n.of(context)!.loadMore),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final item = _discoveredChildren[i];
|
||||||
|
final displayname = item.name ??
|
||||||
|
item.canonicalAlias ??
|
||||||
|
L10n.of(context)!.emptyChat;
|
||||||
|
if (!displayname.toLowerCase().contains(filter)) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 1,
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppConfig.borderRadius),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () => _joinChildRoom(item),
|
||||||
|
leading: Avatar(
|
||||||
|
mxContent: item.avatarUrl,
|
||||||
|
name: displayname,
|
||||||
|
borderRadius: item.roomType == 'm.space'
|
||||||
|
? BorderRadius.circular(
|
||||||
|
AppConfig.borderRadius / 2,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
displayname,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Icon(Icons.add_circle_outline_outlined),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
item.topic ??
|
||||||
|
L10n.of(context)!.countParticipants(
|
||||||
|
item.numJoinedMembers,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SpaceChildContextAction {
|
enum SpaceActions {
|
||||||
join,
|
settings,
|
||||||
|
invite,
|
||||||
leave,
|
leave,
|
||||||
removeFromSpace,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AddRoomType { chat, subspace }
|
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
import '../../config/themes.dart';
|
|
||||||
import 'chat_list.dart';
|
|
||||||
|
|
||||||
class StartChatFloatingActionButton extends StatelessWidget {
|
|
||||||
final ActiveFilter activeFilter;
|
|
||||||
final ValueNotifier<bool> scrolledToTop;
|
|
||||||
final bool roomsIsEmpty;
|
|
||||||
final void Function() createNewSpace;
|
|
||||||
|
|
||||||
const StartChatFloatingActionButton({
|
|
||||||
super.key,
|
|
||||||
required this.activeFilter,
|
|
||||||
required this.scrolledToTop,
|
|
||||||
required this.roomsIsEmpty,
|
|
||||||
required this.createNewSpace,
|
|
||||||
});
|
|
||||||
|
|
||||||
void _onPressed(BuildContext context) async {
|
|
||||||
switch (activeFilter) {
|
|
||||||
case ActiveFilter.allChats:
|
|
||||||
case ActiveFilter.messages:
|
|
||||||
context.go('/rooms/newprivatechat');
|
|
||||||
break;
|
|
||||||
case ActiveFilter.groups:
|
|
||||||
context.go('/rooms/newgroup');
|
|
||||||
break;
|
|
||||||
case ActiveFilter.spaces:
|
|
||||||
createNewSpace();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconData get icon {
|
|
||||||
switch (activeFilter) {
|
|
||||||
case ActiveFilter.allChats:
|
|
||||||
case ActiveFilter.messages:
|
|
||||||
return Icons.add_outlined;
|
|
||||||
case ActiveFilter.groups:
|
|
||||||
return Icons.group_add_outlined;
|
|
||||||
case ActiveFilter.spaces:
|
|
||||||
return Icons.workspaces_outlined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String getLabel(BuildContext context) {
|
|
||||||
switch (activeFilter) {
|
|
||||||
case ActiveFilter.allChats:
|
|
||||||
case ActiveFilter.messages:
|
|
||||||
return roomsIsEmpty
|
|
||||||
? L10n.of(context)!.startFirstChat
|
|
||||||
: L10n.of(context)!.newChat;
|
|
||||||
case ActiveFilter.groups:
|
|
||||||
return L10n.of(context)!.newGroup;
|
|
||||||
case ActiveFilter.spaces:
|
|
||||||
return L10n.of(context)!.newSpace;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ValueListenableBuilder<bool>(
|
|
||||||
valueListenable: scrolledToTop,
|
|
||||||
builder: (context, scrolledToTop, _) => AnimatedSize(
|
|
||||||
duration: FluffyThemes.animationDuration,
|
|
||||||
curve: FluffyThemes.animationCurve,
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
child: scrolledToTop
|
|
||||||
? FloatingActionButton.extended(
|
|
||||||
onPressed: () => _onPressed(context),
|
|
||||||
icon: Icon(icon),
|
|
||||||
label: Text(
|
|
||||||
getLabel(context),
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: FloatingActionButton(
|
|
||||||
onPressed: () => _onPressed(context),
|
|
||||||
child: Icon(icon),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:matrix/matrix.dart';
|
|
||||||
|
|
||||||
import 'package:fluffychat/pages/chat/send_file_dialog.dart';
|
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
|
||||||
|
|
||||||
void onChatTap(Room room, BuildContext context) async {
|
|
||||||
if (room.membership == Membership.invite) {
|
|
||||||
final inviterId =
|
|
||||||
room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId;
|
|
||||||
final inviteAction = await showModalActionSheet<InviteActions>(
|
|
||||||
context: context,
|
|
||||||
message: room.isDirectChat
|
|
||||||
? L10n.of(context)!.invitePrivateChat
|
|
||||||
: L10n.of(context)!.inviteGroupChat,
|
|
||||||
title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
|
||||||
actions: [
|
|
||||||
SheetAction(
|
|
||||||
key: InviteActions.accept,
|
|
||||||
label: L10n.of(context)!.accept,
|
|
||||||
icon: Icons.check_outlined,
|
|
||||||
isDefaultAction: true,
|
|
||||||
),
|
|
||||||
SheetAction(
|
|
||||||
key: InviteActions.decline,
|
|
||||||
label: L10n.of(context)!.decline,
|
|
||||||
icon: Icons.close_outlined,
|
|
||||||
isDestructiveAction: true,
|
|
||||||
),
|
|
||||||
SheetAction(
|
|
||||||
key: InviteActions.block,
|
|
||||||
label: L10n.of(context)!.block,
|
|
||||||
icon: Icons.block_outlined,
|
|
||||||
isDestructiveAction: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (inviteAction == null) return;
|
|
||||||
if (inviteAction == InviteActions.block) {
|
|
||||||
context.go('/rooms/settings/security/ignorelist', extra: inviterId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (inviteAction == InviteActions.decline) {
|
|
||||||
await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: room.leave,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final joinResult = await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () async {
|
|
||||||
final waitForRoom = room.client.waitForRoomInSync(
|
|
||||||
room.id,
|
|
||||||
join: true,
|
|
||||||
);
|
|
||||||
await room.join();
|
|
||||||
await waitForRoom;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (joinResult.error != null) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (room.membership == Membership.ban) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(L10n.of(context)!.youHaveBeenBannedFromThisChat),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (room.membership == Membership.leave) {
|
|
||||||
context.go('/rooms/archive/${room.id}');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Share content into this room
|
|
||||||
final shareContent = Matrix.of(context).shareContent;
|
|
||||||
if (shareContent != null) {
|
|
||||||
final shareFile = shareContent.tryGet<MatrixFile>('file');
|
|
||||||
if (shareContent.tryGet<String>('msgtype') == 'chat.fluffy.shared_file' &&
|
|
||||||
shareFile != null) {
|
|
||||||
await showDialog(
|
|
||||||
context: context,
|
|
||||||
useRootNavigator: false,
|
|
||||||
builder: (c) => SendFileDialog(
|
|
||||||
files: [shareFile],
|
|
||||||
room: room,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Matrix.of(context).shareContent = null;
|
|
||||||
} else {
|
|
||||||
final consent = await showOkCancelAlertDialog(
|
|
||||||
context: context,
|
|
||||||
title: L10n.of(context)!.forward,
|
|
||||||
message: L10n.of(context)!.forwardMessageTo(
|
|
||||||
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
|
||||||
),
|
|
||||||
okLabel: L10n.of(context)!.forward,
|
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
|
||||||
);
|
|
||||||
if (consent == OkCancelResult.cancel) {
|
|
||||||
Matrix.of(context).shareContent = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (consent == OkCancelResult.ok) {
|
|
||||||
room.sendEvent(shareContent);
|
|
||||||
Matrix.of(context).shareContent = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context.go('/rooms/${room.id}');
|
|
||||||
}
|
|
||||||
|
|
||||||
enum InviteActions {
|
|
||||||
accept,
|
|
||||||
decline,
|
|
||||||
block,
|
|
||||||
}
|
|
|
@ -185,12 +185,6 @@ class SettingsStyleView extends StatelessWidget {
|
||||||
storeKey: SettingKeys.showPresences,
|
storeKey: SettingKeys.showPresences,
|
||||||
defaultValue: AppConfig.showPresences,
|
defaultValue: AppConfig.showPresences,
|
||||||
),
|
),
|
||||||
SettingsSwitchListTile.adaptive(
|
|
||||||
title: L10n.of(context)!.separateChatTypes,
|
|
||||||
onChanged: (b) => AppConfig.separateChatTypes = b,
|
|
||||||
storeKey: SettingKeys.separateChatTypes,
|
|
||||||
defaultValue: AppConfig.separateChatTypes,
|
|
||||||
),
|
|
||||||
Divider(
|
Divider(
|
||||||
height: 1,
|
height: 1,
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
|
|
|
@ -15,6 +15,8 @@ class Avatar extends StatelessWidget {
|
||||||
final Client? client;
|
final Client? client;
|
||||||
final String? presenceUserId;
|
final String? presenceUserId;
|
||||||
final Color? presenceBackgroundColor;
|
final Color? presenceBackgroundColor;
|
||||||
|
final BorderRadius? borderRadius;
|
||||||
|
final IconData? icon;
|
||||||
|
|
||||||
const Avatar({
|
const Avatar({
|
||||||
this.mxContent,
|
this.mxContent,
|
||||||
|
@ -24,6 +26,8 @@ class Avatar extends StatelessWidget {
|
||||||
this.client,
|
this.client,
|
||||||
this.presenceUserId,
|
this.presenceUserId,
|
||||||
this.presenceBackgroundColor,
|
this.presenceBackgroundColor,
|
||||||
|
this.borderRadius,
|
||||||
|
this.icon,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -50,18 +54,25 @@ class Avatar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final borderRadius = BorderRadius.circular(size / 2);
|
final borderRadius = this.borderRadius ?? BorderRadius.circular(size / 2);
|
||||||
final presenceUserId = this.presenceUserId;
|
final presenceUserId = this.presenceUserId;
|
||||||
final color =
|
final color =
|
||||||
noPic ? name?.lightColorAvatar : Theme.of(context).secondaryHeaderColor;
|
noPic ? name?.lightColorAvatar : Theme.of(context).secondaryHeaderColor;
|
||||||
final container = Stack(
|
final container = Stack(
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
SizedBox(
|
||||||
borderRadius: borderRadius,
|
width: size,
|
||||||
child: Container(
|
height: size,
|
||||||
width: size,
|
child: Material(
|
||||||
height: size,
|
|
||||||
color: color,
|
color: color,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
side: BorderSide(
|
||||||
|
width: 0,
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
child: noPic
|
child: noPic
|
||||||
? textWidget
|
? textWidget
|
||||||
: MxcImage(
|
: MxcImage(
|
||||||
|
@ -75,48 +86,49 @@ class Avatar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PresenceBuilder(
|
if (presenceUserId != null)
|
||||||
client: client,
|
PresenceBuilder(
|
||||||
userId: presenceUserId,
|
client: client,
|
||||||
builder: (context, presence) {
|
userId: presenceUserId,
|
||||||
if (presence == null ||
|
builder: (context, presence) {
|
||||||
(presence.presence == PresenceType.offline &&
|
if (presence == null ||
|
||||||
presence.lastActiveTimestamp == null)) {
|
(presence.presence == PresenceType.offline &&
|
||||||
return const SizedBox.shrink();
|
presence.lastActiveTimestamp == null)) {
|
||||||
}
|
return const SizedBox.shrink();
|
||||||
final dotColor = presence.presence.isOnline
|
}
|
||||||
? Colors.green
|
final dotColor = presence.presence.isOnline
|
||||||
: presence.presence.isUnavailable
|
? Colors.green
|
||||||
? Colors.orange
|
: presence.presence.isUnavailable
|
||||||
: Colors.grey;
|
? Colors.orange
|
||||||
return Positioned(
|
: Colors.grey;
|
||||||
bottom: -3,
|
return Positioned(
|
||||||
right: -3,
|
bottom: -3,
|
||||||
child: Container(
|
right: -3,
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: presenceBackgroundColor ??
|
|
||||||
Theme.of(context).colorScheme.surface,
|
|
||||||
borderRadius: BorderRadius.circular(32),
|
|
||||||
),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 10,
|
width: 16,
|
||||||
height: 10,
|
height: 16,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: dotColor,
|
color: presenceBackgroundColor ??
|
||||||
borderRadius: BorderRadius.circular(16),
|
Theme.of(context).colorScheme.surface,
|
||||||
border: Border.all(
|
borderRadius: BorderRadius.circular(32),
|
||||||
width: 1,
|
),
|
||||||
color: Theme.of(context).colorScheme.surface,
|
alignment: Alignment.center,
|
||||||
|
child: Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: dotColor,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
width: 1,
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
if (onTap == null) return container;
|
if (onTap == null) return container;
|
||||||
|
|
|
@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
|
||||||
class TwoColumnLayout extends StatelessWidget {
|
class TwoColumnLayout extends StatelessWidget {
|
||||||
final Widget mainView;
|
final Widget mainView;
|
||||||
final Widget sideView;
|
final Widget sideView;
|
||||||
final bool displayNavigationRail;
|
|
||||||
|
|
||||||
const TwoColumnLayout({
|
const TwoColumnLayout({
|
||||||
super.key,
|
super.key,
|
||||||
required this.mainView,
|
required this.mainView,
|
||||||
required this.sideView,
|
required this.sideView,
|
||||||
required this.displayNavigationRail,
|
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -20,7 +18,7 @@ class TwoColumnLayout extends StatelessWidget {
|
||||||
Container(
|
Container(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
decoration: const BoxDecoration(),
|
decoration: const BoxDecoration(),
|
||||||
width: 360.0 + (displayNavigationRail ? 64 : 0),
|
width: 384.0,
|
||||||
child: mainView,
|
child: mainView,
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
|
|
|
@ -433,10 +433,6 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
||||||
store.getBool(SettingKeys.hideUnimportantStateEvents) ??
|
store.getBool(SettingKeys.hideUnimportantStateEvents) ??
|
||||||
AppConfig.hideUnimportantStateEvents;
|
AppConfig.hideUnimportantStateEvents;
|
||||||
|
|
||||||
AppConfig.separateChatTypes =
|
|
||||||
store.getBool(SettingKeys.separateChatTypes) ??
|
|
||||||
AppConfig.separateChatTypes;
|
|
||||||
|
|
||||||
AppConfig.autoplayImages =
|
AppConfig.autoplayImages =
|
||||||
store.getBool(SettingKeys.autoplayImages) ?? AppConfig.autoplayImages;
|
store.getBool(SettingKeys.autoplayImages) ?? AppConfig.autoplayImages;
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ static void my_application_activate(GApplication* application) {
|
||||||
gtk_window_set_title(window, "FluffyChat");
|
gtk_window_set_title(window, "FluffyChat");
|
||||||
}
|
}
|
||||||
|
|
||||||
gtk_window_set_default_size(window, 864, 680);
|
gtk_window_set_default_size(window, 800, 600);
|
||||||
gtk_widget_show(GTK_WIDGET(window));
|
gtk_widget_show(GTK_WIDGET(window));
|
||||||
|
|
||||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||||
|
|
Loading…
Reference in a new issue