fluffychat merge - resolve conflicts

pull/1384/head
ggurdin 1 year ago
commit 5ea202062c
No known key found for this signature in database
GPG Key ID: A01CB41737CBB478

@ -2,8 +2,8 @@
"@@locale": "de", "@@locale": "de",
"@@last_modified": "2021-08-14 12:41:10.119255", "@@last_modified": "2021-08-14 12:41:10.119255",
"alwaysUse24HourFormat": "true", "alwaysUse24HourFormat": "true",
"@alwaysUse24HourFormat": { "@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format." "description": "Set to true to always display time of day in 24 hour format."
}, },
"about": "Über", "about": "Über",
"@about": { "@about": {
@ -2710,5 +2710,22 @@
} }
}, },
"searchMore": "Weiter suchen ...", "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", "@@locale": "en",
"@@last_modified": "2021-08-14 12:38:37.885451", "@@last_modified": "2021-08-14 12:38:37.885451",
"alwaysUse24HourFormat": "false", "alwaysUse24HourFormat": "false",
"@alwaysUse24HourFormat": { "@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format." "description": "Set to true to always display time of day in 24 hour format."
}, },
"repeatPassword": "Repeat password", "repeatPassword": "Repeat password",
"@repeatPassword": {}, "@repeatPassword": {},
@ -4146,5 +4146,11 @@
"error520Title": "Please try again.", "error520Title": "Please try again.",
"error520Desc": "Sorry, we could not understand your message...", "error520Desc": "Sorry, we could not understand your message...",
"translationChoicesBody": "Click and hold an option for a hint.", "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": "Non accesible",
"@restricted": {}, "@restricted": {},
"swipeRightToLeftToReply": "Despraza hacia a esquerda para responder", "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": {} "count": {}
}, },
"swipeRightToLeftToReply": "Za odgovaranje povuci prstom zdesna ulijevo", "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) && 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,
) )
@ -280,6 +284,7 @@ abstract class AppRoutes {
? TwoColumnLayout( ? TwoColumnLayout(
mainView: const Settings(), mainView: const Settings(),
sideView: child, sideView: child,
displayNavigationRail: false,
) )
: child, : child,
), ),

@ -202,15 +202,17 @@ class ChatView extends StatelessWidget {
tooltip: L10n.of(context)!.close, tooltip: L10n.of(context)!.close,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
) )
: UnreadRoomsBadge( : StreamBuilder<Object>(
filter: (r) => stream: Matrix.of(context)
r.id != controller.roomId .client
// #Pangea .onSync
&& .stream
!r.isAnalyticsRoom, .where((syncUpdate) => syncUpdate.hasRoomUpdate),
// Pangea# builder: (context, _) => UnreadRoomsBadge(
badgePosition: BadgePosition.topEnd(end: 8, top: 4), filter: (r) => r.id != controller.roomId,
child: const Center(child: BackButton()), badgePosition: BadgePosition.topEnd(end: 8, top: 4),
child: const Center(child: BackButton()),
),
), ),
titleSpacing: 0, titleSpacing: 0,
title: ChatAppBarTitle(controller), 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/platform_infos.dart';
import 'package:fluffychat/utils/tor_stub.dart' import 'package:fluffychat/utils/tor_stub.dart'
if (dart.library.html) 'package:tor_detector_web/tor_detector_web.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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -76,10 +77,12 @@ extension LocalizedActiveFilter on ActiveFilter {
class ChatList extends StatefulWidget { class ChatList extends StatefulWidget {
static BuildContext? contextForVoip; static BuildContext? contextForVoip;
final String? activeChat; final String? activeChat;
final bool displayNavigationRail;
const ChatList({ const ChatList({
super.key, super.key,
required this.activeChat, required this.activeChat,
this.displayNavigationRail = false,
}); });
@override @override
@ -207,11 +210,7 @@ class ChatListController extends State<ChatList>
if (result.error != null) return; if (result.error != null) return;
} }
void onChatTap(Room room, BuildContext context) async { void onChatTap(Room room) async {
if (room.isSpace) {
setActiveSpace(room.id);
return;
}
if (room.membership == Membership.invite) { if (room.membership == Membership.invite) {
final inviterId = final inviterId =
room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId; room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId;
@ -282,6 +281,10 @@ class ChatListController extends State<ChatList>
return; return;
} }
if (room.isSpace) {
setActiveSpace(room.id);
return;
}
// Share content into this room // Share content into this room
final shareContent = Matrix.of(context).shareContent; final shareContent = Matrix.of(context).shareContent;
if (shareContent != null) { if (shareContent != null) {
@ -725,41 +728,138 @@ class ChatListController extends State<ChatList>
super.dispose(); super.dispose();
} }
void chatContextAction(Room room) async { void chatContextAction(
final action = await showModalActionSheet<ChatContextAction>( Room room,
context: context, BuildContext posContext, [
title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), Room? space,
actions: [ ]) async {
SheetAction( final overlay =
key: ChatContextAction.markUnread, Overlay.of(posContext).context.findRenderObject() as RenderBox;
icon: room.markedUnread
? Icons.mark_as_unread final button = posContext.findRenderObject() as RenderBox;
: Icons.mark_as_unread_outlined,
label: room.markedUnread final position = RelativeRect.fromRect(
? L10n.of(context)!.markAsRead Rect.fromPoints(
: L10n.of(context)!.unread, 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, Offset.zero & overlay.size,
icon: room.isFavourite ? Icons.pin : Icons.pin_outlined, );
label: room.isFavourite
? L10n.of(context)!.unpin final displayname =
: L10n.of(context)!.pin, 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( const PopupMenuDivider(),
key: ChatContextAction.mute, if (space != null)
icon: room.pushRuleState == PushRuleState.notify PopupMenuItem(
? Icons.notifications_off_outlined value: ChatContextAction.goToSpace,
: Icons.notifications, child: Row(
label: room.pushRuleState == PushRuleState.notify mainAxisSize: MainAxisSize.min,
? L10n.of(context)!.muteChat children: [
: L10n.of(context)!.unmuteChat, 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,
),
],
),
),
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,
),
],
),
), ),
SheetAction( PopupMenuItem(
isDestructiveAction: true, value: ChatContextAction.leave,
key: ChatContextAction.leave, child: Row(
icon: Icons.delete_outlined, mainAxisSize: MainAxisSize.min,
label: L10n.of(context)!.leave, 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 (action == null) return;
if (!mounted) 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( await showFutureLoadingDialog(
context: context, context: context,
future: () { future: () async {
switch (action) { switch (action) {
case ChatContextAction.goToSpace:
return;
case ChatContextAction.favorite: case ChatContextAction.favorite:
return room.setFavourite(!room.isFavourite); return room.setFavourite(!room.isFavourite);
case ChatContextAction.markUnread: case ChatContextAction.markUnread:
@ -1015,6 +1136,7 @@ enum InviteActions {
enum AddRoomType { chat, subspace } enum AddRoomType { chat, subspace }
enum ChatContextAction { enum ChatContextAction {
goToSpace,
favorite, favorite,
markUnread, markUnread,
mute, 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/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/avatar.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:fluffychat/widgets/public_room_bottom_sheet.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -25,17 +26,29 @@ class ChatListViewBody extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final client = Matrix.of(context).client;
final activeSpace = controller.activeSpaceId; final activeSpace = controller.activeSpaceId;
if (activeSpace != null) { if (activeSpace != null) {
return SpaceView( return SpaceView(
spaceId: activeSpace, spaceId: activeSpace,
onBack: controller.clearActiveSpace, onBack: controller.clearActiveSpace,
onChatTab: (room) => controller.onChatTap(room, context), onChatTab: (room) => controller.onChatTap(room),
onChatContext: (room) => controller.chatContextAction(room), onChatContext: (room, context) =>
controller.chatContextAction(room, context),
activeChat: controller.activeChat, activeChat: controller.activeChat,
toParentSpace: controller.setActiveSpace, 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 final publicRooms = controller.roomSearchResult?.chunk
.where((room) => room.roomType != 'm.space') .where((room) => room.roomType != 'm.space')
.toList(); .toList();
@ -43,7 +56,6 @@ class ChatListViewBody extends StatelessWidget {
.where((room) => room.roomType == 'm.space') .where((room) => room.roomType == 'm.space')
.toList(); .toList();
final userSearchResult = controller.userSearchResult; final userSearchResult = controller.userSearchResult;
final client = Matrix.of(context).client;
const dummyChatCount = 4; const dummyChatCount = 4;
final titleColor = final titleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100); Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100);
@ -60,18 +72,6 @@ class ChatListViewBody extends StatelessWidget {
builder: (context, _) { builder: (context, _) {
final rooms = controller.filteredRooms; 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( return SafeArea(
child: CustomScrollView( child: CustomScrollView(
controller: controller.scrollController, controller: controller.scrollController,
@ -164,50 +164,66 @@ class ChatListViewBody extends StatelessWidget {
), ),
shrinkWrap: true, shrinkWrap: true,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
children: ActiveFilter.values children: [
ActiveFilter.allChats,
ActiveFilter.unread,
ActiveFilter.groups,
if (spaceDelegateCandidates.isNotEmpty &&
!controller.widget.displayNavigationRail)
ActiveFilter.spaces,
]
.map( .map(
(filter) => Padding( (filter) => Padding(
padding: padding:
const EdgeInsets.symmetric(horizontal: 4), const EdgeInsets.symmetric(horizontal: 4),
child: InkWell( child: HoverBuilder(
borderRadius: BorderRadius.circular( builder: (context, hovered) =>
AppConfig.borderRadius, AnimatedScale(
), duration: FluffyThemes.animationDuration,
onTap: () => curve: FluffyThemes.animationCurve,
controller.setActiveFilter(filter), scale: hovered ? 1.1 : 1.0,
child: Container( child: InkWell(
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( borderRadius: BorderRadius.circular(
AppConfig.borderRadius, AppConfig.borderRadius,
), ),
), onTap: () =>
alignment: Alignment.center, controller.setActiveFilter(filter),
child: Text( child: Container(
filter.toLocalizedString(context), padding: const EdgeInsets.symmetric(
style: TextStyle( horizontal: 12,
fontWeight: vertical: 6,
filter == controller.activeFilter ),
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.bold
: FontWeight.normal, : FontWeight.normal,
color: color: filter ==
filter == controller.activeFilter controller.activeFilter
? Theme.of(context) ? Theme.of(context)
.colorScheme .colorScheme
.onPrimary .onPrimary
: Theme.of(context) : Theme.of(context)
.colorScheme .colorScheme
.onSecondaryContainer, .onSecondaryContainer,
),
),
), ),
), ),
), ),
@ -305,27 +321,16 @@ class ChatListViewBody extends StatelessWidget {
SliverList.builder( SliverList.builder(
itemCount: rooms.length, itemCount: rooms.length,
itemBuilder: (BuildContext context, int i) { itemBuilder: (BuildContext context, int i) {
var room = rooms[i]; final room = rooms[i];
if (controller.activeFilter != ActiveFilter.groups) { final space = spaceDelegateCandidates[room.id];
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;
}
}
return ChatListItem( return ChatListItem(
room, room,
lastEventRoom: rooms[i], space: space,
key: Key('chat_list_item_${room.id}'), key: Key('chat_list_item_${room.id}'),
filter: filter, filter: filter,
onTap: () => controller.onChatTap(room, context), onTap: () => controller.onChatTap(room),
onLongPress: () => controller.chatContextAction(room), onLongPress: (context) =>
controller.chatContextAction(room, context, space),
activeChat: controller.activeChat == room.id, activeChat: controller.activeChat == room.id,
); );
}, },

@ -19,9 +19,9 @@ enum ArchivedRoomAction { delete, rejoin }
class ChatListItem extends StatelessWidget { class ChatListItem extends StatelessWidget {
final Room room; final Room room;
final Room? lastEventRoom; final Room? space;
final bool activeChat; final bool activeChat;
final void Function()? onLongPress; final void Function(BuildContext context)? onLongPress;
final void Function()? onForget; final void Function()? onForget;
final void Function() onTap; final void Function() onTap;
final String? filter; final String? filter;
@ -33,7 +33,7 @@ class ChatListItem extends StatelessWidget {
this.onLongPress, this.onLongPress,
this.onForget, this.onForget,
this.filter, this.filter,
this.lastEventRoom, this.space,
super.key, super.key,
}); });
@ -66,21 +66,19 @@ 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 lastEventRoom = this.lastEventRoom ?? room; final typingText = room.getLocalizedTypingText(context);
final typingText = lastEventRoom.getLocalizedTypingText(context); final lastEvent = room.lastEvent;
final lastEvent = lastEventRoom.lastEvent;
final ownMessage = lastEvent?.senderId == room.client.userID; final ownMessage = lastEvent?.senderId == room.client.userID;
final unread = final unread = room.isUnread || room.membership == Membership.invite;
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 || lastEventRoom.hasNewMessages final unreadBubbleSize = unread || room.hasNewMessages
? lastEventRoom.notificationCount > 0 ? room.notificationCount > 0
? 20.0 ? 20.0
: 14.0 : 14.0
: 0.0; : 0.0;
final hasNotifications = lastEventRoom.notificationCount > 0; final hasNotifications = room.notificationCount > 0;
final backgroundColor = final backgroundColor =
activeChat ? theme.colorScheme.secondaryContainer : null; activeChat ? theme.colorScheme.secondaryContainer : null;
final displayname = room.getLocalizedDisplayname( final displayname = room.getLocalizedDisplayname(
@ -94,6 +92,7 @@ class ChatListItem extends StatelessWidget {
final needLastEventSender = lastEvent == null final needLastEventSender = lastEvent == null
? false ? false
: room.getState(EventTypes.RoomMember, lastEvent.senderId) == null; : room.getState(EventTypes.RoomMember, lastEvent.senderId) == null;
final space = this.space;
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -107,48 +106,93 @@ class ChatListItem extends StatelessWidget {
child: FutureBuilder( child: FutureBuilder(
future: room.loadHeroUsers(), future: room.loadHeroUsers(),
builder: (context, snapshot) => HoverBuilder( builder: (context, snapshot) => HoverBuilder(
builder: (context, hovered) => ListTile( builder: (context, listTileHovered) => ListTile(
visualDensity: const VisualDensity(vertical: -0.5), visualDensity: const VisualDensity(vertical: -0.5),
contentPadding: const EdgeInsets.symmetric(horizontal: 8), contentPadding: const EdgeInsets.symmetric(horizontal: 8),
onLongPress: onLongPress, onLongPress: () => onLongPress?.call(context),
leading: Stack( leading: HoverBuilder(
clipBehavior: Clip.none, builder: (context, hovered) => AnimatedScale(
children: [ duration: FluffyThemes.animationDuration,
HoverBuilder( curve: FluffyThemes.animationCurve,
builder: (context, hovered) => AnimatedScale( scale: hovered ? 1.1 : 1.0,
duration: FluffyThemes.animationDuration, child: SizedBox(
curve: FluffyThemes.animationCurve, width: Avatar.defaultSize,
scale: hovered ? 1.1 : 1.0, height: Avatar.defaultSize,
child: Avatar( child: Stack(
borderRadius: room.isSpace children: [
? BorderRadius.circular(AppConfig.borderRadius / 3) if (space != null)
: null, Positioned(
mxContent: room.avatar, top: 0,
name: displayname, left: 0,
presenceUserId: directChatMatrixId, child: Avatar(
presenceBackgroundColor: backgroundColor, border: BorderSide(
onTap: onLongPress, width: 2,
), color: backgroundColor ??
), Theme.of(context).colorScheme.surface,
), ),
Positioned( borderRadius: BorderRadius.circular(
bottom: -2, AppConfig.borderRadius / 4,
right: -2, ),
child: AnimatedScale( mxContent: space.avatar,
duration: FluffyThemes.animationDuration, size: Avatar.defaultSize * 0.75,
curve: FluffyThemes.animationCurve, name: space.getLocalizedDisplayname(),
scale: (hovered) ? 1.0 : 0.0, onTap: () => onLongPress?.call(context),
child: Material( ),
color: backgroundColor, ),
borderRadius: BorderRadius.circular(16), Positioned(
child: const Icon( bottom: 0,
Icons.check_circle_outlined, right: 0,
size: 18, 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( title: Row(
children: <Widget>[ children: <Widget>[
@ -207,20 +251,6 @@ class ChatListItem extends StatelessWidget {
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) ...[
@ -245,7 +275,7 @@ class ChatListItem extends StatelessWidget {
), ),
), ),
Expanded( Expanded(
child: room.isSpace && !lastEventRoom.isUnreadOrInvited child: room.isSpace && room.membership == Membership.join
? Text( ? Text(
L10n.of(context)!.countChatsAndCountParticipants( L10n.of(context)!.countChatsAndCountParticipants(
room.spaceChildren.length.toString(), room.spaceChildren.length.toString(),
@ -308,10 +338,9 @@ class ChatListItem extends StatelessWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontWeight: fontWeight: unread || room.hasNewMessages
unread || lastEventRoom.hasNewMessages ? FontWeight.bold
? FontWeight.bold : null,
: null,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
decoration: room.lastEvent?.redacted == true decoration: room.lastEvent?.redacted == true
? TextDecoration.lineThrough ? TextDecoration.lineThrough
@ -339,9 +368,7 @@ class ChatListItem extends StatelessWidget {
width: !hasNotifications && !unread && !room.hasNewMessages width: !hasNotifications && !unread && !room.hasNewMessages
? 0 ? 0
: (unreadBubbleSize - 9) * : (unreadBubbleSize - 9) *
lastEventRoom.notificationCount room.notificationCount.toString().length +
.toString()
.length +
9, 9,
decoration: BoxDecoration( decoration: BoxDecoration(
color: room.highlightCount > 0 || color: room.highlightCount > 0 ||
@ -356,7 +383,7 @@ class ChatListItem extends StatelessWidget {
child: Center( child: Center(
child: hasNotifications child: hasNotifications
? Text( ? Text(
lastEventRoom.notificationCount.toString(), room.notificationCount.toString(),
style: TextStyle( style: TextStyle(
color: room.highlightCount > 0 color: room.highlightCount > 0
? Colors.white ? Colors.white

@ -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:flutter/material.dart';
import 'package:badges/badges.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.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'; import '../../config/themes.dart';
class NaviRailItem extends StatefulWidget { class NaviRailItem extends StatelessWidget {
final String toolTip; final String toolTip;
final bool isSelected; final bool isSelected;
final void Function() onTap; final void Function() onTap;
final Widget icon; final Widget icon;
final Widget? selectedIcon; final Widget? selectedIcon;
final bool Function(Room)? unreadBadgeFilter;
const NaviRailItem({ const NaviRailItem({
required this.toolTip, required this.toolTip,
@ -16,80 +22,78 @@ class NaviRailItem extends StatefulWidget {
required this.onTap, required this.onTap,
required this.icon, required this.icon,
this.selectedIcon, this.selectedIcon,
this.unreadBadgeFilter,
super.key, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final borderRadius = BorderRadius.circular(AppConfig.borderRadius); final borderRadius = BorderRadius.circular(AppConfig.borderRadius);
return SizedBox( final icon = isSelected ? selectedIcon ?? this.icon : this.icon;
height: 64, final unreadBadgeFilter = this.unreadBadgeFilter;
width: 64, return HoverBuilder(
child: Stack( builder: (context, hovered) {
children: [ return SizedBox(
Positioned( height: 64,
top: 16, width: 64,
bottom: 16, child: Stack(
left: 0, children: [
child: AnimatedContainer( Positioned(
width: widget.isSelected ? 4 : 0, top: 16,
duration: FluffyThemes.animationDuration, bottom: 16,
curve: FluffyThemes.animationCurve, left: 0,
decoration: BoxDecoration( child: AnimatedContainer(
color: Theme.of(context).colorScheme.primary, width: isSelected ? 4 : 0,
borderRadius: const BorderRadius.only( duration: FluffyThemes.animationDuration,
topRight: Radius.circular(90), curve: FluffyThemes.animationCurve,
bottomRight: Radius.circular(90), decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(90),
bottomRight: Radius.circular(90),
),
),
), ),
), ),
), Center(
), child: AnimatedScale(
Center( scale: hovered ? 1.2 : 1.0,
child: AnimatedScale( duration: FluffyThemes.animationDuration,
scale: _hovered ? 1.2 : 1.0, curve: FluffyThemes.animationCurve,
duration: FluffyThemes.animationDuration, child: Material(
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, borderRadius: borderRadius,
onTap: widget.onTap, color: isSelected
onHover: _onHover, ? Theme.of(context).colorScheme.primaryContainer
child: Padding( : Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.symmetric( child: Tooltip(
horizontal: 8.0, message: toolTip,
vertical: 8.0, 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,
),
),
), ),
child: widget.isSelected
? widget.selectedIcon ?? widget.icon
: widget.icon,
), ),
), ),
), ),
), ),
), ],
), ),
], );
), },
); );
} }
} }

@ -18,7 +18,7 @@ class SpaceView extends StatefulWidget {
final void Function() onBack; final void Function() onBack;
final void Function(String spaceId) toParentSpace; final void Function(String spaceId) toParentSpace;
final void Function(Room room) onChatTab; final void Function(Room room) onChatTab;
final void Function(Room room) onChatContext; final void Function(Room room, BuildContext context) onChatContext;
final String? activeChat; final String? activeChat;
const SpaceView({ const SpaceView({
@ -163,316 +163,315 @@ class _SpaceViewState extends State<SpaceView> {
final room = Matrix.of(context).client.getRoomById(widget.spaceId); final room = Matrix.of(context).client.getRoomById(widget.spaceId);
final displayname = final displayname =
room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound; room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound;
return Scaffold( return PopScope(
appBar: AppBar( canPop: false,
leading: Center( onPopInvoked: (_) => widget.onBack(),
child: CloseButton( child: Scaffold(
onPressed: widget.onBack, appBar: AppBar(
), leading: Center(
), child: CloseButton(
titleSpacing: 0, onPressed: widget.onBack,
title: ListTile( ),
contentPadding: EdgeInsets.zero,
leading: Avatar(
mxContent: room?.avatar,
name: displayname,
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
), ),
title: Text( titleSpacing: 0,
displayname, title: ListTile(
maxLines: 1, contentPadding: EdgeInsets.zero,
overflow: TextOverflow.ellipsis, 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,
),
), ),
subtitle: room == null actions: [
? null PopupMenuButton<SpaceActions>(
: Text( onSelected: _onSpaceAction,
L10n.of(context)!.countChatsAndCountParticipants( itemBuilder: (context) => [
room.spaceChildren.length, PopupMenuItem(
room.summary.mJoinedMemberCount ?? 1, value: SpaceActions.settings,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.settings_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.settings),
],
), ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
), PopupMenuItem(
actions: [ value: SpaceActions.invite,
PopupMenuButton<SpaceActions>( child: Row(
onSelected: _onSpaceAction, mainAxisSize: MainAxisSize.min,
itemBuilder: (context) => [ children: [
PopupMenuItem( const Icon(Icons.person_add_outlined),
value: SpaceActions.settings, const SizedBox(width: 12),
child: Row( Text(L10n.of(context)!.invite),
mainAxisSize: MainAxisSize.min, ],
children: [ ),
const Icon(Icons.settings_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.settings),
],
), ),
), PopupMenuItem(
PopupMenuItem( value: SpaceActions.leave,
value: SpaceActions.invite, child: Row(
child: Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ const Icon(Icons.delete_outlined),
const Icon(Icons.person_add_outlined), const SizedBox(width: 12),
const SizedBox(width: 12), Text(L10n.of(context)!.leave),
Text(L10n.of(context)!.invite), ],
], ),
), ),
), ],
PopupMenuItem( ),
value: SpaceActions.leave, ],
child: Row( ),
mainAxisSize: MainAxisSize.min, body: room == null
children: [ ? const Center(
const Icon(Icons.delete_outlined), child: Icon(
const SizedBox(width: 12), Icons.search_outlined,
Text(L10n.of(context)!.leave), size: 80,
],
), ),
), )
], : StreamBuilder(
), stream: room.client.onSync.stream
], .where((s) => s.hasRoomUpdate)
), .rateLimit(const Duration(seconds: 1)),
body: room == null builder: (context, snapshot) {
? const Center( final childrenIds = room.spaceChildren
child: Icon( .map((c) => c.roomId)
Icons.search_outlined, .whereType<String>()
size: 80, .toSet();
),
)
: 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 final joinedRooms = room.client.rooms
joinedRooms.sort( .where((room) => childrenIds.remove(room.id))
(b, a) => (a.lastEvent?.originServerTs ?? .toList();
DateTime.fromMillisecondsSinceEpoch(0))
.compareTo(
b.lastEvent?.originServerTs ??
DateTime.fromMillisecondsSinceEpoch(0),
),
);
final joinedParents = room.spaceParents final joinedParents = room.spaceParents
.map((parent) { .map((parent) {
final roomId = parent.roomId; final roomId = parent.roomId;
if (roomId == null) return null; if (roomId == null) return null;
return room.client.getRoomById(roomId); return room.client.getRoomById(roomId);
}) })
.whereType<Room>() .whereType<Room>()
.toList(); .toList();
final filter = _filterController.text.trim().toLowerCase(); final filter = _filterController.text.trim().toLowerCase();
return CustomScrollView( return CustomScrollView(
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
floating: true, floating: true,
toolbarHeight: 72, toolbarHeight: 72,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
title: TextField( title: TextField(
controller: _filterController, controller: _filterController,
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
decoration: InputDecoration( decoration: InputDecoration(
fillColor: fillColor: Theme.of(context)
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)
.colorScheme .colorScheme
.onPrimaryContainer, .secondaryContainer,
fontWeight: FontWeight.normal, border: OutlineInputBorder(
), borderSide: BorderSide.none,
floatingLabelBehavior: FloatingLabelBehavior.never, borderRadius: BorderRadius.circular(99),
prefixIcon: IconButton( ),
onPressed: () {}, contentPadding: EdgeInsets.zero,
icon: Icon( hintText: L10n.of(context)!.search,
Icons.search_outlined, hintStyle: TextStyle(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onPrimaryContainer, .onPrimaryContainer,
fontWeight: FontWeight.normal,
),
floatingLabelBehavior: FloatingLabelBehavior.never,
prefixIcon: IconButton(
onPressed: () {},
icon: Icon(
Icons.search_outlined,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
), ),
), ),
), ),
), ),
), SliverList.builder(
SliverList.builder( itemCount: joinedParents.length,
itemCount: joinedParents.length, itemBuilder: (context, i) {
itemBuilder: (context, i) { final displayname =
final displayname = joinedParents[i].getLocalizedDisplayname();
joinedParents[i].getLocalizedDisplayname(); return Padding(
return Padding( padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 8,
horizontal: 8, vertical: 1,
vertical: 1, ),
), child: Material(
child: Material( borderRadius:
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
BorderRadius.circular(AppConfig.borderRadius), clipBehavior: Clip.hardEdge,
clipBehavior: Clip.hardEdge, child: ListTile(
child: ListTile( minVerticalPadding: 0,
minVerticalPadding: 0, leading: Icon(
leading: Icon( Icons.adaptive.arrow_back_outlined,
Icons.adaptive.arrow_back_outlined, size: 16,
size: 16, ),
), title: Row(
title: Row( children: [
children: [ Avatar(
Avatar( mxContent: joinedParents[i].avatar,
mxContent: joinedParents[i].avatar, name: displayname,
name: displayname, size: Avatar.defaultSize / 2,
size: Avatar.defaultSize / 2, borderRadius: BorderRadius.circular(
borderRadius: BorderRadius.circular( AppConfig.borderRadius / 4,
AppConfig.borderRadius / 4, ),
), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), Expanded(child: Text(displayname)),
Expanded(child: Text(displayname)), ],
], ),
onTap: () =>
widget.toParentSpace(joinedParents[i].id),
), ),
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]; SliverList.builder(
return ChatListItem( itemCount: joinedRooms.length + 1,
room, itemBuilder: (context, i) {
filter: filter, if (i == 0) {
onTap: () => widget.onChatTab(room), return SearchTitle(
onLongPress: () => widget.onChatContext(room), title: L10n.of(context)!.joinedChats,
activeChat: widget.activeChat == room.id, icon: const Icon(Icons.chat_outlined),
); );
}, }
), i--;
SliverList.builder( final room = joinedRooms[i];
itemCount: _discoveredChildren.length + 2, return ChatListItem(
itemBuilder: (context, i) { room,
if (i == 0) { filter: filter,
return SearchTitle( onTap: () => widget.onChatTab(room),
title: L10n.of(context)!.discover, onLongPress: (context) => widget.onChatContext(
icon: const Icon(Icons.explore_outlined), room,
context,
),
activeChat: widget.activeChat == room.id,
); );
} },
i--; ),
if (i == _discoveredChildren.length) { SliverList.builder(
if (_noMoreRooms) { itemCount: _discoveredChildren.length + 2,
return Padding( itemBuilder: (context, i) {
padding: const EdgeInsets.all(12.0), if (i == 0) {
child: Center( return SearchTitle(
child: Text( title: L10n.of(context)!.discover,
L10n.of(context)!.noMoreChatsFound, icon: const Icon(Icons.explore_outlined),
style: const TextStyle(fontSize: 13), );
}
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( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12.0, horizontal: 8,
vertical: 2.0, vertical: 1,
), ),
child: TextButton( child: Material(
onPressed: _isLoading ? null : _loadHierarchy, borderRadius:
child: _isLoading BorderRadius.circular(AppConfig.borderRadius),
? LinearProgressIndicator( clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular( child: ListTile(
AppConfig.borderRadius, 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),
),
);
}
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 SizedBox(width: 8), const Icon(
const Icon(Icons.add_circle_outline_outlined), Icons.add_circle_outline_outlined,
],
),
subtitle: Text(
item.topic ??
L10n.of(context)!.countParticipants(
item.numJoinedMembers,
), ),
maxLines: 1, ],
overflow: TextOverflow.ellipsis, ),
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 Color? presenceBackgroundColor;
final BorderRadius? borderRadius; final BorderRadius? borderRadius;
final IconData? icon; final IconData? icon;
final BorderSide? border;
const Avatar({ const Avatar({
this.mxContent, this.mxContent,
@ -25,6 +26,7 @@ class Avatar extends StatelessWidget {
this.presenceUserId, this.presenceUserId,
this.presenceBackgroundColor, this.presenceBackgroundColor,
this.borderRadius, this.borderRadius,
this.border,
this.icon, this.icon,
super.key, super.key,
}); });
@ -65,10 +67,7 @@ class Avatar extends StatelessWidget {
color: color, color: color,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: borderRadius, borderRadius: borderRadius,
side: BorderSide( side: border ?? BorderSide.none,
width: 0,
color: Theme.of(context).dividerColor,
),
), ),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: noPic child: noPic

@ -3,11 +3,13 @@ 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) {
@ -18,7 +20,7 @@ class TwoColumnLayout extends StatelessWidget {
Container( Container(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
decoration: const BoxDecoration(), decoration: const BoxDecoration(),
width: 384.0, width: 360.0 + (displayNavigationRail ? 64 : 0),
child: mainView, child: mainView,
), ),
Container( Container(

@ -30,12 +30,6 @@ class UnreadRoomsBadge extends StatelessWidget {
// Pangea# // Pangea#
builder: (context, _) { builder: (context, _) {
// #Pangea // #Pangea
// final unreadCount = Matrix.of(context)
// .client
// .rooms
// .where(filter)
// .where((r) => (r.isUnread || r.membership == Membership.invite))
// .length;
final unreadCounts = Matrix.of(context) final unreadCounts = Matrix.of(context)
.client .client
.rooms .rooms
@ -44,6 +38,12 @@ class UnreadRoomsBadge extends StatelessWidget {
.map((r) => r.notificationCount); .map((r) => r.notificationCount);
final unreadCount = final unreadCount =
unreadCounts.isEmpty ? 0 : unreadCounts.reduce((a, b) => a + b); 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# // Pangea#
return b.Badge( return b.Badge(
badgeStyle: b.BadgeStyle( badgeStyle: b.BadgeStyle(

@ -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, 800, 600); gtk_window_set_default_size(window, 864, 680);
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…
Cancel
Save