diff --git a/README.md b/README.md index 73f00de..b97e9e7 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,8 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [x] Events - [ ] Rooms - [x] Member list + - [x] Sort by power level - [ ] Colors based off of power level - - [ ] Sort by power level - [ ] Notifications using UnifiedPush ([#35](https://git.federated.nexus/Nexus/nexus/issues/35)) - [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195) - [ ] Invites diff --git a/assets/screenshotDark.png b/assets/screenshotDark.png index 0a89f01..322a64e 100644 Binary files a/assets/screenshotDark.png and b/assets/screenshotDark.png differ diff --git a/assets/screenshotLight.png b/assets/screenshotLight.png index 4e6fd36..8772bcf 100644 Binary files a/assets/screenshotLight.png and b/assets/screenshotLight.png differ diff --git a/lib/helpers/extensions/string_to_color.dart b/lib/helpers/extensions/string_to_color.dart index 8d30e76..eaa7714 100644 --- a/lib/helpers/extensions/string_to_color.dart +++ b/lib/helpers/extensions/string_to_color.dart @@ -2,5 +2,5 @@ import "package:color_hash/color_hash.dart"; import "package:flutter/material.dart"; extension ToColor on String { - Color get colorHash => ColorHash(this, lightness: .7, saturation: .7).color; + Color get colorHash => ColorHash(this, lightness: .5, saturation: .7).color; } diff --git a/lib/widgets/divider_text.dart b/lib/widgets/divider_text.dart index f53795d..2b0f9bd 100644 --- a/lib/widgets/divider_text.dart +++ b/lib/widgets/divider_text.dart @@ -1,4 +1,5 @@ import "package:flutter/material.dart"; +import "package:nexus/widgets/divider_widget.dart"; class DividerText extends StatelessWidget { final String text; @@ -6,24 +7,6 @@ class DividerText extends StatelessWidget { const DividerText(this.text, {super.key}); @override - Widget build(BuildContext context) => LayoutBuilder( - builder: (context, constraints) => Row( - children: [ - SizedBox( - width: 16, - child: Divider(color: Theme.of(context).colorScheme.onSurface), - ), - ConstrainedBox( - constraints: .new(maxWidth: constraints.maxWidth - 32), - child: Padding( - padding: const .all(8), - child: Text(text, style: Theme.of(context).textTheme.labelLarge), - ), - ), - Expanded( - child: Divider(color: Theme.of(context).colorScheme.onSurface), - ), - ], - ), - ); + Widget build(BuildContext context) => + DividerWidget(Text(text, style: Theme.of(context).textTheme.labelLarge)); } diff --git a/lib/widgets/divider_widget.dart b/lib/widgets/divider_widget.dart new file mode 100644 index 0000000..6f13bd4 --- /dev/null +++ b/lib/widgets/divider_widget.dart @@ -0,0 +1,25 @@ +import "package:flutter/material.dart"; + +class DividerWidget extends StatelessWidget { + final Widget widget; + const DividerWidget(this.widget, {super.key}); + + @override + Widget build(BuildContext context) => LayoutBuilder( + builder: (_, constraints) => Row( + children: [ + SizedBox( + width: 16, + child: Divider(color: Theme.of(context).colorScheme.onSurface), + ), + ConstrainedBox( + constraints: .new(maxWidth: constraints.maxWidth - 32), + child: Padding(padding: const .all(8), child: widget), + ), + Expanded( + child: Divider(color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + ); +} diff --git a/lib/widgets/error_dialog.dart b/lib/widgets/error_dialog.dart index b016a8b..9b62200 100644 --- a/lib/widgets/error_dialog.dart +++ b/lib/widgets/error_dialog.dart @@ -21,11 +21,12 @@ class ErrorDialog extends ConsumerWidget { onPressed: () => ref.invalidate(provider!), child: const Text("Try Again"), ), - TextButton( - onPressed: () => - Navigator.of(context).popUntil((route) => route.isFirst), - child: const Text("Go Back"), - ), + if (Navigator.of(context).canPop()) + TextButton( + onPressed: () => + Navigator.of(context).popUntil((route) => route.isFirst), + child: const Text("Go Back"), + ), ], ); } diff --git a/lib/widgets/message_image.dart b/lib/widgets/message_image.dart index d92783a..8865a5c 100644 --- a/lib/widgets/message_image.dart +++ b/lib/widgets/message_image.dart @@ -18,36 +18,37 @@ class MessageImage extends ConsumerWidget { url.toString(), child: ClipRRect( borderRadius: .all(.circular(8)), - child: Image( - image: CachedNetworkImage( - url.toString(), - ref.watch(CrossCacheController.provider), - headers: ref.headers, - ), - width: info?.width, - loadingBuilder: (_, child, loadingProgress) => loadingProgress == null - ? child - : switch (info?.blurHash) { - final blurHash? => - info?.width == null || info?.height == null - ? SizedBox( - width: 200, - height: 200, - child: BlurHash(hash: blurHash), - ) - : AspectRatio( - aspectRatio: info!.width! / info!.height!, - child: SizedBox( + child: AspectRatio( + aspectRatio: info!.width! / info!.height!, + child: Image( + image: CachedNetworkImage( + url.toString(), + ref.watch(CrossCacheController.provider), + headers: ref.headers, + ), + width: info?.width, + fit: BoxFit.fitWidth, + loadingBuilder: (_, child, loadingProgress) => loadingProgress == null + ? child + : switch (info?.blurHash) { + final blurHash? => + info?.width == null || info?.height == null + ? SizedBox( + width: 200, + height: 200, + child: BlurHash(hash: blurHash), + ) + : SizedBox( width: info!.width, child: BlurHash(hash: blurHash), ), - ), - _ => Loading(), - }, - errorBuilder: (context, error, stackTrace) => Center( - child: Text( - "Image Failed to Load", - style: .new(color: Theme.of(context).colorScheme.error), + _ => Loading(), + }, + errorBuilder: (context, error, stackTrace) => Center( + child: Text( + "Image Failed to Load", + style: .new(color: Theme.of(context).colorScheme.error), + ), ), ), ), diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index d1c1c7c..a4f659c 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -17,6 +17,7 @@ import "package:nexus/models/content/server_acl.dart"; import "package:nexus/models/content/sticker.dart"; import "package:nexus/models/content/topic.dart"; import "package:nexus/models/event.dart"; +import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/lazy_loading/message_displayname.dart"; import "package:nexus/widgets/renderers/message.dart"; import "package:nexus/widgets/reaction_row.dart"; @@ -49,9 +50,13 @@ class EventRenderer extends ConsumerWidget { final child = event.redactedBy != null || event.relationType == "m.replace" ? null : switch (event.content) { - Content(:final parseError?) => SelectableText( - "An error occurred while parsing this event:\n$parseError\n${parseError.stackTrace}", - style: errorStyle, + Content(:final parseError?) => Row( + children: [ + ErrorDialog( + "An error occurred while parsing event ${event.eventId}:\n$parseError", + parseError.stackTrace, + ), + ], ), MessageContent() || diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart index abe9a0a..591e25f 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -7,6 +7,7 @@ import "package:nexus/controllers/key_controller.dart"; import "package:nexus/controllers/spaces_controller.dart"; import "package:nexus/models/room.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/divider_widget.dart"; import "package:nexus/widgets/join_dialog.dart"; import "package:nexus/widgets/room_menu.dart"; @@ -38,9 +39,11 @@ class Sidebar extends HookConsumerWidget { spaces.firstWhereOrNull((space) => space.id == selectedSpaceId) ?? spaces.first; - final indexOfSelectedRoom = selectedSpace.children.indexWhere( - (room) => room.metadata?.id == selectedRoomId, - ); + final indexOfSelectedRoom = selectedSpace.children + .addAll( + selectedSpace.subSpaces.map((element) => element.children).flattened, + ) + .indexWhere((room) => room.metadata?.id == selectedRoomId); final selectedRoomIndex = indexOfSelectedRoom == -1 ? null : indexOfSelectedRoom; @@ -60,7 +63,6 @@ class Sidebar extends HookConsumerWidget { fallback: selectedSpaceId == "dms" ? null : Icon(Icons.numbers), - // space.client.headers, ), ), ) @@ -98,17 +100,32 @@ class Sidebar extends HookConsumerWidget { destinations: spaces .map( (space) => NavigationRailM3EDestination( - badgeCount: switch (space.children.fold( - 0, - (previousValue, room) => - previousValue + - (room.metadata?.unreadNotifications ?? 0), - )) { + badgeCount: switch (space.children + .addAll( + space.subSpaces + .map((element) => element.children) + .flattened, + ) + .fold( + 0, + (previousValue, room) => + previousValue + + (room.metadata?.unreadNotifications ?? 0), + )) { 0 => - space.children.any( - (room) => - room.metadata?.unreadMessages != 0, - ) + space.children + .addAll( + space.subSpaces + .map( + (element) => element.children, + ) + .flattened, + ) + .any( + (room) => + room.metadata?.unreadMessages != + 0, + ) ? 0 : null, int badgeCount => badgeCount, @@ -213,20 +230,47 @@ class Sidebar extends HookConsumerWidget { selectedIndex: selectedRoomIndex ?? 0, sections: [ .new( - header: selectedSpace.room == null ? null : Text("Rooms"), + header: selectedSpace.room == null + ? null + : DividerWidget(Text("Rooms")), destinations: roomsToDestinations(selectedSpace.children), ), for (final subSpace in selectedSpace.subSpaces) .new( - header: Text( - subSpace.room.metadata?.name ?? "Unnamed Space", + header: DividerWidget( + Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + if (subSpace.room.metadata?.avatar != null) + AvatarOrHash( + subSpace.room.metadata?.avatar, + subSpace.room.metadata?.name ?? + "Unnamed Room", + height: 16, + ), + Flexible( + child: Text( + subSpace.room.metadata?.name ?? + "Unnamed Space", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), ), destinations: roomsToDestinations(subSpace.children), ), ], onDestinationSelected: (value) { + final children = selectedSpace.children.addAll( + selectedSpace.subSpaces + .map((element) => element.children) + .flattened, + ); selectedRoomIdNotifier.set( - selectedSpace.children[value].metadata?.id, + children[value].metadata?.id, // ); if (!isDesktop) Navigator.of(context).pop(); },