From aaeee8e355f4ddb03bea66e8381eb028dde46f3f Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 11 Nov 2025 14:45:01 -0500 Subject: [PATCH] ROOMS --- lib/controllers/client_controller.dart | 28 ++- lib/controllers/room_chat_controller.dart | 31 +-- lib/controllers/spaces_controller.dart | 74 ++++-- lib/helpers/extension_helper.dart | 18 ++ lib/models/full_room.dart | 13 + lib/models/full_room.freezed.dart | 277 ++++++++++++++++++++++ lib/models/space.dart | 10 +- lib/models/space.freezed.dart | 64 +++-- lib/widgets/avatar.dart | 25 ++ lib/widgets/sidebar.dart | 151 ++++++------ 10 files changed, 527 insertions(+), 164 deletions(-) create mode 100644 lib/models/full_room.dart create mode 100644 lib/models/full_room.freezed.dart create mode 100644 lib/widgets/avatar.dart diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index 7691a1e..08ff185 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -1,7 +1,10 @@ import "dart:io"; +import "package:flutter/foundation.dart"; import "package:matrix/matrix.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:path/path.dart"; +import "package:path_provider/path_provider.dart"; import "package:sqflite_common_ffi/sqflite_ffi.dart"; class ClientController extends AsyncNotifier { @@ -9,18 +12,27 @@ class ClientController extends AsyncNotifier { Future build() async { final client = Client( "nexus", + logLevel: kReleaseMode ? Level.warning : Level.verbose, + importantStateEvents: {"im.ponies.room_emotes"}, database: await MatrixSdkDatabase.init( "nexus", - database: await databaseFactoryFfi.openDatabase("./test.db"), + database: await databaseFactoryFfi.openDatabase( + join((await getApplicationSupportDirectory()).path, "database.db"), + ), ), ); - //mxc - await client.checkHomeserver(Uri.https("federated.nexus")); - await client.login( - LoginType.mLoginPassword, - identifier: AuthenticationUserIdentifier(user: "quadradical"), - password: File("./password.txt").readAsStringSync(), - ); + + // TODO: Save info + if (client.homeserver == null) { + await client.checkHomeserver(Uri.https("federated.nexus")); + } + if (client.accessToken == null) { + await client.login( + LoginType.mLoginPassword, + identifier: AuthenticationUserIdentifier(user: "quadradical"), + password: File("./password.txt").readAsStringSync(), + ); + } return client; } diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index fab7e29..6dae669 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -2,34 +2,13 @@ import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; class RoomChatController extends Notifier { - RoomChatController(this.id); - final String id; + RoomChatController(this.roomId); + final String roomId; @override - InMemoryChatController build() => InMemoryChatController( - messages: [ - Message.text(id: "foo2", authorId: "foo", text: "**Some** text"), - Message.text( - id: "foo3", - authorId: "foo5", - text: "Some text 2 https://federated.nexus", - ), - Message.text( - id: "aksdjflkasdjf", - authorId: "foo", - text: "Some text 2 https://github.com/Henry-hiles/nixos", - ), - Message.system(id: "foo4", authorId: "", text: "system"), - Message.text(id: "foo6", authorId: "foo5", text: "Some text 2"), - Message.image( - id: "foo5", - authorId: "foobar3", - source: - "https://henryhiles.com/_astro/federatedNexus.BvZmkdyc_2b28Im.webp", - ), - Message.text(id: "foo7", authorId: "foobar3", text: "this has an image"), - ], - ); + InMemoryChatController build() => InMemoryChatController(); + + // void setRoom(Room room) => state = (await ref.watch(ClientController.provider.future)); void send(String message) { state.insertMessage( diff --git a/lib/controllers/spaces_controller.dart b/lib/controllers/spaces_controller.dart index e87368e..0e13075 100644 --- a/lib/controllers/spaces_controller.dart +++ b/lib/controllers/spaces_controller.dart @@ -1,7 +1,8 @@ -import "package:flutter/widgets.dart"; -import "package:matrix/matrix.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/helpers/extension_helper.dart"; import "package:nexus/models/space.dart"; class SpacesController extends AsyncNotifier> { @@ -9,25 +10,60 @@ class SpacesController extends AsyncNotifier> { Future> build() async { final client = await ref.watch(ClientController.provider.future); - return Future.wait( - client.rooms.where((room) => room.isSpace).map((data) async { - final thumb = await data.avatar?.getThumbnailUri( - client, - width: 40, - height: 40, - ); - return Space( - roomData: data, - avatar: thumb == null - ? null - : Image.network( - thumb.toString(), - width: 40, - headers: {"authorization": "Bearer ${client.accessToken}"}, + final topLevel = await Future.wait( + client.rooms + .where((room) => !room.isDirectChat) + .where( + (room) => client.rooms + .where((room) => room.isSpace) + .every( + (match) => match.spaceChildren.every( + (child) => child.roomId != room.id, + ), ), - ); - }), + ) + .map((room) => room.fullRoom), ); + + final topLevelSpaces = topLevel.where((r) => r.roomData.isSpace).toList(); + final topLevelRooms = topLevel.where((r) => !r.roomData.isSpace).toList(); + + return [ + Space( + title: "Home", + children: topLevelRooms, + avatar: Icon(Icons.home), + fake: true, + ), + Space( + title: "Direct Messages", + children: await Future.wait( + client.rooms + .where((room) => room.isDirectChat) + .map((room) => room.fullRoom), + ), + avatar: Icon(Icons.person), + fake: true, + ), + ...(await Future.wait( + topLevelSpaces.map( + (space) async => Space( + title: space.title, + avatar: space.avatar, + children: await Future.wait( + space.roomData.spaceChildren + .map( + (child) => client.rooms.firstWhereOrNull( + (room) => room.id == child.roomId, + ), + ) + .nonNulls + .map((room) => room.fullRoom), + ), + ), + ), + )), + ]; } static final provider = AsyncNotifierProvider>( diff --git a/lib/helpers/extension_helper.dart b/lib/helpers/extension_helper.dart index beaae85..1d9b8f8 100644 --- a/lib/helpers/extension_helper.dart +++ b/lib/helpers/extension_helper.dart @@ -1,5 +1,7 @@ import "package:flutter/widgets.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/models/full_room.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; @@ -15,3 +17,19 @@ extension BetterWhen on AsyncValue { skipLoadingOnRefresh: skipLoadingOnRefresh, ); } + +extension GetFullRoom on Room { + Future get fullRoom async { + final thumb = await avatar?.getThumbnailUri(client, width: 24, height: 24); + return FullRoom( + roomData: this, + title: getLocalizedDisplayname(), + avatar: thumb == null + ? null + : Image.network( + thumb.toString(), + headers: {"authorization": "Bearer ${client.accessToken}"}, + ), + ); + } +} diff --git a/lib/models/full_room.dart b/lib/models/full_room.dart new file mode 100644 index 0000000..ad951d6 --- /dev/null +++ b/lib/models/full_room.dart @@ -0,0 +1,13 @@ +import "package:flutter/widgets.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:matrix/matrix.dart"; +part "full_room.freezed.dart"; + +@freezed +abstract class FullRoom with _$FullRoom { + const factory FullRoom({ + required Room roomData, + required String title, + required Image? avatar, + }) = _FullRoom; +} diff --git a/lib/models/full_room.freezed.dart b/lib/models/full_room.freezed.dart new file mode 100644 index 0000000..2e42537 --- /dev/null +++ b/lib/models/full_room.freezed.dart @@ -0,0 +1,277 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'full_room.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$FullRoom { + + Room get roomData; String get title; Image? get avatar; +/// Create a copy of FullRoom +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$FullRoomCopyWith get copyWith => _$FullRoomCopyWithImpl(this as FullRoom, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is FullRoom&&(identical(other.roomData, roomData) || other.roomData == roomData)&&(identical(other.title, title) || other.title == title)&&(identical(other.avatar, avatar) || other.avatar == avatar)); +} + + +@override +int get hashCode => Object.hash(runtimeType,roomData,title,avatar); + +@override +String toString() { + return 'FullRoom(roomData: $roomData, title: $title, avatar: $avatar)'; +} + + +} + +/// @nodoc +abstract mixin class $FullRoomCopyWith<$Res> { + factory $FullRoomCopyWith(FullRoom value, $Res Function(FullRoom) _then) = _$FullRoomCopyWithImpl; +@useResult +$Res call({ + Room roomData, String title, Image? avatar +}); + + + + +} +/// @nodoc +class _$FullRoomCopyWithImpl<$Res> + implements $FullRoomCopyWith<$Res> { + _$FullRoomCopyWithImpl(this._self, this._then); + + final FullRoom _self; + final $Res Function(FullRoom) _then; + +/// Create a copy of FullRoom +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? roomData = null,Object? title = null,Object? avatar = freezed,}) { + return _then(_self.copyWith( +roomData: null == roomData ? _self.roomData : roomData // ignore: cast_nullable_to_non_nullable +as Room,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable +as Image?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [FullRoom]. +extension FullRoomPatterns on FullRoom { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _FullRoom value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _FullRoom() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _FullRoom value) $default,){ +final _that = this; +switch (_that) { +case _FullRoom(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _FullRoom value)? $default,){ +final _that = this; +switch (_that) { +case _FullRoom() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( Room roomData, String title, Image? avatar)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _FullRoom() when $default != null: +return $default(_that.roomData,_that.title,_that.avatar);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( Room roomData, String title, Image? avatar) $default,) {final _that = this; +switch (_that) { +case _FullRoom(): +return $default(_that.roomData,_that.title,_that.avatar);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( Room roomData, String title, Image? avatar)? $default,) {final _that = this; +switch (_that) { +case _FullRoom() when $default != null: +return $default(_that.roomData,_that.title,_that.avatar);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _FullRoom implements FullRoom { + const _FullRoom({required this.roomData, required this.title, required this.avatar}); + + +@override final Room roomData; +@override final String title; +@override final Image? avatar; + +/// Create a copy of FullRoom +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$FullRoomCopyWith<_FullRoom> get copyWith => __$FullRoomCopyWithImpl<_FullRoom>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _FullRoom&&(identical(other.roomData, roomData) || other.roomData == roomData)&&(identical(other.title, title) || other.title == title)&&(identical(other.avatar, avatar) || other.avatar == avatar)); +} + + +@override +int get hashCode => Object.hash(runtimeType,roomData,title,avatar); + +@override +String toString() { + return 'FullRoom(roomData: $roomData, title: $title, avatar: $avatar)'; +} + + +} + +/// @nodoc +abstract mixin class _$FullRoomCopyWith<$Res> implements $FullRoomCopyWith<$Res> { + factory _$FullRoomCopyWith(_FullRoom value, $Res Function(_FullRoom) _then) = __$FullRoomCopyWithImpl; +@override @useResult +$Res call({ + Room roomData, String title, Image? avatar +}); + + + + +} +/// @nodoc +class __$FullRoomCopyWithImpl<$Res> + implements _$FullRoomCopyWith<$Res> { + __$FullRoomCopyWithImpl(this._self, this._then); + + final _FullRoom _self; + final $Res Function(_FullRoom) _then; + +/// Create a copy of FullRoom +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? roomData = null,Object? title = null,Object? avatar = freezed,}) { + return _then(_FullRoom( +roomData: null == roomData ? _self.roomData : roomData // ignore: cast_nullable_to_non_nullable +as Room,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable +as Image?, + )); +} + + +} + +// dart format on diff --git a/lib/models/space.dart b/lib/models/space.dart index f0220c1..be10bcc 100644 --- a/lib/models/space.dart +++ b/lib/models/space.dart @@ -1,10 +1,14 @@ import "package:flutter/widgets.dart"; import "package:freezed_annotation/freezed_annotation.dart"; -import "package:matrix/matrix.dart"; +import "package:nexus/models/full_room.dart"; part "space.freezed.dart"; @freezed abstract class Space with _$Space { - const factory Space({required Room roomData, required Image? avatar}) = - _Space; + const factory Space({ + required String title, + required Widget? avatar, + required List children, + @Default(false) bool fake, + }) = _Space; } diff --git a/lib/models/space.freezed.dart b/lib/models/space.freezed.dart index b2a36e9..31439d0 100644 --- a/lib/models/space.freezed.dart +++ b/lib/models/space.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$Space { - Room get roomData; Image? get avatar; + String get title; Widget? get avatar; List get children; bool get fake; /// Create a copy of Space /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -25,16 +25,16 @@ $SpaceCopyWith get copyWith => _$SpaceCopyWithImpl(this as Space, @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is Space&&(identical(other.roomData, roomData) || other.roomData == roomData)&&(identical(other.avatar, avatar) || other.avatar == avatar)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is Space&&(identical(other.title, title) || other.title == title)&&(identical(other.avatar, avatar) || other.avatar == avatar)&&const DeepCollectionEquality().equals(other.children, children)&&(identical(other.fake, fake) || other.fake == fake)); } @override -int get hashCode => Object.hash(runtimeType,roomData,avatar); +int get hashCode => Object.hash(runtimeType,title,avatar,const DeepCollectionEquality().hash(children),fake); @override String toString() { - return 'Space(roomData: $roomData, avatar: $avatar)'; + return 'Space(title: $title, avatar: $avatar, children: $children, fake: $fake)'; } @@ -45,7 +45,7 @@ abstract mixin class $SpaceCopyWith<$Res> { factory $SpaceCopyWith(Space value, $Res Function(Space) _then) = _$SpaceCopyWithImpl; @useResult $Res call({ - Room roomData, Image? avatar + String title, Widget? avatar, List children, bool fake }); @@ -62,11 +62,13 @@ class _$SpaceCopyWithImpl<$Res> /// Create a copy of Space /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? roomData = null,Object? avatar = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? title = null,Object? avatar = freezed,Object? children = null,Object? fake = null,}) { return _then(_self.copyWith( -roomData: null == roomData ? _self.roomData : roomData // ignore: cast_nullable_to_non_nullable -as Room,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable -as Image?, +title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable +as Widget?,children: null == children ? _self.children : children // ignore: cast_nullable_to_non_nullable +as List,fake: null == fake ? _self.fake : fake // ignore: cast_nullable_to_non_nullable +as bool, )); } @@ -151,10 +153,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( Room roomData, Image? avatar)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String title, Widget? avatar, List children, bool fake)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _Space() when $default != null: -return $default(_that.roomData,_that.avatar);case _: +return $default(_that.title,_that.avatar,_that.children,_that.fake);case _: return orElse(); } @@ -172,10 +174,10 @@ return $default(_that.roomData,_that.avatar);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( Room roomData, Image? avatar) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String title, Widget? avatar, List children, bool fake) $default,) {final _that = this; switch (_that) { case _Space(): -return $default(_that.roomData,_that.avatar);case _: +return $default(_that.title,_that.avatar,_that.children,_that.fake);case _: throw StateError('Unexpected subclass'); } @@ -192,10 +194,10 @@ return $default(_that.roomData,_that.avatar);case _: /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( Room roomData, Image? avatar)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String title, Widget? avatar, List children, bool fake)? $default,) {final _that = this; switch (_that) { case _Space() when $default != null: -return $default(_that.roomData,_that.avatar);case _: +return $default(_that.title,_that.avatar,_that.children,_that.fake);case _: return null; } @@ -207,11 +209,19 @@ return $default(_that.roomData,_that.avatar);case _: class _Space implements Space { - const _Space({required this.roomData, required this.avatar}); + const _Space({required this.title, required this.avatar, required final List children, this.fake = false}): _children = children; -@override final Room roomData; -@override final Image? avatar; +@override final String title; +@override final Widget? avatar; + final List _children; +@override List get children { + if (_children is EqualUnmodifiableListView) return _children; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_children); +} + +@override@JsonKey() final bool fake; /// Create a copy of Space /// with the given fields replaced by the non-null parameter values. @@ -223,16 +233,16 @@ _$SpaceCopyWith<_Space> get copyWith => __$SpaceCopyWithImpl<_Space>(this, _$ide @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _Space&&(identical(other.roomData, roomData) || other.roomData == roomData)&&(identical(other.avatar, avatar) || other.avatar == avatar)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Space&&(identical(other.title, title) || other.title == title)&&(identical(other.avatar, avatar) || other.avatar == avatar)&&const DeepCollectionEquality().equals(other._children, _children)&&(identical(other.fake, fake) || other.fake == fake)); } @override -int get hashCode => Object.hash(runtimeType,roomData,avatar); +int get hashCode => Object.hash(runtimeType,title,avatar,const DeepCollectionEquality().hash(_children),fake); @override String toString() { - return 'Space(roomData: $roomData, avatar: $avatar)'; + return 'Space(title: $title, avatar: $avatar, children: $children, fake: $fake)'; } @@ -243,7 +253,7 @@ abstract mixin class _$SpaceCopyWith<$Res> implements $SpaceCopyWith<$Res> { factory _$SpaceCopyWith(_Space value, $Res Function(_Space) _then) = __$SpaceCopyWithImpl; @override @useResult $Res call({ - Room roomData, Image? avatar + String title, Widget? avatar, List children, bool fake }); @@ -260,11 +270,13 @@ class __$SpaceCopyWithImpl<$Res> /// Create a copy of Space /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? roomData = null,Object? avatar = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? title = null,Object? avatar = freezed,Object? children = null,Object? fake = null,}) { return _then(_Space( -roomData: null == roomData ? _self.roomData : roomData // ignore: cast_nullable_to_non_nullable -as Room,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable -as Image?, +title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable +as Widget?,children: null == children ? _self._children : children // ignore: cast_nullable_to_non_nullable +as List,fake: null == fake ? _self.fake : fake // ignore: cast_nullable_to_non_nullable +as bool, )); } diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart new file mode 100644 index 0000000..2d2bf1b --- /dev/null +++ b/lib/widgets/avatar.dart @@ -0,0 +1,25 @@ +import "package:color_hash/color_hash.dart"; +import "package:flutter/widgets.dart"; + +class Avatar extends StatelessWidget { + final Widget? avatar; + final String title; + final Widget? fallback; + const Avatar(this.avatar, this.title, {this.fallback, super.key}); + + @override + Widget build(BuildContext context) => ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(4)), + child: SizedBox( + width: 24, + height: 24, + child: + avatar ?? + fallback ?? + ColoredBox( + color: ColorHash(title).color, + child: Center(child: Text(title[0])), + ), + ), + ); +} diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart index 8a82f68..63f4948 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -1,8 +1,9 @@ -import "package:color_hash/color_hash.dart"; import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/spaces_controller.dart"; +import "package:nexus/helpers/extension_helper.dart"; +import "package:nexus/widgets/avatar.dart"; class Sidebar extends HookConsumerWidget { const Sidebar({super.key}); @@ -14,89 +15,75 @@ class Sidebar extends HookConsumerWidget { shape: Border(), child: Row( children: [ - NavigationRail( - scrollable: true, - useIndicator: false, - onDestinationSelected: (value) => index.value = value, - destinations: [ - NavigationRailDestination( - icon: Icon(Icons.home), - label: Text("Home"), - padding: EdgeInsets.only(top: 12), - ), - NavigationRailDestination( - icon: Icon(Icons.person), - label: Text("Messages"), - padding: EdgeInsets.only(top: 12), - ), - ...ref - .watch(SpacesController.provider) - .when( - loading: () => [], - error: (error, stack) { - debugPrintStack( - label: error.toString(), - stackTrace: stack, - ); - throw error; - }, - data: (spaces) => spaces.map( - (space) => NavigationRailDestination( - icon: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(12)), - child: SizedBox( - width: 40, - height: 40, - child: - space.avatar ?? - ColoredBox( - color: ColorHash(space.roomData.name).color, - child: Center( - child: Text(space.roomData.name[0]), - ), - ), - ), + ref + .watch(SpacesController.provider) + .when( + loading: SizedBox.shrink, + error: (error, stack) { + debugPrintStack(label: error.toString(), stackTrace: stack); + throw error; + }, + data: (spaces) => NavigationRail( + scrollable: true, + onDestinationSelected: (value) => index.value = value, + destinations: spaces + .map( + (space) => NavigationRailDestination( + icon: Avatar(space.avatar, space.title), + label: Text(space.title), + padding: EdgeInsets.only(top: 4), ), - label: Text(space.roomData.name), - padding: EdgeInsets.only(top: 12), - ), - ), - ), - ], - selectedIndex: index.value, - ), + ) + .toList(), + selectedIndex: index.value, + ), + ), Expanded( - child: Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - title: Text("Some Space"), - backgroundColor: Colors.transparent, - ), - body: NavigationRail( - scrollable: true, - backgroundColor: Colors.transparent, - extended: true, - destinations: [ - NavigationRailDestination( - icon: Icon(Icons.numbers), - label: Text("Room 1"), - ), - NavigationRailDestination( - icon: Icon(Icons.numbers), - label: Text("Room 2"), - ), - NavigationRailDestination( - icon: Icon(Icons.numbers), - label: Text("Room 3"), - ), - NavigationRailDestination( - icon: Icon(Icons.numbers), - label: Text("Room 4"), - ), - ], - selectedIndex: 0, - ), - ), + child: ref + .watch(SpacesController.provider) + .betterWhen( + data: (spaces) { + final space = spaces[index.value]; + return Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + title: Row( + children: [ + Avatar(space.avatar, space.title), + SizedBox(width: 12), + Expanded( + child: Text( + space.title, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + backgroundColor: Colors.transparent, + ), + body: NavigationRail( + scrollable: true, + backgroundColor: Colors.transparent, + extended: true, + destinations: space.children + .map( + (room) => NavigationRailDestination( + icon: Avatar( + room.avatar, + room.title, + fallback: index.value == 1 + ? null + : Icon(Icons.numbers), + ), + label: Text(room.title), + ), + ) + .toList(), + selectedIndex: space.children.isEmpty ? null : 0, + ), + ); + }, + ), ), ], ),