fluffychat merge - resolve conflicts
This commit is contained in:
commit
5ea202062c
16 changed files with 842 additions and 551 deletions
|
|
@ -2,8 +2,8 @@
|
|||
"@@locale": "de",
|
||||
"@@last_modified": "2021-08-14 12:41:10.119255",
|
||||
"alwaysUse24HourFormat": "true",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"description": "Set to true to always display time of day in 24 hour format."
|
||||
"@alwaysUse24HourFormat": {
|
||||
"description": "Set to true to always display time of day in 24 hour format."
|
||||
},
|
||||
"about": "Über",
|
||||
"@about": {
|
||||
|
|
@ -2710,5 +2710,22 @@
|
|||
}
|
||||
},
|
||||
"searchMore": "Weiter suchen ...",
|
||||
"@searchMore": {}
|
||||
"@searchMore": {},
|
||||
"unread": "Ungelesen",
|
||||
"@unread": {},
|
||||
"noMoreChatsFound": "Keine weiteren Chats gefunden ...",
|
||||
"@noMoreChatsFound": {},
|
||||
"joinedChats": "Beigetretene Chats",
|
||||
"@joinedChats": {},
|
||||
"space": "Space",
|
||||
"@space": {},
|
||||
"spaces": "Spaces",
|
||||
"@spaces": {},
|
||||
"goToSpace": "Geh zum Space: {space}",
|
||||
"@goToSpace": {
|
||||
"type": "text",
|
||||
"space": {}
|
||||
},
|
||||
"markAsUnread": "Als ungelesen markieren",
|
||||
"@markAsUnread": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
"@@locale": "en",
|
||||
"@@last_modified": "2021-08-14 12:38:37.885451",
|
||||
"alwaysUse24HourFormat": "false",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"description": "Set to true to always display time of day in 24 hour format."
|
||||
"@alwaysUse24HourFormat": {
|
||||
"description": "Set to true to always display time of day in 24 hour format."
|
||||
},
|
||||
"repeatPassword": "Repeat password",
|
||||
"@repeatPassword": {},
|
||||
|
|
@ -4146,5 +4146,11 @@
|
|||
"error520Title": "Please try again.",
|
||||
"error520Desc": "Sorry, we could not understand your message...",
|
||||
"translationChoicesBody": "Click and hold an option for a hint.",
|
||||
"sendCanceled": "Sending canceled"
|
||||
"sendCanceled": "Sending canceled",
|
||||
"goToSpace": "Go to space: {space}",
|
||||
"@goToSpace": {
|
||||
"type": "text",
|
||||
"space": {}
|
||||
},
|
||||
"markAsUnread": "Mark as unread"
|
||||
}
|
||||
|
|
@ -2708,5 +2708,9 @@
|
|||
"restricted": "Non accesible",
|
||||
"@restricted": {},
|
||||
"swipeRightToLeftToReply": "Despraza hacia a esquerda para responder",
|
||||
"@swipeRightToLeftToReply": {}
|
||||
"@swipeRightToLeftToReply": {},
|
||||
"alwaysUse24HourFormat": "falso",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"description": "Set to true to always display time of day in 24 hour format."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2708,5 +2708,9 @@
|
|||
"count": {}
|
||||
},
|
||||
"swipeRightToLeftToReply": "Za odgovaranje povuci prstom zdesna ulijevo",
|
||||
"@swipeRightToLeftToReply": {}
|
||||
"@swipeRightToLeftToReply": {},
|
||||
"alwaysUse24HourFormat": "true",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"description": "Set to true to always display time of day in 24 hour format."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,8 +136,12 @@ abstract class AppRoutes {
|
|||
FluffyThemes.isColumnMode(context) &&
|
||||
state.fullPath?.startsWith('/rooms/settings') == false
|
||||
? TwoColumnLayout(
|
||||
displayNavigationRail:
|
||||
state.path?.startsWith('/rooms/settings') != true,
|
||||
mainView: ChatList(
|
||||
activeChat: state.pathParameters['roomid'],
|
||||
displayNavigationRail:
|
||||
state.path?.startsWith('/rooms/settings') != true,
|
||||
),
|
||||
sideView: child,
|
||||
)
|
||||
|
|
@ -280,6 +284,7 @@ abstract class AppRoutes {
|
|||
? TwoColumnLayout(
|
||||
mainView: const Settings(),
|
||||
sideView: child,
|
||||
displayNavigationRail: false,
|
||||
)
|
||||
: child,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -202,15 +202,17 @@ class ChatView extends StatelessWidget {
|
|||
tooltip: L10n.of(context)!.close,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: UnreadRoomsBadge(
|
||||
filter: (r) =>
|
||||
r.id != controller.roomId
|
||||
// #Pangea
|
||||
&&
|
||||
!r.isAnalyticsRoom,
|
||||
// Pangea#
|
||||
badgePosition: BadgePosition.topEnd(end: 8, top: 4),
|
||||
child: const Center(child: BackButton()),
|
||||
: StreamBuilder<Object>(
|
||||
stream: Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where((syncUpdate) => syncUpdate.hasRoomUpdate),
|
||||
builder: (context, _) => UnreadRoomsBadge(
|
||||
filter: (r) => r.id != controller.roomId,
|
||||
badgePosition: BadgePosition.topEnd(end: 8, top: 4),
|
||||
child: const Center(child: BackButton()),
|
||||
),
|
||||
),
|
||||
titleSpacing: 0,
|
||||
title: ChatAppBarTitle(controller),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
|||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/utils/tor_stub.dart'
|
||||
if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -76,10 +77,12 @@ extension LocalizedActiveFilter on ActiveFilter {
|
|||
class ChatList extends StatefulWidget {
|
||||
static BuildContext? contextForVoip;
|
||||
final String? activeChat;
|
||||
final bool displayNavigationRail;
|
||||
|
||||
const ChatList({
|
||||
super.key,
|
||||
required this.activeChat,
|
||||
this.displayNavigationRail = false,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -207,11 +210,7 @@ class ChatListController extends State<ChatList>
|
|||
if (result.error != null) return;
|
||||
}
|
||||
|
||||
void onChatTap(Room room, BuildContext context) async {
|
||||
if (room.isSpace) {
|
||||
setActiveSpace(room.id);
|
||||
return;
|
||||
}
|
||||
void onChatTap(Room room) async {
|
||||
if (room.membership == Membership.invite) {
|
||||
final inviterId =
|
||||
room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId;
|
||||
|
|
@ -282,6 +281,10 @@ class ChatListController extends State<ChatList>
|
|||
return;
|
||||
}
|
||||
|
||||
if (room.isSpace) {
|
||||
setActiveSpace(room.id);
|
||||
return;
|
||||
}
|
||||
// Share content into this room
|
||||
final shareContent = Matrix.of(context).shareContent;
|
||||
if (shareContent != null) {
|
||||
|
|
@ -725,41 +728,138 @@ class ChatListController extends State<ChatList>
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
void chatContextAction(Room room) async {
|
||||
final action = await showModalActionSheet<ChatContextAction>(
|
||||
context: context,
|
||||
title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
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,
|
||||
void chatContextAction(
|
||||
Room room,
|
||||
BuildContext posContext, [
|
||||
Room? space,
|
||||
]) async {
|
||||
final overlay =
|
||||
Overlay.of(posContext).context.findRenderObject() as RenderBox;
|
||||
|
||||
final button = posContext.findRenderObject() as RenderBox;
|
||||
|
||||
final position = RelativeRect.fromRect(
|
||||
Rect.fromPoints(
|
||||
button.localToGlobal(const Offset(0, -65), ancestor: overlay),
|
||||
button.localToGlobal(
|
||||
button.size.bottomRight(Offset.zero) + const Offset(-50, 0),
|
||||
ancestor: overlay,
|
||||
),
|
||||
SheetAction(
|
||||
key: ChatContextAction.favorite,
|
||||
icon: room.isFavourite ? Icons.pin : Icons.pin_outlined,
|
||||
label: room.isFavourite
|
||||
? L10n.of(context)!.unpin
|
||||
: L10n.of(context)!.pin,
|
||||
),
|
||||
Offset.zero & overlay.size,
|
||||
);
|
||||
|
||||
final displayname =
|
||||
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!));
|
||||
|
||||
final action = await showMenu<ChatContextAction>(
|
||||
context: posContext,
|
||||
position: position,
|
||||
items: [
|
||||
PopupMenuItem(
|
||||
enabled: false,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: room.avatar,
|
||||
size: Avatar.defaultSize / 2,
|
||||
name: displayname,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
displayname,
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
const PopupMenuDivider(),
|
||||
if (space != null)
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.goToSpace,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: space.avatar,
|
||||
size: Avatar.defaultSize / 2,
|
||||
name: space.getLocalizedDisplayname(),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context)!
|
||||
.goToSpace(space.getLocalizedDisplayname()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.mute,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
room.pushRuleState == PushRuleState.notify
|
||||
? Icons.notifications_off_outlined
|
||||
: Icons.notifications_off,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
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,
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.markUnread,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
room.markedUnread
|
||||
? Icons.mark_as_unread
|
||||
: Icons.mark_as_unread_outlined,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
room.markedUnread
|
||||
? L10n.of(context)!.markAsRead
|
||||
: L10n.of(context)!.markAsUnread,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.favorite,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
room.isFavourite
|
||||
? L10n.of(context)!.unpin
|
||||
: L10n.of(context)!.pin,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.leave,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.delete_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.leave),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -767,10 +867,31 @@ class ChatListController extends State<ChatList>
|
|||
if (action == null) return;
|
||||
if (!mounted) return;
|
||||
|
||||
if (action == ChatContextAction.goToSpace) {
|
||||
setActiveSpace(space!.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == ChatContextAction.leave) {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.areYouSure,
|
||||
okLabel: L10n.of(context)!.leave,
|
||||
cancelLabel: L10n.of(context)!.no,
|
||||
message: L10n.of(context)!.archiveRoomDescription,
|
||||
isDestructiveAction: true,
|
||||
);
|
||||
if (confirmed == OkCancelResult.cancel) return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () {
|
||||
future: () async {
|
||||
switch (action) {
|
||||
case ChatContextAction.goToSpace:
|
||||
return;
|
||||
case ChatContextAction.favorite:
|
||||
return room.setFavourite(!room.isFavourite);
|
||||
case ChatContextAction.markUnread:
|
||||
|
|
@ -1015,6 +1136,7 @@ enum InviteActions {
|
|||
enum AddRoomType { chat, subspace }
|
||||
|
||||
enum ChatContextAction {
|
||||
goToSpace,
|
||||
favorite,
|
||||
markUnread,
|
||||
mute,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:fluffychat/pangea/widgets/chat_list/chat_list_body_text.dart';
|
|||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/stream_extension.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/hover_builder.dart';
|
||||
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
@ -25,17 +26,29 @@ class ChatListViewBody extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
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),
|
||||
onChatTab: (room) => controller.onChatTap(room),
|
||||
onChatContext: (room, context) =>
|
||||
controller.chatContextAction(room, context),
|
||||
activeChat: controller.activeChat,
|
||||
toParentSpace: controller.setActiveSpace,
|
||||
);
|
||||
}
|
||||
final spaces = client.rooms.where((r) => r.isSpace);
|
||||
final spaceDelegateCandidates = <String, Room>{};
|
||||
for (final space in spaces) {
|
||||
for (final spaceChild in space.spaceChildren) {
|
||||
final roomId = spaceChild.roomId;
|
||||
if (roomId == null) continue;
|
||||
spaceDelegateCandidates[roomId] = space;
|
||||
}
|
||||
}
|
||||
|
||||
final publicRooms = controller.roomSearchResult?.chunk
|
||||
.where((room) => room.roomType != 'm.space')
|
||||
.toList();
|
||||
|
|
@ -43,7 +56,6 @@ class ChatListViewBody extends StatelessWidget {
|
|||
.where((room) => room.roomType == 'm.space')
|
||||
.toList();
|
||||
final userSearchResult = controller.userSearchResult;
|
||||
final client = Matrix.of(context).client;
|
||||
const dummyChatCount = 4;
|
||||
final titleColor =
|
||||
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100);
|
||||
|
|
@ -60,18 +72,6 @@ class ChatListViewBody extends StatelessWidget {
|
|||
builder: (context, _) {
|
||||
final rooms = controller.filteredRooms;
|
||||
|
||||
final spaces = rooms.where((r) => r.isSpace);
|
||||
final spaceDelegateCandidates = <String, Room>{};
|
||||
for (final space in spaces) {
|
||||
spaceDelegateCandidates[space.id] = space;
|
||||
for (final spaceChild in space.spaceChildren) {
|
||||
final roomId = spaceChild.roomId;
|
||||
if (roomId == null) continue;
|
||||
spaceDelegateCandidates[roomId] = space;
|
||||
}
|
||||
}
|
||||
final spaceDelegates = <String>{};
|
||||
|
||||
return SafeArea(
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
|
|
@ -164,50 +164,66 @@ class ChatListViewBody extends StatelessWidget {
|
|||
),
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: ActiveFilter.values
|
||||
children: [
|
||||
ActiveFilter.allChats,
|
||||
ActiveFilter.unread,
|
||||
ActiveFilter.groups,
|
||||
if (spaceDelegateCandidates.isNotEmpty &&
|
||||
!controller.widget.displayNavigationRail)
|
||||
ActiveFilter.spaces,
|
||||
]
|
||||
.map(
|
||||
(filter) => Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
onTap: () =>
|
||||
controller.setActiveFilter(filter),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: filter == controller.activeFilter
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer,
|
||||
child: HoverBuilder(
|
||||
builder: (context, hovered) =>
|
||||
AnimatedScale(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
scale: hovered ? 1.1 : 1.0,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
filter.toLocalizedString(context),
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
filter == controller.activeFilter
|
||||
onTap: () =>
|
||||
controller.setActiveFilter(filter),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: filter ==
|
||||
controller.activeFilter
|
||||
? Theme.of(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
|
||||
color: filter ==
|
||||
controller.activeFilter
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -305,27 +321,16 @@ class ChatListViewBody extends StatelessWidget {
|
|||
SliverList.builder(
|
||||
itemCount: rooms.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
var room = rooms[i];
|
||||
if (controller.activeFilter != ActiveFilter.groups) {
|
||||
final parent = room.isSpace
|
||||
? room
|
||||
: spaceDelegateCandidates[room.id];
|
||||
if (parent != null) {
|
||||
if (spaceDelegates.contains(parent.id)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
spaceDelegates.add(parent.id);
|
||||
room = parent;
|
||||
}
|
||||
}
|
||||
|
||||
final room = rooms[i];
|
||||
final space = spaceDelegateCandidates[room.id];
|
||||
return ChatListItem(
|
||||
room,
|
||||
lastEventRoom: rooms[i],
|
||||
space: space,
|
||||
key: Key('chat_list_item_${room.id}'),
|
||||
filter: filter,
|
||||
onTap: () => controller.onChatTap(room, context),
|
||||
onLongPress: () => controller.chatContextAction(room),
|
||||
onTap: () => controller.onChatTap(room),
|
||||
onLongPress: (context) =>
|
||||
controller.chatContextAction(room, context, space),
|
||||
activeChat: controller.activeChat == room.id,
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ enum ArchivedRoomAction { delete, rejoin }
|
|||
|
||||
class ChatListItem extends StatelessWidget {
|
||||
final Room room;
|
||||
final Room? lastEventRoom;
|
||||
final Room? space;
|
||||
final bool activeChat;
|
||||
final void Function()? onLongPress;
|
||||
final void Function(BuildContext context)? onLongPress;
|
||||
final void Function()? onForget;
|
||||
final void Function() onTap;
|
||||
final String? filter;
|
||||
|
|
@ -33,7 +33,7 @@ class ChatListItem extends StatelessWidget {
|
|||
this.onLongPress,
|
||||
this.onForget,
|
||||
this.filter,
|
||||
this.lastEventRoom,
|
||||
this.space,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -66,21 +66,19 @@ class ChatListItem extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMuted = room.pushRuleState != PushRuleState.notify;
|
||||
final lastEventRoom = this.lastEventRoom ?? room;
|
||||
final typingText = lastEventRoom.getLocalizedTypingText(context);
|
||||
final lastEvent = lastEventRoom.lastEvent;
|
||||
final typingText = room.getLocalizedTypingText(context);
|
||||
final lastEvent = room.lastEvent;
|
||||
final ownMessage = lastEvent?.senderId == room.client.userID;
|
||||
final unread =
|
||||
lastEventRoom.isUnread || lastEventRoom.membership == Membership.invite;
|
||||
final unread = room.isUnread || room.membership == Membership.invite;
|
||||
final theme = Theme.of(context);
|
||||
final directChatMatrixId = room.directChatMatrixID;
|
||||
final isDirectChat = directChatMatrixId != null;
|
||||
final unreadBubbleSize = unread || lastEventRoom.hasNewMessages
|
||||
? lastEventRoom.notificationCount > 0
|
||||
final unreadBubbleSize = unread || room.hasNewMessages
|
||||
? room.notificationCount > 0
|
||||
? 20.0
|
||||
: 14.0
|
||||
: 0.0;
|
||||
final hasNotifications = lastEventRoom.notificationCount > 0;
|
||||
final hasNotifications = room.notificationCount > 0;
|
||||
final backgroundColor =
|
||||
activeChat ? theme.colorScheme.secondaryContainer : null;
|
||||
final displayname = room.getLocalizedDisplayname(
|
||||
|
|
@ -94,6 +92,7 @@ class ChatListItem extends StatelessWidget {
|
|||
final needLastEventSender = lastEvent == null
|
||||
? false
|
||||
: room.getState(EventTypes.RoomMember, lastEvent.senderId) == null;
|
||||
final space = this.space;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
|
|
@ -107,48 +106,93 @@ class ChatListItem extends StatelessWidget {
|
|||
child: FutureBuilder(
|
||||
future: room.loadHeroUsers(),
|
||||
builder: (context, snapshot) => HoverBuilder(
|
||||
builder: (context, hovered) => ListTile(
|
||||
builder: (context, listTileHovered) => ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -0.5),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
onLongPress: onLongPress,
|
||||
leading: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
HoverBuilder(
|
||||
builder: (context, hovered) => AnimatedScale(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
scale: hovered ? 1.1 : 1.0,
|
||||
child: Avatar(
|
||||
borderRadius: room.isSpace
|
||||
? BorderRadius.circular(AppConfig.borderRadius / 3)
|
||||
: null,
|
||||
mxContent: room.avatar,
|
||||
name: displayname,
|
||||
presenceUserId: directChatMatrixId,
|
||||
presenceBackgroundColor: backgroundColor,
|
||||
onTap: onLongPress,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -2,
|
||||
right: -2,
|
||||
child: AnimatedScale(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
scale: (hovered) ? 1.0 : 0.0,
|
||||
child: Material(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: const Icon(
|
||||
Icons.check_circle_outlined,
|
||||
size: 18,
|
||||
onLongPress: () => onLongPress?.call(context),
|
||||
leading: HoverBuilder(
|
||||
builder: (context, hovered) => AnimatedScale(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
scale: hovered ? 1.1 : 1.0,
|
||||
child: SizedBox(
|
||||
width: Avatar.defaultSize,
|
||||
height: Avatar.defaultSize,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (space != null)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: Avatar(
|
||||
border: BorderSide(
|
||||
width: 2,
|
||||
color: backgroundColor ??
|
||||
Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius / 4,
|
||||
),
|
||||
mxContent: space.avatar,
|
||||
size: Avatar.defaultSize * 0.75,
|
||||
name: space.getLocalizedDisplayname(),
|
||||
onTap: () => onLongPress?.call(context),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Avatar(
|
||||
border: space == null
|
||||
? room.isSpace
|
||||
? BorderSide(
|
||||
width: 0,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
)
|
||||
: null
|
||||
: BorderSide(
|
||||
width: 2,
|
||||
color: backgroundColor ??
|
||||
Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
borderRadius: room.isSpace
|
||||
? BorderRadius.circular(
|
||||
AppConfig.borderRadius / 4,
|
||||
)
|
||||
: null,
|
||||
mxContent: room.avatar,
|
||||
size: space != null
|
||||
? Avatar.defaultSize * 0.75
|
||||
: Avatar.defaultSize,
|
||||
name: displayname,
|
||||
presenceUserId: directChatMatrixId,
|
||||
presenceBackgroundColor: backgroundColor,
|
||||
onTap: () => onLongPress?.call(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: AnimatedScale(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
scale: listTileHovered ? 1.0 : 0.0,
|
||||
child: Material(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: const Icon(
|
||||
Icons.arrow_drop_down_circle_outlined,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
children: <Widget>[
|
||||
|
|
@ -207,20 +251,6 @@ class ChatListItem extends StatelessWidget {
|
|||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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 &&
|
||||
ownMessage &&
|
||||
room.lastEvent!.status.isSending) ...[
|
||||
|
|
@ -245,7 +275,7 @@ class ChatListItem extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
Expanded(
|
||||
child: room.isSpace && !lastEventRoom.isUnreadOrInvited
|
||||
child: room.isSpace && room.membership == Membership.join
|
||||
? Text(
|
||||
L10n.of(context)!.countChatsAndCountParticipants(
|
||||
room.spaceChildren.length.toString(),
|
||||
|
|
@ -308,10 +338,9 @@ class ChatListItem extends StatelessWidget {
|
|||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
unread || lastEventRoom.hasNewMessages
|
||||
? FontWeight.bold
|
||||
: null,
|
||||
fontWeight: unread || room.hasNewMessages
|
||||
? FontWeight.bold
|
||||
: null,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
decoration: room.lastEvent?.redacted == true
|
||||
? TextDecoration.lineThrough
|
||||
|
|
@ -339,9 +368,7 @@ class ChatListItem extends StatelessWidget {
|
|||
width: !hasNotifications && !unread && !room.hasNewMessages
|
||||
? 0
|
||||
: (unreadBubbleSize - 9) *
|
||||
lastEventRoom.notificationCount
|
||||
.toString()
|
||||
.length +
|
||||
room.notificationCount.toString().length +
|
||||
9,
|
||||
decoration: BoxDecoration(
|
||||
color: room.highlightCount > 0 ||
|
||||
|
|
@ -356,7 +383,7 @@ class ChatListItem extends StatelessWidget {
|
|||
child: Center(
|
||||
child: hasNotifications
|
||||
? Text(
|
||||
lastEventRoom.notificationCount.toString(),
|
||||
room.notificationCount.toString(),
|
||||
style: TextStyle(
|
||||
color: room.highlightCount > 0
|
||||
? Colors.white
|
||||
|
|
|
|||
95
lib/pages/chat_list/nav_rail_item.dart
Normal file
95
lib/pages/chat_list/nav_rail_item.dart
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import '../../config/themes.dart';
|
||||
|
||||
class NaviRailItem extends StatefulWidget {
|
||||
final String toolTip;
|
||||
final bool isSelected;
|
||||
final void Function() onTap;
|
||||
final Widget icon;
|
||||
final Widget? selectedIcon;
|
||||
|
||||
const NaviRailItem({
|
||||
required this.toolTip,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
required this.icon,
|
||||
this.selectedIcon,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NaviRailItem> createState() => _NaviRailItemState();
|
||||
}
|
||||
|
||||
class _NaviRailItemState extends State<NaviRailItem> {
|
||||
bool _hovered = false;
|
||||
|
||||
void _onHover(bool hover) {
|
||||
if (hover == _hovered) return;
|
||||
setState(() {
|
||||
_hovered = hover;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderRadius = BorderRadius.circular(AppConfig.borderRadius);
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
width: 64,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
child: AnimatedContainer(
|
||||
width: widget.isSelected ? 4 : 0,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(90),
|
||||
bottomRight: Radius.circular(90),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: AnimatedScale(
|
||||
scale: _hovered ? 1.2 : 1.0,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: Material(
|
||||
borderRadius: borderRadius,
|
||||
color: widget.isSelected
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
child: Tooltip(
|
||||
message: widget.toolTip,
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
onTap: widget.onTap,
|
||||
onHover: _onHover,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: widget.isSelected
|
||||
? widget.selectedIcon ?? widget.icon
|
||||
: widget.icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,20 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:badges/badges.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/widgets/hover_builder.dart';
|
||||
import 'package:fluffychat/widgets/unread_rooms_badge.dart';
|
||||
import '../../config/themes.dart';
|
||||
|
||||
class NaviRailItem extends StatefulWidget {
|
||||
class NaviRailItem extends StatelessWidget {
|
||||
final String toolTip;
|
||||
final bool isSelected;
|
||||
final void Function() onTap;
|
||||
final Widget icon;
|
||||
final Widget? selectedIcon;
|
||||
final bool Function(Room)? unreadBadgeFilter;
|
||||
|
||||
const NaviRailItem({
|
||||
required this.toolTip,
|
||||
|
|
@ -16,80 +22,78 @@ class NaviRailItem extends StatefulWidget {
|
|||
required this.onTap,
|
||||
required this.icon,
|
||||
this.selectedIcon,
|
||||
this.unreadBadgeFilter,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NaviRailItem> createState() => _NaviRailItemState();
|
||||
}
|
||||
|
||||
class _NaviRailItemState extends State<NaviRailItem> {
|
||||
bool _hovered = false;
|
||||
|
||||
void _onHover(bool hover) {
|
||||
if (hover == _hovered) return;
|
||||
setState(() {
|
||||
_hovered = hover;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderRadius = BorderRadius.circular(AppConfig.borderRadius);
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
width: 64,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
child: AnimatedContainer(
|
||||
width: widget.isSelected ? 4 : 0,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(90),
|
||||
bottomRight: Radius.circular(90),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: AnimatedScale(
|
||||
scale: _hovered ? 1.2 : 1.0,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: Material(
|
||||
borderRadius: borderRadius,
|
||||
color: widget.isSelected
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
child: Tooltip(
|
||||
message: widget.toolTip,
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
onTap: widget.onTap,
|
||||
onHover: _onHover,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: widget.isSelected
|
||||
? widget.selectedIcon ?? widget.icon
|
||||
: widget.icon,
|
||||
final icon = isSelected ? selectedIcon ?? this.icon : this.icon;
|
||||
final unreadBadgeFilter = this.unreadBadgeFilter;
|
||||
return HoverBuilder(
|
||||
builder: (context, hovered) {
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
width: 64,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
child: AnimatedContainer(
|
||||
width: isSelected ? 4 : 0,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(90),
|
||||
bottomRight: Radius.circular(90),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: AnimatedScale(
|
||||
scale: hovered ? 1.2 : 1.0,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: Material(
|
||||
borderRadius: borderRadius,
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
child: Tooltip(
|
||||
message: toolTip,
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: unreadBadgeFilter == null
|
||||
? icon
|
||||
: UnreadRoomsBadge(
|
||||
filter: unreadBadgeFilter,
|
||||
badgePosition: BadgePosition.topEnd(
|
||||
top: -12,
|
||||
end: -8,
|
||||
),
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class SpaceView extends StatefulWidget {
|
|||
final void Function() onBack;
|
||||
final void Function(String spaceId) toParentSpace;
|
||||
final void Function(Room room) onChatTab;
|
||||
final void Function(Room room) onChatContext;
|
||||
final void Function(Room room, BuildContext context) onChatContext;
|
||||
final String? activeChat;
|
||||
|
||||
const SpaceView({
|
||||
|
|
@ -163,316 +163,315 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
final room = Matrix.of(context).client.getRoomById(widget.spaceId);
|
||||
final displayname =
|
||||
room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: Center(
|
||||
child: CloseButton(
|
||||
onPressed: widget.onBack,
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvoked: (_) => widget.onBack(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: Center(
|
||||
child: CloseButton(
|
||||
onPressed: widget.onBack,
|
||||
),
|
||||
),
|
||||
),
|
||||
titleSpacing: 0,
|
||||
title: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Avatar(
|
||||
mxContent: room?.avatar,
|
||||
name: displayname,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
title: Text(
|
||||
displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: room == null
|
||||
? null
|
||||
: Text(
|
||||
L10n.of(context)!.countChatsAndCountParticipants(
|
||||
room.spaceChildren.length,
|
||||
room.summary.mJoinedMemberCount ?? 1,
|
||||
titleSpacing: 0,
|
||||
title: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Avatar(
|
||||
mxContent: room?.avatar,
|
||||
name: displayname,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
title: Text(
|
||||
displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: room == null
|
||||
? null
|
||||
: Text(
|
||||
L10n.of(context)!.countChatsAndCountParticipants(
|
||||
room.spaceChildren.length,
|
||||
room.summary.mJoinedMemberCount ?? 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton<SpaceActions>(
|
||||
onSelected: _onSpaceAction,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.settings,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.settings_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.settings),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.invite,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.person_add_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.invite),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.leave,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.delete_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.leave),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: room == null
|
||||
? const Center(
|
||||
child: Icon(
|
||||
Icons.search_outlined,
|
||||
size: 80,
|
||||
),
|
||||
)
|
||||
: StreamBuilder(
|
||||
stream: room.client.onSync.stream
|
||||
.where((s) => s.hasRoomUpdate)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, snapshot) {
|
||||
final joinedRooms = room.spaceChildren
|
||||
.map((child) {
|
||||
final roomId = child.roomId;
|
||||
if (roomId == null) return null;
|
||||
return room.client.getRoomById(roomId);
|
||||
})
|
||||
.whereType<Room>()
|
||||
.where((room) => room.membership != Membership.leave)
|
||||
.toList();
|
||||
|
||||
// Sort rooms by last activity
|
||||
joinedRooms.sort(
|
||||
(b, a) => (a.lastEvent?.originServerTs ??
|
||||
DateTime.fromMillisecondsSinceEpoch(0))
|
||||
.compareTo(
|
||||
b.lastEvent?.originServerTs ??
|
||||
DateTime.fromMillisecondsSinceEpoch(0),
|
||||
actions: [
|
||||
PopupMenuButton<SpaceActions>(
|
||||
onSelected: _onSpaceAction,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.settings,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.settings_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.settings),
|
||||
],
|
||||
),
|
||||
);
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.invite,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.person_add_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.invite),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.leave,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.delete_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.leave),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: room == null
|
||||
? const Center(
|
||||
child: Icon(
|
||||
Icons.search_outlined,
|
||||
size: 80,
|
||||
),
|
||||
)
|
||||
: StreamBuilder(
|
||||
stream: room.client.onSync.stream
|
||||
.where((s) => s.hasRoomUpdate)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, snapshot) {
|
||||
final childrenIds = room.spaceChildren
|
||||
.map((c) => c.roomId)
|
||||
.whereType<String>()
|
||||
.toSet();
|
||||
|
||||
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,
|
||||
hintStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
final joinedRooms = room.client.rooms
|
||||
.where((room) => childrenIds.remove(room.id))
|
||||
.toList();
|
||||
|
||||
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
|
||||
.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
prefixIcon: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
Icons.search_outlined,
|
||||
.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: L10n.of(context)!.search,
|
||||
hintStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
prefixIcon: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
Icons.search_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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: joinedParents.length,
|
||||
itemBuilder: (context, i) {
|
||||
final displayname =
|
||||
joinedParents[i].getLocalizedDisplayname();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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),
|
||||
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: (context) => widget.onChatContext(
|
||||
room,
|
||||
context,
|
||||
),
|
||||
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: 12.0,
|
||||
vertical: 2.0,
|
||||
horizontal: 8,
|
||||
vertical: 1,
|
||||
),
|
||||
child: TextButton(
|
||||
onPressed: _isLoading ? null : _loadHierarchy,
|
||||
child: _isLoading
|
||||
? LinearProgressIndicator(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
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,
|
||||
),
|
||||
)
|
||||
: Text(L10n.of(context)!.loadMore),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class Avatar extends StatelessWidget {
|
|||
final Color? presenceBackgroundColor;
|
||||
final BorderRadius? borderRadius;
|
||||
final IconData? icon;
|
||||
final BorderSide? border;
|
||||
|
||||
const Avatar({
|
||||
this.mxContent,
|
||||
|
|
@ -25,6 +26,7 @@ class Avatar extends StatelessWidget {
|
|||
this.presenceUserId,
|
||||
this.presenceBackgroundColor,
|
||||
this.borderRadius,
|
||||
this.border,
|
||||
this.icon,
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -65,10 +67,7 @@ class Avatar extends StatelessWidget {
|
|||
color: color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
side: BorderSide(
|
||||
width: 0,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
side: border ?? BorderSide.none,
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: noPic
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import 'package:flutter/material.dart';
|
|||
class TwoColumnLayout extends StatelessWidget {
|
||||
final Widget mainView;
|
||||
final Widget sideView;
|
||||
final bool displayNavigationRail;
|
||||
|
||||
const TwoColumnLayout({
|
||||
super.key,
|
||||
required this.mainView,
|
||||
required this.sideView,
|
||||
required this.displayNavigationRail,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -18,7 +20,7 @@ class TwoColumnLayout extends StatelessWidget {
|
|||
Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: const BoxDecoration(),
|
||||
width: 384.0,
|
||||
width: 360.0 + (displayNavigationRail ? 64 : 0),
|
||||
child: mainView,
|
||||
),
|
||||
Container(
|
||||
|
|
|
|||
|
|
@ -30,12 +30,6 @@ class UnreadRoomsBadge extends StatelessWidget {
|
|||
// Pangea#
|
||||
builder: (context, _) {
|
||||
// #Pangea
|
||||
// final unreadCount = Matrix.of(context)
|
||||
// .client
|
||||
// .rooms
|
||||
// .where(filter)
|
||||
// .where((r) => (r.isUnread || r.membership == Membership.invite))
|
||||
// .length;
|
||||
final unreadCounts = Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
|
|
@ -44,6 +38,12 @@ class UnreadRoomsBadge extends StatelessWidget {
|
|||
.map((r) => r.notificationCount);
|
||||
final unreadCount =
|
||||
unreadCounts.isEmpty ? 0 : unreadCounts.reduce((a, b) => a + b);
|
||||
// final unreadCount = Matrix.of(context)
|
||||
// .client
|
||||
// .rooms
|
||||
// .where(filter)
|
||||
// .where((r) => (r.isUnread || r.membership == Membership.invite))
|
||||
// .length;
|
||||
// Pangea#
|
||||
return b.Badge(
|
||||
badgeStyle: b.BadgeStyle(
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ static void my_application_activate(GApplication* application) {
|
|||
gtk_window_set_title(window, "FluffyChat");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 800, 600);
|
||||
gtk_window_set_default_size(window, 864, 680);
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue