lots of stuff
This commit is contained in:
parent
8bc010cfc7
commit
ba9e99a951
19 changed files with 608 additions and 360 deletions
17
lib/controllers/avatar_controller.dart
Normal file
17
lib/controllers/avatar_controller.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:matrix/matrix.dart";
|
||||||
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
|
|
||||||
|
class AvatarController extends AsyncNotifier<Uri> {
|
||||||
|
final String mxc;
|
||||||
|
AvatarController(this.mxc);
|
||||||
|
@override
|
||||||
|
Future<Uri> build() async => Uri.parse(mxc).getThumbnailUri(
|
||||||
|
await ref.watch(ClientController.provider.future),
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
);
|
||||||
|
|
||||||
|
static final provider = AsyncNotifierProvider.family
|
||||||
|
.autoDispose<AvatarController, Uri, String>(AvatarController.new);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
|
|
||||||
import "package:flutter/foundation.dart";
|
import "package:flutter/foundation.dart";
|
||||||
import "package:matrix/matrix.dart";
|
import "package:matrix/matrix.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
|
@ -29,6 +28,8 @@ class ClientController extends AsyncNotifier<Client> {
|
||||||
if (client.accessToken == null) {
|
if (client.accessToken == null) {
|
||||||
await client.login(
|
await client.login(
|
||||||
LoginType.mLoginPassword,
|
LoginType.mLoginPassword,
|
||||||
|
initialDeviceDisplayName: "Nexus Client",
|
||||||
|
deviceId: "temp", // TODO
|
||||||
identifier: AuthenticationUserIdentifier(user: "quadradical"),
|
identifier: AuthenticationUserIdentifier(user: "quadradical"),
|
||||||
password: File("./password.txt").readAsStringSync(),
|
password: File("./password.txt").readAsStringSync(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
20
lib/controllers/members_controller.dart
Normal file
20
lib/controllers/members_controller.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:matrix/matrix.dart";
|
||||||
|
|
||||||
|
class MembersController extends AsyncNotifier<List<MatrixEvent>> {
|
||||||
|
final Room room;
|
||||||
|
MembersController(this.room);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<MatrixEvent>> build() async =>
|
||||||
|
(await room.client.getMembersByRoom(
|
||||||
|
room.id,
|
||||||
|
notMembership: Membership.leave,
|
||||||
|
)) ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider.family<MembersController, List<MatrixEvent>, Room>(
|
||||||
|
MembersController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,22 +3,26 @@ import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart" as chat;
|
import "package:flutter_chat_core/flutter_chat_core.dart" as chat;
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:matrix/matrix.dart";
|
import "package:matrix/matrix.dart";
|
||||||
|
import "package:nexus/controllers/timeline_controller.dart";
|
||||||
import "package:nexus/helpers/extension_helper.dart";
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
|
|
||||||
class RoomChatController extends AsyncNotifier<ChatController> {
|
class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
RoomChatController(this.room);
|
|
||||||
final Room room;
|
final Room room;
|
||||||
|
RoomChatController(this.room);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ChatController> build() async {
|
Future<ChatController> build() async {
|
||||||
final timeline = await room.getTimeline();
|
final timeline = await ref.watch(TimelineController.provider(room).future);
|
||||||
|
|
||||||
|
ref.onDispose(
|
||||||
room.client.onTimelineEvent.stream.listen((event) async {
|
room.client.onTimelineEvent.stream.listen((event) async {
|
||||||
if (event.roomId != room.id) return;
|
if (event.roomId != room.id) return;
|
||||||
final message = await event.toMessage();
|
final message = await event.toMessage();
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
await insertMessage(message);
|
await insertMessage(message);
|
||||||
}
|
}
|
||||||
});
|
}).cancel,
|
||||||
|
);
|
||||||
|
|
||||||
return InMemoryChatController(
|
return InMemoryChatController(
|
||||||
messages: (await Future.wait(
|
messages: (await Future.wait(
|
||||||
|
|
@ -29,8 +33,11 @@ class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
|
|
||||||
Future<void> insertMessage(Message message) async {
|
Future<void> insertMessage(Message message) async {
|
||||||
final controller = await future;
|
final controller = await future;
|
||||||
final oldMessage = controller.messages.firstWhereOrNull(
|
final oldMessage = message.metadata?["txnId"] == null
|
||||||
(element) => element.metadata?["txnId"] == message.metadata?["txnId"],
|
? null
|
||||||
|
: controller.messages.firstWhereOrNull(
|
||||||
|
(element) =>
|
||||||
|
element.metadata?["txnId"] == message.metadata?["txnId"],
|
||||||
);
|
);
|
||||||
|
|
||||||
return oldMessage == null
|
return oldMessage == null
|
||||||
|
|
@ -38,6 +45,10 @@ class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
: controller.updateMessage(oldMessage, message);
|
: controller.updateMessage(oldMessage, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadOlder() async {
|
||||||
|
await ref.watch(TimelineController.provider(room).notifier).prev();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> updateMessage(Message message, Message newMessage) async {
|
Future<void> updateMessage(Message message, Message newMessage) async {
|
||||||
final controller = await future;
|
final controller = await future;
|
||||||
return controller.updateMessage(message, newMessage);
|
return controller.updateMessage(message, newMessage);
|
||||||
|
|
@ -55,6 +66,7 @@ class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
id: id,
|
id: id,
|
||||||
name: user.displayname,
|
name: user.displayname,
|
||||||
imageSource: (await user.avatarUrl?.getThumbnailUri(
|
imageSource: (await user.avatarUrl?.getThumbnailUri(
|
||||||
|
// TODO: Fix use of account avatar not room avatar
|
||||||
room.client,
|
room.client,
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
|
|
|
||||||
|
|
@ -30,24 +30,27 @@ class SpacesController extends AsyncNotifier<List<Space>> {
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Space(
|
Space(
|
||||||
|
client: client,
|
||||||
title: "Home",
|
title: "Home",
|
||||||
children: topLevelRooms,
|
children: topLevelRooms,
|
||||||
avatar: Icon(Icons.home),
|
icon: Icon(Icons.home),
|
||||||
fake: true,
|
fake: true,
|
||||||
),
|
),
|
||||||
Space(
|
Space(
|
||||||
|
client: client,
|
||||||
title: "Direct Messages",
|
title: "Direct Messages",
|
||||||
children: await Future.wait(
|
children: await Future.wait(
|
||||||
client.rooms
|
client.rooms
|
||||||
.where((room) => room.isDirectChat)
|
.where((room) => room.isDirectChat)
|
||||||
.map((room) => room.fullRoom),
|
.map((room) => room.fullRoom),
|
||||||
),
|
),
|
||||||
avatar: Icon(Icons.person),
|
icon: Icon(Icons.person),
|
||||||
fake: true,
|
fake: true,
|
||||||
),
|
),
|
||||||
...(await Future.wait(
|
...(await Future.wait(
|
||||||
topLevelSpaces.map(
|
topLevelSpaces.map(
|
||||||
(space) async => Space(
|
(space) async => Space(
|
||||||
|
client: client,
|
||||||
title: space.title,
|
title: space.title,
|
||||||
avatar: space.avatar,
|
avatar: space.avatar,
|
||||||
children: await Future.wait(
|
children: await Future.wait(
|
||||||
|
|
|
||||||
21
lib/controllers/timeline_controller.dart
Normal file
21
lib/controllers/timeline_controller.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:matrix/matrix.dart";
|
||||||
|
|
||||||
|
class TimelineController extends AsyncNotifier<Timeline> {
|
||||||
|
TimelineController(this.room);
|
||||||
|
final Room room;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Timeline> build() => room.getTimeline();
|
||||||
|
|
||||||
|
Future<void> prev() async {
|
||||||
|
final timeline = await future;
|
||||||
|
await timeline.requestHistory();
|
||||||
|
state = AsyncValue.data(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider.family<TimelineController, Timeline, Room>(
|
||||||
|
TimelineController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -21,23 +21,15 @@ extension BetterWhen<T> on AsyncValue<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension GetFullRoom on Room {
|
extension GetFullRoom on Room {
|
||||||
Future<FullRoom> get fullRoom async {
|
Future<FullRoom> get fullRoom async => FullRoom(
|
||||||
return FullRoom(
|
|
||||||
roomData: this,
|
roomData: this,
|
||||||
title: getLocalizedDisplayname(),
|
title: getLocalizedDisplayname(),
|
||||||
avatar: await avatar?.asImage(client),
|
avatar: await avatar?.getThumbnailUri(client, width: 24, height: 24),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension GetImage on Uri {
|
extension GetHeaders on Client {
|
||||||
Future<Image?> asImage(Client client) async {
|
Map<String, String> get headers => {"authorization": "Bearer $accessToken"};
|
||||||
final thumb = await getThumbnailUri(client, width: 24, height: 24);
|
|
||||||
return Image.network(
|
|
||||||
thumb.toString(),
|
|
||||||
headers: {"authorization": "Bearer ${client.accessToken}"},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ToMessage on Event {
|
extension ToMessage on Event {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import "package:flutter/widgets.dart";
|
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
import "package:matrix/matrix.dart";
|
import "package:matrix/matrix.dart";
|
||||||
part "full_room.freezed.dart";
|
part "full_room.freezed.dart";
|
||||||
|
|
@ -8,6 +7,6 @@ abstract class FullRoom with _$FullRoom {
|
||||||
const factory FullRoom({
|
const factory FullRoom({
|
||||||
required Room roomData,
|
required Room roomData,
|
||||||
required String title,
|
required String title,
|
||||||
required Image? avatar,
|
required Uri? avatar,
|
||||||
}) = _FullRoom;
|
}) = _FullRoom;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$FullRoom {
|
mixin _$FullRoom {
|
||||||
|
|
||||||
Room get roomData; String get title; Image? get avatar;
|
Room get roomData; String get title; Uri? get avatar;
|
||||||
/// Create a copy of FullRoom
|
/// Create a copy of FullRoom
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
|
@ -45,7 +45,7 @@ abstract mixin class $FullRoomCopyWith<$Res> {
|
||||||
factory $FullRoomCopyWith(FullRoom value, $Res Function(FullRoom) _then) = _$FullRoomCopyWithImpl;
|
factory $FullRoomCopyWith(FullRoom value, $Res Function(FullRoom) _then) = _$FullRoomCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
Room roomData, String title, Image? avatar
|
Room roomData, String title, Uri? avatar
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ class _$FullRoomCopyWithImpl<$Res>
|
||||||
roomData: null == roomData ? _self.roomData : roomData // ignore: cast_nullable_to_non_nullable
|
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 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 String,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable
|
||||||
as Image?,
|
as Uri?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,7 +152,7 @@ return $default(_that);case _:
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Room roomData, String title, Image? avatar)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Room roomData, String title, Uri? avatar)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _FullRoom() when $default != null:
|
case _FullRoom() when $default != null:
|
||||||
return $default(_that.roomData,_that.title,_that.avatar);case _:
|
return $default(_that.roomData,_that.title,_that.avatar);case _:
|
||||||
|
|
@ -173,7 +173,7 @@ return $default(_that.roomData,_that.title,_that.avatar);case _:
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Room roomData, String title, Image? avatar) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Room roomData, String title, Uri? avatar) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _FullRoom():
|
case _FullRoom():
|
||||||
return $default(_that.roomData,_that.title,_that.avatar);case _:
|
return $default(_that.roomData,_that.title,_that.avatar);case _:
|
||||||
|
|
@ -193,7 +193,7 @@ return $default(_that.roomData,_that.title,_that.avatar);case _:
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Room roomData, String title, Image? avatar)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Room roomData, String title, Uri? avatar)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _FullRoom() when $default != null:
|
case _FullRoom() when $default != null:
|
||||||
return $default(_that.roomData,_that.title,_that.avatar);case _:
|
return $default(_that.roomData,_that.title,_that.avatar);case _:
|
||||||
|
|
@ -213,7 +213,7 @@ class _FullRoom implements FullRoom {
|
||||||
|
|
||||||
@override final Room roomData;
|
@override final Room roomData;
|
||||||
@override final String title;
|
@override final String title;
|
||||||
@override final Image? avatar;
|
@override final Uri? avatar;
|
||||||
|
|
||||||
/// Create a copy of FullRoom
|
/// Create a copy of FullRoom
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
|
@ -245,7 +245,7 @@ abstract mixin class _$FullRoomCopyWith<$Res> implements $FullRoomCopyWith<$Res>
|
||||||
factory _$FullRoomCopyWith(_FullRoom value, $Res Function(_FullRoom) _then) = __$FullRoomCopyWithImpl;
|
factory _$FullRoomCopyWith(_FullRoom value, $Res Function(_FullRoom) _then) = __$FullRoomCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
Room roomData, String title, Image? avatar
|
Room roomData, String title, Uri? avatar
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -267,7 +267,7 @@ class __$FullRoomCopyWithImpl<$Res>
|
||||||
roomData: null == roomData ? _self.roomData : roomData // ignore: cast_nullable_to_non_nullable
|
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 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 String,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable
|
||||||
as Image?,
|
as Uri?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import "package:flutter/widgets.dart";
|
import "package:flutter/widgets.dart";
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
import "package:matrix/matrix.dart";
|
||||||
import "package:nexus/models/full_room.dart";
|
import "package:nexus/models/full_room.dart";
|
||||||
part "space.freezed.dart";
|
part "space.freezed.dart";
|
||||||
|
|
||||||
|
|
@ -7,8 +8,10 @@ part "space.freezed.dart";
|
||||||
abstract class Space with _$Space {
|
abstract class Space with _$Space {
|
||||||
const factory Space({
|
const factory Space({
|
||||||
required String title,
|
required String title,
|
||||||
required Widget? avatar,
|
|
||||||
required List<FullRoom> children,
|
required List<FullRoom> children,
|
||||||
|
required Client client,
|
||||||
@Default(false) bool fake,
|
@Default(false) bool fake,
|
||||||
|
Uri? avatar,
|
||||||
|
Icon? icon,
|
||||||
}) = _Space;
|
}) = _Space;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$Space {
|
mixin _$Space {
|
||||||
|
|
||||||
String get title; Widget? get avatar; List<FullRoom> get children; bool get fake;
|
String get title; List<FullRoom> get children; Client get client; bool get fake; Uri? get avatar; Icon? get icon;
|
||||||
/// Create a copy of Space
|
/// Create a copy of Space
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
|
@ -25,16 +25,16 @@ $SpaceCopyWith<Space> get copyWith => _$SpaceCopyWithImpl<Space>(this as Space,
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
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));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is Space&&(identical(other.title, title) || other.title == title)&&const DeepCollectionEquality().equals(other.children, children)&&(identical(other.client, client) || other.client == client)&&(identical(other.fake, fake) || other.fake == fake)&&(identical(other.avatar, avatar) || other.avatar == avatar)&&(identical(other.icon, icon) || other.icon == icon));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,title,avatar,const DeepCollectionEquality().hash(children),fake);
|
int get hashCode => Object.hash(runtimeType,title,const DeepCollectionEquality().hash(children),client,fake,avatar,icon);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'Space(title: $title, avatar: $avatar, children: $children, fake: $fake)';
|
return 'Space(title: $title, children: $children, client: $client, fake: $fake, avatar: $avatar, icon: $icon)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ abstract mixin class $SpaceCopyWith<$Res> {
|
||||||
factory $SpaceCopyWith(Space value, $Res Function(Space) _then) = _$SpaceCopyWithImpl;
|
factory $SpaceCopyWith(Space value, $Res Function(Space) _then) = _$SpaceCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String title, Widget? avatar, List<FullRoom> children, bool fake
|
String title, List<FullRoom> children, Client client, bool fake, Uri? avatar, Icon? icon
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -62,13 +62,15 @@ class _$SpaceCopyWithImpl<$Res>
|
||||||
|
|
||||||
/// Create a copy of Space
|
/// Create a copy of Space
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? title = null,Object? avatar = freezed,Object? children = null,Object? fake = null,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? title = null,Object? children = null,Object? client = null,Object? fake = null,Object? avatar = freezed,Object? icon = freezed,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
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 String,children: null == children ? _self.children : children // ignore: cast_nullable_to_non_nullable
|
||||||
as Widget?,children: null == children ? _self.children : children // ignore: cast_nullable_to_non_nullable
|
as List<FullRoom>,client: null == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
|
||||||
as List<FullRoom>,fake: null == fake ? _self.fake : fake // ignore: cast_nullable_to_non_nullable
|
as Client,fake: null == fake ? _self.fake : fake // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Uri?,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Icon?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,10 +155,10 @@ return $default(_that);case _:
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String title, Widget? avatar, List<FullRoom> children, bool fake)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String title, List<FullRoom> children, Client client, bool fake, Uri? avatar, Icon? icon)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Space() when $default != null:
|
case _Space() when $default != null:
|
||||||
return $default(_that.title,_that.avatar,_that.children,_that.fake);case _:
|
return $default(_that.title,_that.children,_that.client,_that.fake,_that.avatar,_that.icon);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -174,10 +176,10 @@ return $default(_that.title,_that.avatar,_that.children,_that.fake);case _:
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String title, Widget? avatar, List<FullRoom> children, bool fake) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String title, List<FullRoom> children, Client client, bool fake, Uri? avatar, Icon? icon) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Space():
|
case _Space():
|
||||||
return $default(_that.title,_that.avatar,_that.children,_that.fake);case _:
|
return $default(_that.title,_that.children,_that.client,_that.fake,_that.avatar,_that.icon);case _:
|
||||||
throw StateError('Unexpected subclass');
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -194,10 +196,10 @@ return $default(_that.title,_that.avatar,_that.children,_that.fake);case _:
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String title, Widget? avatar, List<FullRoom> children, bool fake)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String title, List<FullRoom> children, Client client, bool fake, Uri? avatar, Icon? icon)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Space() when $default != null:
|
case _Space() when $default != null:
|
||||||
return $default(_that.title,_that.avatar,_that.children,_that.fake);case _:
|
return $default(_that.title,_that.children,_that.client,_that.fake,_that.avatar,_that.icon);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -209,11 +211,10 @@ return $default(_that.title,_that.avatar,_that.children,_that.fake);case _:
|
||||||
|
|
||||||
|
|
||||||
class _Space implements Space {
|
class _Space implements Space {
|
||||||
const _Space({required this.title, required this.avatar, required final List<FullRoom> children, this.fake = false}): _children = children;
|
const _Space({required this.title, required final List<FullRoom> children, required this.client, this.fake = false, this.avatar, this.icon}): _children = children;
|
||||||
|
|
||||||
|
|
||||||
@override final String title;
|
@override final String title;
|
||||||
@override final Widget? avatar;
|
|
||||||
final List<FullRoom> _children;
|
final List<FullRoom> _children;
|
||||||
@override List<FullRoom> get children {
|
@override List<FullRoom> get children {
|
||||||
if (_children is EqualUnmodifiableListView) return _children;
|
if (_children is EqualUnmodifiableListView) return _children;
|
||||||
|
|
@ -221,7 +222,10 @@ class _Space implements Space {
|
||||||
return EqualUnmodifiableListView(_children);
|
return EqualUnmodifiableListView(_children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override final Client client;
|
||||||
@override@JsonKey() final bool fake;
|
@override@JsonKey() final bool fake;
|
||||||
|
@override final Uri? avatar;
|
||||||
|
@override final Icon? icon;
|
||||||
|
|
||||||
/// Create a copy of Space
|
/// Create a copy of Space
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
|
@ -233,16 +237,16 @@ _$SpaceCopyWith<_Space> get copyWith => __$SpaceCopyWithImpl<_Space>(this, _$ide
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
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));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Space&&(identical(other.title, title) || other.title == title)&&const DeepCollectionEquality().equals(other._children, _children)&&(identical(other.client, client) || other.client == client)&&(identical(other.fake, fake) || other.fake == fake)&&(identical(other.avatar, avatar) || other.avatar == avatar)&&(identical(other.icon, icon) || other.icon == icon));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,title,avatar,const DeepCollectionEquality().hash(_children),fake);
|
int get hashCode => Object.hash(runtimeType,title,const DeepCollectionEquality().hash(_children),client,fake,avatar,icon);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'Space(title: $title, avatar: $avatar, children: $children, fake: $fake)';
|
return 'Space(title: $title, children: $children, client: $client, fake: $fake, avatar: $avatar, icon: $icon)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -253,7 +257,7 @@ abstract mixin class _$SpaceCopyWith<$Res> implements $SpaceCopyWith<$Res> {
|
||||||
factory _$SpaceCopyWith(_Space value, $Res Function(_Space) _then) = __$SpaceCopyWithImpl;
|
factory _$SpaceCopyWith(_Space value, $Res Function(_Space) _then) = __$SpaceCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String title, Widget? avatar, List<FullRoom> children, bool fake
|
String title, List<FullRoom> children, Client client, bool fake, Uri? avatar, Icon? icon
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -270,13 +274,15 @@ class __$SpaceCopyWithImpl<$Res>
|
||||||
|
|
||||||
/// Create a copy of Space
|
/// Create a copy of Space
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? title = null,Object? avatar = freezed,Object? children = null,Object? fake = null,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? title = null,Object? children = null,Object? client = null,Object? fake = null,Object? avatar = freezed,Object? icon = freezed,}) {
|
||||||
return _then(_Space(
|
return _then(_Space(
|
||||||
title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
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 String,children: null == children ? _self._children : children // ignore: cast_nullable_to_non_nullable
|
||||||
as Widget?,children: null == children ? _self._children : children // ignore: cast_nullable_to_non_nullable
|
as List<FullRoom>,client: null == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
|
||||||
as List<FullRoom>,fake: null == fake ? _self.fake : fake // ignore: cast_nullable_to_non_nullable
|
as Client,fake: null == fake ? _self.fake : fake // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Uri?,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Icon?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
38
lib/widgets/avatar_or_hash.dart
Normal file
38
lib/widgets/avatar_or_hash.dart
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import "package:color_hash/color_hash.dart";
|
||||||
|
import "package:flutter/widgets.dart";
|
||||||
|
|
||||||
|
class AvatarOrHash extends StatelessWidget {
|
||||||
|
final Uri? avatar;
|
||||||
|
final String title;
|
||||||
|
final Widget? fallback;
|
||||||
|
final Map<String, String> headers;
|
||||||
|
const AvatarOrHash(
|
||||||
|
this.avatar,
|
||||||
|
this.title, {
|
||||||
|
this.fallback,
|
||||||
|
required this.headers,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final box = ColoredBox(
|
||||||
|
color: ColorHash(title).color,
|
||||||
|
child: Center(child: Text(title[0])),
|
||||||
|
);
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: avatar == null
|
||||||
|
? fallback ?? box
|
||||||
|
: Image.network(
|
||||||
|
avatar.toString(),
|
||||||
|
headers: headers,
|
||||||
|
errorBuilder: (_, _, _) => box,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
lib/widgets/chat_box.dart
Normal file
64
lib/widgets/chat_box.dart
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
|
import "package:flutter_chat_ui/flutter_chat_ui.dart";
|
||||||
|
|
||||||
|
class ChatBox extends StatelessWidget {
|
||||||
|
final Message? replyToMessage;
|
||||||
|
final VoidCallback onDismiss;
|
||||||
|
final Map<String, String> headers;
|
||||||
|
const ChatBox({
|
||||||
|
required this.replyToMessage,
|
||||||
|
required this.onDismiss,
|
||||||
|
required this.headers,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Composer(
|
||||||
|
sigmaX: 0,
|
||||||
|
sigmaY: 0,
|
||||||
|
sendIconColor: Theme.of(context).colorScheme.primary,
|
||||||
|
sendOnEnter: true,
|
||||||
|
topWidget: replyToMessage == null
|
||||||
|
? null
|
||||||
|
: ColoredBox(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
userId: replyToMessage!.authorId,
|
||||||
|
headers: headers,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
replyToMessage!.metadata?["displayName"] ??
|
||||||
|
replyToMessage!.authorId,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: (replyToMessage is TextMessage)
|
||||||
|
? Text(
|
||||||
|
(replyToMessage as TextMessage).text,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
: SizedBox(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onDismiss,
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
iconSize: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
autofocus: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
53
lib/widgets/member_list.dart
Normal file
53
lib/widgets/member_list.dart
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:matrix/matrix.dart";
|
||||||
|
import "package:nexus/controllers/avatar_controller.dart";
|
||||||
|
import "package:nexus/controllers/members_controller.dart";
|
||||||
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
|
class MemberList extends ConsumerWidget {
|
||||||
|
final Room room;
|
||||||
|
const MemberList(this.room, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) => ColoredBox(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 240,
|
||||||
|
child: ref
|
||||||
|
.watch(MembersController.provider(room))
|
||||||
|
.betterWhen(
|
||||||
|
data: (members) => ListView(
|
||||||
|
children: [
|
||||||
|
...members
|
||||||
|
.where(
|
||||||
|
(membership) =>
|
||||||
|
membership.content["membership"] ==
|
||||||
|
Membership.join.name,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(member) => ListTile(
|
||||||
|
leading: AvatarOrHash(
|
||||||
|
ref
|
||||||
|
.watch(
|
||||||
|
AvatarController.provider(
|
||||||
|
member.content["avatar_url"].toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.whenOrNull(data: (data) => data),
|
||||||
|
member.content["displayname"].toString(),
|
||||||
|
headers: room.client.headers,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
member.content["displayname"].toString(),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
66
lib/widgets/room_appbar.dart
Normal file
66
lib/widgets/room_appbar.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
|
import "package:nexus/models/full_room.dart";
|
||||||
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
|
class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
final bool isDesktop;
|
||||||
|
final FullRoom room;
|
||||||
|
final VoidCallback onOpenMemberList;
|
||||||
|
final VoidCallback onOpenDrawer;
|
||||||
|
const RoomAppbar(
|
||||||
|
this.room, {
|
||||||
|
required this.isDesktop,
|
||||||
|
required this.onOpenMemberList,
|
||||||
|
required this.onOpenDrawer,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => Size.fromHeight(AppBar().preferredSize.height + 16);
|
||||||
|
|
||||||
|
@override
|
||||||
|
AppBar build(BuildContext context) => AppBar(
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: Size.zero, // Does this even matter??
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(8).copyWith(top: 0),
|
||||||
|
child: Text(
|
||||||
|
room.roomData.topic,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leading: isDesktop ? null : DrawerButton(onPressed: onOpenDrawer),
|
||||||
|
actionsPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
AvatarOrHash(
|
||||||
|
room.avatar,
|
||||||
|
room.title,
|
||||||
|
fallback: Icon(Icons.numbers),
|
||||||
|
headers: room.roomData.client.headers,
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Expanded(child: Text(room.title, overflow: TextOverflow.ellipsis)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(onPressed: onOpenMemberList, icon: Icon(Icons.people)),
|
||||||
|
if (!(Platform.isAndroid || Platform.isIOS))
|
||||||
|
IconButton(onPressed: () => exit(0), icon: Icon(Icons.close)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import "package:color_hash/color_hash.dart";
|
|
||||||
import "package:flutter/widgets.dart";
|
|
||||||
|
|
||||||
class RoomAvatar extends StatelessWidget {
|
|
||||||
final Widget? avatar;
|
|
||||||
final String title;
|
|
||||||
final Widget? fallback;
|
|
||||||
const RoomAvatar(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])),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import "dart:io";
|
|
||||||
|
|
||||||
import "package:flutter/foundation.dart";
|
import "package:flutter/foundation.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
|
|
@ -15,8 +13,10 @@ import "package:nexus/controllers/current_room_controller.dart";
|
||||||
import "package:nexus/controllers/room_chat_controller.dart";
|
import "package:nexus/controllers/room_chat_controller.dart";
|
||||||
import "package:nexus/helpers/extension_helper.dart";
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
import "package:nexus/helpers/launch_helper.dart";
|
import "package:nexus/helpers/launch_helper.dart";
|
||||||
|
import "package:nexus/widgets/chat_box.dart";
|
||||||
|
import "package:nexus/widgets/member_list.dart";
|
||||||
|
import "package:nexus/widgets/room_appbar.dart";
|
||||||
import "package:nexus/widgets/top_widget.dart";
|
import "package:nexus/widgets/top_widget.dart";
|
||||||
import "package:nexus/widgets/room_avatar.dart";
|
|
||||||
|
|
||||||
class RoomChat extends HookConsumerWidget {
|
class RoomChat extends HookConsumerWidget {
|
||||||
final bool isDesktop;
|
final bool isDesktop;
|
||||||
|
|
@ -33,12 +33,18 @@ class RoomChat extends HookConsumerWidget {
|
||||||
Offset.zero & (context.findRenderObject() as RenderBox).size,
|
Offset.zero & (context.findRenderObject() as RenderBox).size,
|
||||||
),
|
),
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
items: [PopupMenuItem(onTap: onTap, child: Text("Reply"))],
|
items: [
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: onTap,
|
||||||
|
child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final replyToMessage = useState<Message?>(null);
|
final replyToMessage = useState<Message?>(null);
|
||||||
|
final memberListOpened = useState<bool>(isDesktop);
|
||||||
final urlRegex = RegExp(r"https?://[^\s\]\(\)]+");
|
final urlRegex = RegExp(r"https?://[^\s\]\(\)]+");
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return ref
|
return ref
|
||||||
|
|
@ -48,37 +54,18 @@ class RoomChat extends HookConsumerWidget {
|
||||||
final controllerProvider = RoomChatController.provider(
|
final controllerProvider = RoomChatController.provider(
|
||||||
room.roomData,
|
room.roomData,
|
||||||
);
|
);
|
||||||
final headers = {
|
|
||||||
"authorization": "Bearer ${room.roomData.client.accessToken}",
|
|
||||||
};
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: RoomAppbar(
|
||||||
leading: isDesktop
|
room,
|
||||||
? null
|
isDesktop: isDesktop,
|
||||||
: DrawerButton(onPressed: Scaffold.of(context).openDrawer),
|
onOpenDrawer: Scaffold.of(context).openDrawer,
|
||||||
actionsPadding: EdgeInsets.symmetric(horizontal: 8),
|
onOpenMemberList: () =>
|
||||||
title: Row(
|
memberListOpened.value = !memberListOpened.value,
|
||||||
|
),
|
||||||
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
RoomAvatar(
|
|
||||||
room.avatar,
|
|
||||||
room.title,
|
|
||||||
fallback: Icon(Icons.numbers),
|
|
||||||
),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(room.title, overflow: TextOverflow.ellipsis),
|
child: ref
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
if (!(Platform.isAndroid || Platform.isIOS))
|
|
||||||
IconButton(
|
|
||||||
onPressed: () => exit(0),
|
|
||||||
icon: Icon(Icons.close),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: ref
|
|
||||||
.watch(controllerProvider)
|
.watch(controllerProvider)
|
||||||
.betterWhen(
|
.betterWhen(
|
||||||
data: (controller) => Chat(
|
data: (controller) => Chat(
|
||||||
|
|
@ -112,85 +99,19 @@ class RoomChat extends HookConsumerWidget {
|
||||||
onTap: () => replyToMessage.value = message,
|
onTap: () => replyToMessage.value = message,
|
||||||
),
|
),
|
||||||
builders: Builders(
|
builders: Builders(
|
||||||
composerBuilder: (_) => Column(
|
chatAnimatedListBuilder: (context, itemBuilder) {
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
return ChatAnimatedList(
|
||||||
children: [
|
itemBuilder: itemBuilder,
|
||||||
if (replyToMessage.value != null)
|
onEndReached: ref
|
||||||
ColoredBox(
|
.watch(controllerProvider.notifier)
|
||||||
color: theme.colorScheme.surfaceContainer,
|
.loadOlder,
|
||||||
child: Padding(
|
);
|
||||||
padding: EdgeInsets.symmetric(
|
},
|
||||||
horizontal: 16,
|
composerBuilder: (_) => ChatBox(
|
||||||
vertical: 4,
|
replyToMessage: replyToMessage.value,
|
||||||
|
onDismiss: () => replyToMessage.value = null,
|
||||||
|
headers: room.roomData.client.headers,
|
||||||
),
|
),
|
||||||
child: Row(
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
Avatar(
|
|
||||||
userId: replyToMessage.value!.authorId,
|
|
||||||
headers: headers,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
replyToMessage
|
|
||||||
.value!
|
|
||||||
.metadata?["displayName"] ??
|
|
||||||
replyToMessage.value!.authorId,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.labelMedium
|
|
||||||
?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: (replyToMessage.value as dynamic)
|
|
||||||
? Text(
|
|
||||||
(replyToMessage.value
|
|
||||||
as TextMessage)
|
|
||||||
.text,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.labelMedium,
|
|
||||||
maxLines: 1,
|
|
||||||
)
|
|
||||||
: SizedBox(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () =>
|
|
||||||
replyToMessage.value = null,
|
|
||||||
icon: Icon(Icons.close),
|
|
||||||
iconSize: 20,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Composer(
|
|
||||||
sigmaX: 0,
|
|
||||||
sigmaY: 0,
|
|
||||||
sendIconColor: theme.colorScheme.primary,
|
|
||||||
sendOnEnter: true,
|
|
||||||
autofocus: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
unsupportedMessageBuilder:
|
|
||||||
(
|
|
||||||
_,
|
|
||||||
message,
|
|
||||||
index, {
|
|
||||||
required bool isSentByMe,
|
|
||||||
MessageGroupStatus? groupStatus,
|
|
||||||
}) => kDebugMode
|
|
||||||
? Text(
|
|
||||||
"${message.authorId} sent ${message.metadata?["eventType"]}",
|
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: SizedBox.shrink(),
|
|
||||||
textMessageBuilder:
|
textMessageBuilder:
|
||||||
(
|
(
|
||||||
context,
|
context,
|
||||||
|
|
@ -199,7 +120,11 @@ class RoomChat extends HookConsumerWidget {
|
||||||
required bool isSentByMe,
|
required bool isSentByMe,
|
||||||
MessageGroupStatus? groupStatus,
|
MessageGroupStatus? groupStatus,
|
||||||
}) => FlyerChatTextMessage(
|
}) => FlyerChatTextMessage(
|
||||||
topWidget: TopWidget(message, headers: headers),
|
topWidget: TopWidget(
|
||||||
|
message,
|
||||||
|
headers: room.roomData.client.headers,
|
||||||
|
groupStatus: groupStatus,
|
||||||
|
),
|
||||||
message: message.copyWith(
|
message: message.copyWith(
|
||||||
text: message.text.replaceAllMapped(
|
text: message.text.replaceAllMapped(
|
||||||
urlRegex,
|
urlRegex,
|
||||||
|
|
@ -219,7 +144,9 @@ class RoomChat extends HookConsumerWidget {
|
||||||
linkPreviewBuilder: (_, message, isSentByMe) =>
|
linkPreviewBuilder: (_, message, isSentByMe) =>
|
||||||
LinkPreview(
|
LinkPreview(
|
||||||
text:
|
text:
|
||||||
urlRegex.firstMatch(message.text)?.group(0) ??
|
urlRegex
|
||||||
|
.firstMatch(message.text)
|
||||||
|
?.group(0) ??
|
||||||
"",
|
"",
|
||||||
backgroundColor: isSentByMe
|
backgroundColor: isSentByMe
|
||||||
? theme.colorScheme.inversePrimary
|
? theme.colorScheme.inversePrimary
|
||||||
|
|
@ -229,12 +156,14 @@ class RoomChat extends HookConsumerWidget {
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
),
|
),
|
||||||
linkPreviewData: message.linkPreviewData,
|
linkPreviewData: message.linkPreviewData,
|
||||||
onLinkPreviewDataFetched: (linkPreviewData) => ref
|
onLinkPreviewDataFetched:
|
||||||
|
(linkPreviewData) => ref
|
||||||
.watch(controllerProvider.notifier)
|
.watch(controllerProvider.notifier)
|
||||||
.updateMessage(
|
.updateMessage(
|
||||||
message,
|
message,
|
||||||
message.copyWith(
|
message.copyWith(
|
||||||
linkPreviewData: linkPreviewData,
|
linkPreviewData:
|
||||||
|
linkPreviewData,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -246,10 +175,14 @@ class RoomChat extends HookConsumerWidget {
|
||||||
required bool isSentByMe,
|
required bool isSentByMe,
|
||||||
MessageGroupStatus? groupStatus,
|
MessageGroupStatus? groupStatus,
|
||||||
}) => FlyerChatImageMessage(
|
}) => FlyerChatImageMessage(
|
||||||
topWidget: TopWidget(message, headers: headers),
|
topWidget: TopWidget(
|
||||||
|
message,
|
||||||
|
headers: room.roomData.client.headers,
|
||||||
|
groupStatus: groupStatus,
|
||||||
|
),
|
||||||
message: message,
|
message: message,
|
||||||
index: index,
|
index: index,
|
||||||
headers: headers,
|
headers: room.roomData.client.headers,
|
||||||
),
|
),
|
||||||
fileMessageBuilder:
|
fileMessageBuilder:
|
||||||
(
|
(
|
||||||
|
|
@ -263,7 +196,11 @@ class RoomChat extends HookConsumerWidget {
|
||||||
context: context,
|
context: context,
|
||||||
), // TODO: Download
|
), // TODO: Download
|
||||||
child: FlyerChatFileMessage(
|
child: FlyerChatFileMessage(
|
||||||
topWidget: TopWidget(message, headers: headers),
|
topWidget: TopWidget(
|
||||||
|
message,
|
||||||
|
headers: room.roomData.client.headers,
|
||||||
|
groupStatus: groupStatus,
|
||||||
|
),
|
||||||
message: message,
|
message: message,
|
||||||
index: index,
|
index: index,
|
||||||
),
|
),
|
||||||
|
|
@ -279,6 +216,20 @@ class RoomChat extends HookConsumerWidget {
|
||||||
message: message,
|
message: message,
|
||||||
index: index,
|
index: index,
|
||||||
),
|
),
|
||||||
|
unsupportedMessageBuilder:
|
||||||
|
(
|
||||||
|
_,
|
||||||
|
message,
|
||||||
|
index, {
|
||||||
|
required bool isSentByMe,
|
||||||
|
MessageGroupStatus? groupStatus,
|
||||||
|
}) => kDebugMode
|
||||||
|
? Text(
|
||||||
|
"${message.authorId} sent ${message.metadata?["eventType"]}",
|
||||||
|
style: theme.textTheme.labelSmall
|
||||||
|
?.copyWith(color: Colors.grey),
|
||||||
|
)
|
||||||
|
: SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
onMessageSend: (message) {
|
onMessageSend: (message) {
|
||||||
ref
|
ref
|
||||||
|
|
@ -292,6 +243,11 @@ class RoomChat extends HookConsumerWidget {
|
||||||
chatController: controller,
|
chatController: controller,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (memberListOpened.value == true) MemberList(room.roomData),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:nexus/controllers/current_room_controller.dart";
|
import "package:nexus/controllers/current_room_controller.dart";
|
||||||
import "package:nexus/controllers/spaces_controller.dart";
|
import "package:nexus/controllers/spaces_controller.dart";
|
||||||
import "package:nexus/helpers/extension_helper.dart";
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
import "package:nexus/widgets/room_avatar.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
class Sidebar extends HookConsumerWidget {
|
class Sidebar extends HookConsumerWidget {
|
||||||
const Sidebar({super.key});
|
const Sidebar({super.key});
|
||||||
|
|
@ -38,7 +38,12 @@ class Sidebar extends HookConsumerWidget {
|
||||||
destinations: spaces
|
destinations: spaces
|
||||||
.map(
|
.map(
|
||||||
(space) => NavigationRailDestination(
|
(space) => NavigationRailDestination(
|
||||||
icon: RoomAvatar(space.avatar, space.title),
|
icon: AvatarOrHash(
|
||||||
|
space.avatar,
|
||||||
|
fallback: space.icon,
|
||||||
|
space.title,
|
||||||
|
headers: space.client.headers,
|
||||||
|
),
|
||||||
label: Text(space.title),
|
label: Text(space.title),
|
||||||
padding: EdgeInsets.only(top: 4),
|
padding: EdgeInsets.only(top: 4),
|
||||||
),
|
),
|
||||||
|
|
@ -58,7 +63,12 @@ class Sidebar extends HookConsumerWidget {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
RoomAvatar(space.avatar, space.title),
|
AvatarOrHash(
|
||||||
|
space.avatar,
|
||||||
|
fallback: space.icon,
|
||||||
|
space.title,
|
||||||
|
headers: space.client.headers,
|
||||||
|
),
|
||||||
SizedBox(width: 12),
|
SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -80,12 +90,13 @@ class Sidebar extends HookConsumerWidget {
|
||||||
destinations: space.children
|
destinations: space.children
|
||||||
.map(
|
.map(
|
||||||
(room) => NavigationRailDestination(
|
(room) => NavigationRailDestination(
|
||||||
icon: RoomAvatar(
|
icon: AvatarOrHash(
|
||||||
room.avatar,
|
room.avatar,
|
||||||
room.title,
|
room.title,
|
||||||
fallback: selectedSpace.value == 1
|
fallback: selectedSpace.value == 1
|
||||||
? null
|
? null
|
||||||
: Icon(Icons.numbers),
|
: Icon(Icons.numbers),
|
||||||
|
headers: space.client.headers,
|
||||||
),
|
),
|
||||||
label: Text(room.title),
|
label: Text(room.title),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,13 @@ import "package:nexus/helpers/extension_helper.dart";
|
||||||
class TopWidget extends ConsumerWidget {
|
class TopWidget extends ConsumerWidget {
|
||||||
final Message message;
|
final Message message;
|
||||||
final Map<String, String> headers;
|
final Map<String, String> headers;
|
||||||
const TopWidget(this.message, {required this.headers, super.key});
|
final MessageGroupStatus? groupStatus;
|
||||||
|
const TopWidget(
|
||||||
|
this.message, {
|
||||||
|
required this.headers,
|
||||||
|
required this.groupStatus,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) => Column(
|
Widget build(BuildContext context, WidgetRef ref) => Column(
|
||||||
|
|
@ -32,16 +38,14 @@ class TopWidget extends ConsumerWidget {
|
||||||
(message as TextMessage).text.length - 20,
|
(message as TextMessage).text.length - 20,
|
||||||
replyMessage.text.length,
|
replyMessage.text.length,
|
||||||
),
|
),
|
||||||
40,
|
5,
|
||||||
),
|
),
|
||||||
replyMessage.text.length,
|
replyMessage.text.length,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: replyMessage.text;
|
: replyMessage.text;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => showAboutDialog(
|
onTap: () => showAboutDialog(context: context),
|
||||||
context: context,
|
|
||||||
), // TODO: Scroll to message
|
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
|
|
@ -62,11 +66,14 @@ class TopWidget extends ConsumerWidget {
|
||||||
headers: headers,
|
headers: headers,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
Text(
|
Flexible(
|
||||||
|
child: Text(
|
||||||
replyMessage.metadata?["displayName"] ??
|
replyMessage.metadata?["displayName"] ??
|
||||||
replyMessage.authorId,
|
replyMessage.authorId,
|
||||||
style: Theme.of(context).textTheme.labelMedium
|
style: Theme.of(context).textTheme.labelMedium
|
||||||
?.copyWith(fontWeight: FontWeight.bold),
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -85,6 +92,7 @@ class TopWidget extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
SizedBox(height: 12),
|
SizedBox(height: 12),
|
||||||
],
|
],
|
||||||
|
if (groupStatus?.isFirst != false)
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
showAboutDialog(context: context), // TODO: Show user profile
|
showAboutDialog(context: context), // TODO: Show user profile
|
||||||
|
|
@ -93,11 +101,14 @@ class TopWidget extends ConsumerWidget {
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Avatar(userId: message.authorId, headers: headers),
|
Avatar(userId: message.authorId, headers: headers),
|
||||||
Text(
|
Flexible(
|
||||||
|
child: Text(
|
||||||
message.metadata?["displayName"] ?? message.authorId,
|
message.metadata?["displayName"] ?? message.authorId,
|
||||||
style: Theme.of(
|
overflow: TextOverflow.ellipsis,
|
||||||
context,
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue