Working images

This commit is contained in:
Henry Hiles 2026-01-30 16:50:25 +01:00
commit 2372ecd141
No known key found for this signature in database
20 changed files with 388 additions and 375 deletions

View file

@ -1,17 +0,0 @@
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);
}

View file

@ -17,6 +17,7 @@ class MembersController extends AsyncNotifier<IList<Event>> {
), ),
) )
.nonNulls .nonNulls
.where((member) => member.content["membership"] == "join")
.toIList(); .toIList();
static final provider = AsyncNotifierProvider.family static final provider = AsyncNotifierProvider.family

View file

@ -1,41 +1,51 @@
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/profile_controller.dart"; import "package:nexus/controllers/profile_controller.dart";
import "package:nexus/models/event.dart"; import "package:nexus/helpers/extensions/mxc_to_https.dart";
import "package:nexus/models/message_config.dart";
import "package:nexus/models/requests/get_event_request.dart"; import "package:nexus/models/requests/get_event_request.dart";
import "package:nexus/models/requests/get_related_events_request.dart"; import "package:nexus/models/requests/get_related_events_request.dart";
extension EventToMessage on Event { class MessageController extends AsyncNotifier<Message?> {
Future<Message?> toMessage( final MessageConfig config;
Ref ref, { MessageController(this.config);
bool mustBeText = false,
bool includeEdits = false, @override
}) async { Future<Message?> build() async {
if (relationType == "m.replace" && !includeEdits) return null; if (config.event.relationType == "m.replace" && !config.includeEdits) {
return null;
}
final client = ref.watch(ClientController.provider.notifier); final client = ref.watch(ClientController.provider.notifier);
final newEvents = await client.getRelatedEvents( final newEvents = await client.getRelatedEvents(
GetRelatedEventsRequest( GetRelatedEventsRequest(
roomId: roomId, roomId: config.event.roomId,
eventId: eventId, eventId: config.event.eventId,
relationType: "m.replace", relationType: "m.replace",
), ),
); );
final event = newEvents?.lastOrNull ?? this; if (!ref.mounted) return null;
final event = newEvents?.lastOrNull ?? config.event;
final replyId = this.content["m.relates_to"]?["m.in_reply_to"]?["event_id"]; final replyId =
config.event.content["m.relates_to"]?["m.in_reply_to"]?["event_id"];
final replyEvent = replyId == null final replyEvent = replyId == null
? null ? null
: await client.getEvent( : await client.getEvent(
GetEventRequest(roomId: roomId, eventId: replyId), GetEventRequest(roomId: config.event.roomId, eventId: replyId),
); );
final author = await ref.watch( if (!ref.mounted) return null;
final author = await ref.read(
ProfileController.provider(event.authorId).future, ProfileController.provider(event.authorId).future,
); );
final content = (decrypted ?? this.content); if (!ref.mounted) return null;
final type = (decryptedType ?? this.type);
final content = (event.decrypted ?? event.content);
final type = (config.event.decryptedType ?? config.event.type);
final newContent = content["m.new_content"] as Map?; final newContent = content["m.new_content"] as Map?;
final metadata = { final metadata = {
"timelineId": event.timelineRowId, "timelineId": event.timelineRowId,
@ -45,18 +55,25 @@ extension EventToMessage on Event {
content["formatted_body"] ?? content["formatted_body"] ??
content["body"] ?? content["body"] ??
"", "",
"reply": await replyEvent?.toMessage(ref, mustBeText: true), if (replyEvent != null)
"reply": await ref.read(
MessageController.provider(
MessageConfig(event: replyEvent, mustBeText: true),
).future,
),
"body": newContent?["body"] ?? content["body"], "body": newContent?["body"] ?? content["body"],
"eventType": type, "eventType": type,
"avatarUrl": author.avatarUrl, "avatarUrl": author.avatarUrl,
"displayName": author.displayName ?? authorId, "displayName": author.displayName ?? event.authorId,
"txnId": transactionId, "txnId": config.event.transactionId,
}; };
if (!ref.mounted) return null;
final editedAt = event.relationType == "m.replace" ? event.timestamp : null; final editedAt = event.relationType == "m.replace" ? event.timestamp : null;
if ((event.redactedBy != null && !mustBeText) || if ((event.redactedBy != null && !config.mustBeText) ||
(!includeEdits && (relationType == "m.replace"))) { (!config.includeEdits && (config.event.relationType == "m.replace"))) {
return null; return null;
} }
@ -69,18 +86,24 @@ extension EventToMessage on Event {
final asText = final asText =
Message.text( Message.text(
metadata: metadata, metadata: metadata,
id: eventId, id: config.event.eventId,
authorId: authorId, authorId: event.authorId,
text: redactedBy == null text: config.event.redactedBy == null
? content["body"] ?? "" ? content["body"] ?? ""
: "This message has been deleted...", : "This message has been deleted...",
replyToMessageId: replyId, replyToMessageId: replyId,
deliveredAt: timestamp, deliveredAt: config.event.timestamp,
editedAt: editedAt, editedAt: editedAt,
) )
as TextMessage; as TextMessage;
if (mustBeText) return asText; if (config.mustBeText) return asText;
final homeserver = ref.read(ClientStateController.provider)?.homeserverUrl;
final source = homeserver == null || content["url"] == null
? "null"
: Uri.parse(content["url"]).mxcToHttps(homeserver).toString();
return switch (type) { return switch (type) {
"m.room.encrypted" => asText.copyWith( "m.room.encrypted" => asText.copyWith(
text: "Unable to decrypt message.", text: "Unable to decrypt message.",
@ -98,42 +121,42 @@ extension EventToMessage on Event {
// ), // ),
("m.sticker" || "m.room.message") => switch (content["msgtype"]) { ("m.sticker" || "m.room.message") => switch (content["msgtype"]) {
("m.sticker" || "m.image") => Message.image( ("m.sticker" || "m.image") => Message.image(
id: eventId, id: config.event.eventId,
metadata: metadata, metadata: metadata,
authorId: authorId, authorId: event.authorId,
text: event.localContent?.sanitizedHtml, text: event.localContent?.sanitizedHtml,
source: "(await getAttachmentUri()).toString()", // TODO source: source,
replyToMessageId: replyId, replyToMessageId: replyId,
deliveredAt: timestamp, deliveredAt: config.event.timestamp,
blurhash: (content["info"] as Map?)?["xyz.amorgan.blurhash"], blurhash: (content["info"] as Map?)?["xyz.amorgan.blurhash"],
), ),
"m.audio" => Message.audio( "m.audio" => Message.audio(
id: eventId, id: config.event.eventId,
metadata: metadata, metadata: metadata,
authorId: authorId, authorId: event.authorId,
text: content["body"], text: content["body"],
replyToMessageId: replyId, replyToMessageId: replyId,
source: "(await event.getAttachmentUri()).toString()", // TODO source: source,
deliveredAt: timestamp, deliveredAt: config.event.timestamp,
// TODO: See if we can figure out duration // TODO: See if we can figure out duration
duration: Duration(hours: 1), duration: Duration(hours: 1),
), ),
"m.file" => Message.file( "m.file" => Message.file(
name: content["filename"].toString(), name: content["filename"].toString(),
metadata: metadata, metadata: metadata,
id: eventId, id: config.event.eventId,
authorId: authorId, authorId: event.authorId,
source: "(await event.getAttachmentUri()).toString()", // TODO source: source,
replyToMessageId: replyId, replyToMessageId: replyId,
deliveredAt: timestamp, deliveredAt: config.event.timestamp,
), ),
_ => asText, _ => asText,
}, },
"m.room.member" => Message.system( "m.room.member" => Message.system(
metadata: metadata, metadata: metadata,
id: eventId, id: config.event.eventId,
authorId: authorId, authorId: event.authorId,
deliveredAt: timestamp, deliveredAt: config.event.timestamp,
text: text:
"${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) { "${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) {
"invite" => "was invited to", "invite" => "was invited to",
@ -151,11 +174,16 @@ extension EventToMessage on Event {
// ignore: dead_code // ignore: dead_code
? Message.unsupported( ? Message.unsupported(
metadata: metadata, metadata: metadata,
id: eventId, id: config.event.eventId,
authorId: authorId, authorId: event.authorId,
replyToMessageId: replyId, replyToMessageId: replyId,
) )
: null, : null,
}; };
} }
static final provider = AsyncNotifierProvider.family
.autoDispose<MessageController, Message?, MessageConfig>(
MessageController.new,
);
} }

View file

@ -0,0 +1,25 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/message_controller.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/message_config.dart";
class MessagesController extends AsyncNotifier<IList<Message>> {
final IList<Event> events;
MessagesController(this.events);
@override
Future<IList<Message>> build() async => (await Future.wait(
events.map(
(event) => ref.watch(
MessageController.provider(MessageConfig(event: event)).future,
),
),
)).nonNulls.toIList();
static final provider = AsyncNotifierProvider.family
.autoDispose<MessagesController, IList<Message>, IList<Event>>(
MessagesController.new,
);
}

View file

@ -3,13 +3,14 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_chat_core/flutter_chat_core.dart"; 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:fluttertagger/fluttertagger.dart" as tagger; import "package:fluttertagger/fluttertagger.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/message_controller.dart";
import "package:nexus/controllers/messages_controller.dart";
import "package:nexus/controllers/new_events_controller.dart"; import "package:nexus/controllers/new_events_controller.dart";
import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/helpers/extensions/event_to_message.dart"; import "package:nexus/models/message_config.dart";
import "package:nexus/helpers/extensions/list_to_messages.dart";
import "package:nexus/models/requests/get_room_state_request.dart"; import "package:nexus/models/requests/get_room_state_request.dart";
import "package:nexus/models/requests/paginate_request.dart"; import "package:nexus/models/requests/paginate_request.dart";
import "package:nexus/models/requests/redact_event_request.dart"; import "package:nexus/models/requests/redact_event_request.dart";
@ -27,15 +28,19 @@ class RoomChatController extends AsyncNotifier<ChatController> {
final room = ref.read(SelectedRoomController.provider); final room = ref.read(SelectedRoomController.provider);
if (room == null) return InMemoryChatController(); if (room == null) return InMemoryChatController();
final messages = await room.timeline final messages = await ref.watch(
.map( MessagesController.provider(
(timelineRowTuple) => room.events.firstWhereOrNull( room.timeline
(event) => event.rowId == timelineRowTuple.eventRowId, .map(
), (timelineRowTuple) => room.events.firstWhereOrNull(
) (event) => event.rowId == timelineRowTuple.eventRowId,
.nonNulls ),
.toMessages(ref); )
final controller = InMemoryChatController(messages: messages); .nonNulls
.toIList(),
).future,
);
final controller = InMemoryChatController(messages: messages.toList());
ref.onDispose( ref.onDispose(
ref.listen(NewEventsController.provider(roomId), (_, next) async { ref.listen(NewEventsController.provider(roomId), (_, next) async {
@ -49,7 +54,11 @@ class RoomChatController extends AsyncNotifier<ChatController> {
await controller.removeMessage(message); await controller.removeMessage(message);
} else { } else {
final message = await event.toMessage(ref, includeEdits: true); final message = await ref.read(
MessageController.provider(
MessageConfig(event: event, includeEdits: true),
).future,
);
if (event.relationType == "m.replace") { if (event.relationType == "m.replace") {
final controller = await future; final controller = await future;
final oldMessage = controller.messages.firstWhereOrNull( final oldMessage = controller.messages.firstWhereOrNull(
@ -180,7 +189,9 @@ class RoomChatController extends AsyncNotifier<ChatController> {
const ISet.empty(), const ISet.empty(),
); );
final messages = await response.events.reversed.toMessages(ref); final messages = await ref.watch(
MessagesController.provider(response.events.reversed).future,
);
await controller.insertAllMessages( await controller.insertAllMessages(
messages messages
.where( .where(
@ -198,7 +209,7 @@ class RoomChatController extends AsyncNotifier<ChatController> {
Future<void> send( Future<void> send(
String message, { String message, {
required Iterable<tagger.Tag> tags, required Iterable<Tag> tags,
required RelationType relationType, required RelationType relationType,
Message? relation, Message? relation,
}) async { }) async {

View file

@ -1,22 +0,0 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:matrix/matrix.dart";
import "package:nexus/controllers/client_controller.dart";
import "package:nexus/models/image_data.dart";
class ThumbnailController extends AsyncNotifier<String?> {
ThumbnailController(this.data);
final ImageData data;
@override
Future<String?> build({String? from}) async {
final client = await ref.watch(ClientController.provider.future);
final uri = await Uri.tryParse(data.uri)?.getDownloadUri(client);
return uri.toString();
}
static final provider = AsyncNotifierProvider.family
.autoDispose<ThumbnailController, String?, ImageData>(
ThumbnailController.new,
);
}

View file

@ -1,10 +0,0 @@
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/helpers/extensions/event_to_message.dart";
import "package:nexus/models/event.dart";
extension ListToMessages on Iterable<Event> {
Future<List<Message>> toMessages(Ref ref) async => (await Future.wait(
map((event) => event.toMessage(ref)),
)).nonNulls.toList();
}

View file

@ -0,0 +1,4 @@
extension MxcToHttps on Uri {
Uri mxcToHttps(String homeserver) =>
Uri.parse("${homeserver}_matrix/client/v1/media/download/$host$path");
}

View file

@ -9,6 +9,7 @@ abstract class ClientState with _$ClientState {
required bool isLoggedIn, required bool isLoggedIn,
required bool isVerified, required bool isVerified,
required String? userId, required String? userId,
required String? homeserverUrl,
}) = _ClientState; }) = _ClientState;
factory ClientState.fromJson(Map<String, Object?> json) => factory ClientState.fromJson(Map<String, Object?> json) =>

View file

@ -0,0 +1,16 @@
import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/models/event.dart";
part "message_config.freezed.dart";
part "message_config.g.dart";
@freezed
abstract class MessageConfig with _$MessageConfig {
const factory MessageConfig({
@Default(false) bool mustBeText,
@Default(false) bool includeEdits,
required Event event,
}) = _MessageConfig;
factory MessageConfig.fromJson(Map<String, Object?> json) =>
_$MessageConfigFromJson(json);
}

View file

@ -1,14 +1,19 @@
import "package:color_hash/color_hash.dart"; import "package:color_hash/color_hash.dart";
import "package:cross_cache/cross_cache.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/cross_cache_controller.dart";
import "package:nexus/helpers/extensions/get_headers.dart";
import "package:nexus/helpers/extensions/mxc_to_https.dart";
class AvatarOrHash extends StatelessWidget { class AvatarOrHash extends ConsumerWidget {
final Uri? avatar; final Uri? avatar;
final String title; final String title;
final Widget? fallback; final Widget? fallback;
final bool hasBadge; final bool hasBadge;
final int badgeNumber; final int badgeNumber;
final double height; final double height;
final Map<String, String> headers;
const AvatarOrHash( const AvatarOrHash(
this.avatar, this.avatar,
this.title, { this.title, {
@ -16,15 +21,14 @@ class AvatarOrHash extends StatelessWidget {
this.badgeNumber = 0, this.badgeNumber = 0,
this.hasBadge = false, this.hasBadge = false,
this.height = 24, this.height = 24,
required this.headers,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final box = ColoredBox( final box = ColoredBox(
color: ColorHash(title).color, color: ColorHash(title).color,
child: Center(child: Text(title[0])), child: Center(child: Text(title.isEmpty ? "" : title[0])),
); );
return SizedBox( return SizedBox(
width: height, width: height,
@ -42,9 +46,21 @@ class AvatarOrHash extends StatelessWidget {
height: height, height: height,
child: avatar == null child: avatar == null
? fallback ?? box ? fallback ?? box
: Image.network( : Image(
avatar.toString(), image: CachedNetworkImage(
headers: headers, avatar!
.mxcToHttps(
ref.watch(
ClientStateController.provider.select(
(value) => value?.homeserverUrl,
),
) ??
"",
)
.toString(),
ref.watch(CrossCacheController.provider),
headers: ref.headers,
),
fit: BoxFit.contain, fit: BoxFit.contain,
errorBuilder: (_, _, _) => box, errorBuilder: (_, _, _) => box,
), ),

View file

@ -2,7 +2,10 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart"; import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart";
import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/helpers/extensions/get_headers.dart";
import "package:nexus/helpers/extensions/link_to_mention.dart"; import "package:nexus/helpers/extensions/link_to_mention.dart";
import "package:nexus/helpers/extensions/mxc_to_https.dart";
import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/helpers/launch_helper.dart";
import "package:nexus/widgets/chat_page/html/mention_chip.dart"; import "package:nexus/widgets/chat_page/html/mention_chip.dart";
import "package:nexus/widgets/chat_page/html/spoiler_text.dart"; import "package:nexus/widgets/chat_page/html/spoiler_text.dart";
@ -40,53 +43,36 @@ class Html extends ConsumerWidget {
? null ? null
: InlineCustomWidget(child: MentionChip(element.text)), : InlineCustomWidget(child: MentionChip(element.text)),
// "img" => TODO: Img support "img" =>
// element.attributes["src"] == null element.attributes["src"] == null
// ? null ? null
// : Consumer( : InlineCustomWidget(
// builder: (_, ref, _) => ref child: Image.network(
// .watch( Uri.parse(element.attributes["src"]!)
// ThumbnailController.provider( .mxcToHttps(
// ImageData( ref.watch(
// uri: element.attributes["src"]!, ClientStateController.provider.select(
// height: height, (value) => value?.homeserverUrl,
// width: width, ),
// ), ) ??
// ), "",
// ) )
// .when( .toString(),
// data: (uri) { headers: ref.headers,
// if (uri == null) return SizedBox.shrink(); errorBuilder: (_, error, _) => Text(
"Image Failed to Load",
// return InlineCustomWidget( style: TextStyle(
// child: Image.network( color: Theme.of(context).colorScheme.error,
// uri, ),
// headers: client.headers, ),
// errorBuilder: (_, error, _) => Text( height: height.toDouble(),
// "Image Failed to Load", width: width?.toDouble(),
// style: TextStyle( loadingBuilder: (_, child, loadingProgress) =>
// color: Theme.of(context).colorScheme.error, loadingProgress == null
// ), ? child
// ), : CircularProgressIndicator(),
// height: height.toDouble(), ),
// width: width?.toDouble(), ),
// loadingBuilder: (_, child, loadingProgress) =>
// loadingProgress == null
// ? child
// : CircularProgressIndicator(),
// ),
// );
// },
// error: ErrorDialog.new,
// loading: () => InlineCustomWidget(
// child: SizedBox(
// width: width?.toDouble(),
// height: height.toDouble(),
// child: CircularProgressIndicator(),
// ),
// ),
// ),
// ),
("del" || ("del" ||
"h1" || "h1" ||
"h2" || "h2" ||

View file

@ -19,7 +19,7 @@ class MentionChip extends StatelessWidget {
context: context, context: context,
builder: (_) => Dialog( builder: (_) => Dialog(
child: Text("TODO: Open room or join room dialog, or user popover"), child: Text("TODO: Open room or join room dialog, or user popover"),
), // TODO ),
), ),
); );
} }

View file

@ -3,6 +3,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/members_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
class MemberList extends ConsumerWidget { class MemberList extends ConsumerWidget {
final Room room; final Room room;
@ -14,58 +15,44 @@ class MemberList extends ConsumerWidget {
child: ref child: ref
.watch(MembersController.provider(room)) .watch(MembersController.provider(room))
.betterWhen( .betterWhen(
data: (members) { data: (members) => ListView(
final joined = members.where( children: [
(membership) => AppBar(
membership.content["membership"] == scrolledUnderElevation: 0,
"join", // TODO: Show invites seperately leading: Icon(Icons.people),
); title: Text("Members (${members.length})"),
return ListView( actionsPadding: EdgeInsets.only(right: 4),
children: [ actions: [
AppBar( if (Scaffold.of(context).hasEndDrawer)
scrolledUnderElevation: 0, IconButton(
leading: Icon(Icons.people), onPressed: Scaffold.of(context).closeEndDrawer,
title: Text("Members (${joined.length})"), icon: Icon(Icons.close),
actionsPadding: EdgeInsets.only(right: 4),
actions: [
if (Scaffold.of(context).hasEndDrawer)
IconButton(
onPressed: Scaffold.of(context).closeEndDrawer,
icon: Icon(Icons.close),
),
],
),
...joined.map(
(member) => ListTile(
onTap: () => showDialog(
context: context,
builder: (context) =>
Dialog(child: Text("TODO: Open member popover")),
),
// leading: AvatarOrHash( TODO
// 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,
),
subtitle: Text(
member.authorId,
overflow: TextOverflow.ellipsis,
), ),
],
),
...members.map(
(member) => ListTile(
onTap: () => showDialog(
context: context,
builder: (context) =>
Dialog(child: Text("TODO: Open member popover")),
),
leading: AvatarOrHash(
Uri.tryParse(member.content["avatar_url"] ?? ""),
member.content["displayname"].toString(),
),
title: Text(
member.content["displayname"].toString(),
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
member.authorId,
overflow: TextOverflow.ellipsis,
), ),
), ),
], ),
); ],
}, ),
), ),
); );
} }

View file

@ -4,6 +4,7 @@ import "package:nexus/controllers/members_controller.dart";
import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/loading.dart";
class MentionOverlay extends ConsumerWidget { class MentionOverlay extends ConsumerWidget {
@ -54,18 +55,12 @@ class MentionOverlay extends ConsumerWidget {
)) ))
.map( .map(
(member) => ListTile( (member) => ListTile(
// leading: AvatarOrHash( TODO: Images leading: AvatarOrHash(
// ref Uri.tryParse(
// .watch( member.content["avatar_url"] ?? "",
// AvatarController.provider( ),
// member.content["avatar_url"] member.content["displayname"] ?? "",
// .toString(), ),
// ),
// )
// .whenOrNull(data: (data) => data),
// member.content["displayname"].toString(),
// headers: room.client.headers,
// ),
title: Text( title: Text(
member.content["displayname"] as String? ?? member.content["displayname"] as String? ??
member.authorId, member.authorId,
@ -93,12 +88,11 @@ class MentionOverlay extends ConsumerWidget {
)) ))
.map( .map(
(room) => ListTile( (room) => ListTile(
// leading: AvatarOrHash( TODO: Images leading: AvatarOrHash(
// room.avatar, room.metadata?.avatar,
// room.title, room.metadata?.name ?? "Unnamed Room",
// fallback: Icon(Icons.numbers), fallback: Icon(Icons.numbers),
// headers: room.roomData.client.headers, ),
// ),
title: Text(room.metadata?.name ?? "Unnamed Room"), title: Text(room.metadata?.name ?? "Unnamed Room"),
subtitle: room.metadata?.topic == null subtitle: room.metadata?.topic == null
? null ? null

View file

@ -24,14 +24,12 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
@override @override
Widget build(BuildContext context) => Appbar( Widget build(BuildContext context) => Appbar(
leading: isDesktop leading: isDesktop
? null ? AvatarOrHash(
// AvatarOrHash( TODO: Images room.metadata?.avatar,
// room.avatar, room.metadata?.name ?? "Unnamed Rooms",
// room.title, height: 24,
// height: 24, fallback: Icon(Icons.numbers),
// fallback: Icon(Icons.numbers), )
// headers: room.roomData.client.headers,
// )
: DrawerButton(onPressed: () => onOpenDrawer(context)), : DrawerButton(onPressed: () => onOpenDrawer(context)),
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
title: Column( title: Column(

View file

@ -488,9 +488,7 @@ class RoomChat extends HookConsumerWidget {
onTap: () => showDialog( onTap: () => showDialog(
context: context, context: context,
builder: (_) => Dialog( builder: (_) => Dialog(
child: Text( child: Text("TODO: Download Attachments"),
"TODO: Download Attachments", // TODO
),
), ),
), ),
child: FlyerChatFileMessage( child: FlyerChatFileMessage(

View file

@ -61,10 +61,9 @@ class Sidebar extends HookConsumerWidget {
.map( .map(
(space) => NavigationRailDestination( (space) => NavigationRailDestination(
icon: AvatarOrHash( icon: AvatarOrHash(
null, // TODO: Url space.room?.metadata?.avatar,
fallback: space.icon == null ? null : Icon(space.icon), fallback: space.icon == null ? null : Icon(space.icon),
space.title, space.title,
headers: {}, // TODO
hasBadge: space.children.any( hasBadge: space.children.any(
(room) => room.metadata?.unreadMessages != 0, (room) => room.metadata?.unreadMessages != 0,
), ),
@ -177,15 +176,12 @@ class Sidebar extends HookConsumerWidget {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
appBar: AppBar( appBar: AppBar(
leading: AvatarOrHash( leading: AvatarOrHash(
null, selectedSpace.room?.metadata?.avatar,
// space.avatar, TODO
fallback: selectedSpace.icon == null fallback: selectedSpace.icon == null
? null ? null
: Icon(selectedSpace.icon), : Icon(selectedSpace.icon),
selectedSpace.title, selectedSpace.title,
headers: {},
// space.client.headers, TODO
), ),
title: Text( title: Text(
selectedSpace.title, selectedSpace.title,
@ -210,15 +206,13 @@ class Sidebar extends HookConsumerWidget {
(room) => NavigationRailDestination( (room) => NavigationRailDestination(
label: Text(room.metadata?.name ?? "Unnamed Room"), label: Text(room.metadata?.name ?? "Unnamed Room"),
icon: AvatarOrHash( icon: AvatarOrHash(
null, room.metadata?.avatar,
hasBadge: room.metadata?.unreadMessages != 0, hasBadge: room.metadata?.unreadMessages != 0,
badgeNumber: room.metadata?.unreadNotifications ?? 0, badgeNumber: room.metadata?.unreadNotifications ?? 0,
// room.avatar, TODO
room.metadata?.name ?? "Unnamed Room", room.metadata?.name ?? "Unnamed Room",
fallback: selectedSpaceId == "dms" fallback: selectedSpaceId == "dms"
? null ? null
: Icon(Icons.numbers), : Icon(Icons.numbers),
headers: {},
// space.client.headers, // space.client.headers,
), ),
), ),

View file

@ -1,8 +1,8 @@
import "dart:math"; import "dart:math";
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";
import "package:flutter_chat_ui/flutter_chat_ui.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/chat_page/html/quoted.dart"; import "package:nexus/widgets/chat_page/html/quoted.dart";
class TopWidget extends ConsumerWidget { class TopWidget extends ConsumerWidget {
@ -60,11 +60,11 @@ class TopWidget extends ConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
spacing: 8, spacing: 8,
children: [ children: [
// Avatar( TODO: images AvatarOrHash(
// userId: replyMessage.authorId, Uri.tryParse(replyMessage.metadata?["avatarUrl"] ?? ""),
// headers: headers, replyMessage.metadata?["displayName"] ?? "",
// size: 16, height: 16,
// ), ),
Flexible( Flexible(
child: Text( child: Text(
replyMessage.metadata?["displayName"] ?? replyMessage.metadata?["displayName"] ??
@ -102,7 +102,10 @@ class TopWidget extends ConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
spacing: 8, spacing: 8,
children: [ children: [
// Avatar(userId: message.authorId, headers: headers), TODO: images AvatarOrHash(
Uri.parse(message.metadata?["avatarUrl"] ?? ""),
message.metadata?["displayName"] ?? "",
),
Flexible( Flexible(
child: Text( child: Text(
message.metadata?["displayName"] ?? message.authorId, message.metadata?["displayName"] ?? message.authorId,