Working images

This commit is contained in:
Henry Hiles 2026-01-30 16:50:25 +01:00
commit 099725063f
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
.where((member) => member.content["membership"] == "join")
.toIList();
static final provider = AsyncNotifierProvider.family

View file

@ -1,41 +1,51 @@
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.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/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_related_events_request.dart";
extension EventToMessage on Event {
Future<Message?> toMessage(
Ref ref, {
bool mustBeText = false,
bool includeEdits = false,
}) async {
if (relationType == "m.replace" && !includeEdits) return null;
class MessageController extends AsyncNotifier<Message?> {
final MessageConfig config;
MessageController(this.config);
@override
Future<Message?> build() async {
if (config.event.relationType == "m.replace" && !config.includeEdits) {
return null;
}
final client = ref.watch(ClientController.provider.notifier);
final newEvents = await client.getRelatedEvents(
GetRelatedEventsRequest(
roomId: roomId,
eventId: eventId,
roomId: config.event.roomId,
eventId: config.event.eventId,
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
? null
: 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,
);
final content = (decrypted ?? this.content);
final type = (decryptedType ?? this.type);
if (!ref.mounted) return null;
final content = (event.decrypted ?? event.content);
final type = (config.event.decryptedType ?? config.event.type);
final newContent = content["m.new_content"] as Map?;
final metadata = {
"timelineId": event.timelineRowId,
@ -45,18 +55,25 @@ extension EventToMessage on Event {
content["formatted_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"],
"eventType": type,
"avatarUrl": author.avatarUrl,
"displayName": author.displayName ?? authorId,
"txnId": transactionId,
"displayName": author.displayName ?? event.authorId,
"txnId": config.event.transactionId,
};
if (!ref.mounted) return null;
final editedAt = event.relationType == "m.replace" ? event.timestamp : null;
if ((event.redactedBy != null && !mustBeText) ||
(!includeEdits && (relationType == "m.replace"))) {
if ((event.redactedBy != null && !config.mustBeText) ||
(!config.includeEdits && (config.event.relationType == "m.replace"))) {
return null;
}
@ -69,18 +86,24 @@ extension EventToMessage on Event {
final asText =
Message.text(
metadata: metadata,
id: eventId,
authorId: authorId,
text: redactedBy == null
id: config.event.eventId,
authorId: event.authorId,
text: config.event.redactedBy == null
? content["body"] ?? ""
: "This message has been deleted...",
replyToMessageId: replyId,
deliveredAt: timestamp,
deliveredAt: config.event.timestamp,
editedAt: editedAt,
)
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) {
"m.room.encrypted" => asText.copyWith(
text: "Unable to decrypt message.",
@ -98,42 +121,42 @@ extension EventToMessage on Event {
// ),
("m.sticker" || "m.room.message") => switch (content["msgtype"]) {
("m.sticker" || "m.image") => Message.image(
id: eventId,
id: config.event.eventId,
metadata: metadata,
authorId: authorId,
authorId: event.authorId,
text: event.localContent?.sanitizedHtml,
source: "(await getAttachmentUri()).toString()", // TODO
source: source,
replyToMessageId: replyId,
deliveredAt: timestamp,
deliveredAt: config.event.timestamp,
blurhash: (content["info"] as Map?)?["xyz.amorgan.blurhash"],
),
"m.audio" => Message.audio(
id: eventId,
id: config.event.eventId,
metadata: metadata,
authorId: authorId,
authorId: event.authorId,
text: content["body"],
replyToMessageId: replyId,
source: "(await event.getAttachmentUri()).toString()", // TODO
deliveredAt: timestamp,
source: source,
deliveredAt: config.event.timestamp,
// TODO: See if we can figure out duration
duration: Duration(hours: 1),
),
"m.file" => Message.file(
name: content["filename"].toString(),
metadata: metadata,
id: eventId,
authorId: authorId,
source: "(await event.getAttachmentUri()).toString()", // TODO
id: config.event.eventId,
authorId: event.authorId,
source: source,
replyToMessageId: replyId,
deliveredAt: timestamp,
deliveredAt: config.event.timestamp,
),
_ => asText,
},
"m.room.member" => Message.system(
metadata: metadata,
id: eventId,
authorId: authorId,
deliveredAt: timestamp,
id: config.event.eventId,
authorId: event.authorId,
deliveredAt: config.event.timestamp,
text:
"${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) {
"invite" => "was invited to",
@ -151,11 +174,16 @@ extension EventToMessage on Event {
// ignore: dead_code
? Message.unsupported(
metadata: metadata,
id: eventId,
authorId: authorId,
id: config.event.eventId,
authorId: event.authorId,
replyToMessageId: replyId,
)
: 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" as chat;
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/message_controller.dart";
import "package:nexus/controllers/messages_controller.dart";
import "package:nexus/controllers/new_events_controller.dart";
import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/helpers/extensions/event_to_message.dart";
import "package:nexus/helpers/extensions/list_to_messages.dart";
import "package:nexus/models/message_config.dart";
import "package:nexus/models/requests/get_room_state_request.dart";
import "package:nexus/models/requests/paginate_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);
if (room == null) return InMemoryChatController();
final messages = await room.timeline
.map(
(timelineRowTuple) => room.events.firstWhereOrNull(
(event) => event.rowId == timelineRowTuple.eventRowId,
),
)
.nonNulls
.toMessages(ref);
final controller = InMemoryChatController(messages: messages);
final messages = await ref.watch(
MessagesController.provider(
room.timeline
.map(
(timelineRowTuple) => room.events.firstWhereOrNull(
(event) => event.rowId == timelineRowTuple.eventRowId,
),
)
.nonNulls
.toIList(),
).future,
);
final controller = InMemoryChatController(messages: messages.toList());
ref.onDispose(
ref.listen(NewEventsController.provider(roomId), (_, next) async {
@ -49,7 +54,11 @@ class RoomChatController extends AsyncNotifier<ChatController> {
await controller.removeMessage(message);
} 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") {
final controller = await future;
final oldMessage = controller.messages.firstWhereOrNull(
@ -180,7 +189,9 @@ class RoomChatController extends AsyncNotifier<ChatController> {
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(
messages
.where(
@ -198,7 +209,7 @@ class RoomChatController extends AsyncNotifier<ChatController> {
Future<void> send(
String message, {
required Iterable<tagger.Tag> tags,
required Iterable<Tag> tags,
required RelationType relationType,
Message? relation,
}) 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 isVerified,
required String? userId,
required String? homeserverUrl,
}) = _ClientState;
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:cross_cache/cross_cache.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 String title;
final Widget? fallback;
final bool hasBadge;
final int badgeNumber;
final double height;
final Map<String, String> headers;
const AvatarOrHash(
this.avatar,
this.title, {
@ -16,15 +21,14 @@ class AvatarOrHash extends StatelessWidget {
this.badgeNumber = 0,
this.hasBadge = false,
this.height = 24,
required this.headers,
super.key,
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final box = ColoredBox(
color: ColorHash(title).color,
child: Center(child: Text(title[0])),
child: Center(child: Text(title.isEmpty ? "" : title[0])),
);
return SizedBox(
width: height,
@ -42,9 +46,21 @@ class AvatarOrHash extends StatelessWidget {
height: height,
child: avatar == null
? fallback ?? box
: Image.network(
avatar.toString(),
headers: headers,
: Image(
image: CachedNetworkImage(
avatar!
.mxcToHttps(
ref.watch(
ClientStateController.provider.select(
(value) => value?.homeserverUrl,
),
) ??
"",
)
.toString(),
ref.watch(CrossCacheController.provider),
headers: ref.headers,
),
fit: BoxFit.contain,
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_riverpod/flutter_riverpod.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/mxc_to_https.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/spoiler_text.dart";
@ -40,53 +43,36 @@ class Html extends ConsumerWidget {
? null
: InlineCustomWidget(child: MentionChip(element.text)),
// "img" => TODO: Img support
// element.attributes["src"] == null
// ? null
// : Consumer(
// builder: (_, ref, _) => ref
// .watch(
// ThumbnailController.provider(
// ImageData(
// uri: element.attributes["src"]!,
// height: height,
// width: width,
// ),
// ),
// )
// .when(
// data: (uri) {
// if (uri == null) return SizedBox.shrink();
// return InlineCustomWidget(
// child: Image.network(
// uri,
// headers: client.headers,
// errorBuilder: (_, error, _) => Text(
// "Image Failed to Load",
// style: TextStyle(
// color: Theme.of(context).colorScheme.error,
// ),
// ),
// 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(),
// ),
// ),
// ),
// ),
"img" =>
element.attributes["src"] == null
? null
: InlineCustomWidget(
child: Image.network(
Uri.parse(element.attributes["src"]!)
.mxcToHttps(
ref.watch(
ClientStateController.provider.select(
(value) => value?.homeserverUrl,
),
) ??
"",
)
.toString(),
headers: ref.headers,
errorBuilder: (_, error, _) => Text(
"Image Failed to Load",
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
height: height.toDouble(),
width: width?.toDouble(),
loadingBuilder: (_, child, loadingProgress) =>
loadingProgress == null
? child
: CircularProgressIndicator(),
),
),
("del" ||
"h1" ||
"h2" ||

View file

@ -19,7 +19,7 @@ class MentionChip extends StatelessWidget {
context: context,
builder: (_) => Dialog(
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/helpers/extensions/better_when.dart";
import "package:nexus/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
class MemberList extends ConsumerWidget {
final Room room;
@ -14,58 +15,44 @@ class MemberList extends ConsumerWidget {
child: ref
.watch(MembersController.provider(room))
.betterWhen(
data: (members) {
final joined = members.where(
(membership) =>
membership.content["membership"] ==
"join", // TODO: Show invites seperately
);
return ListView(
children: [
AppBar(
scrolledUnderElevation: 0,
leading: Icon(Icons.people),
title: Text("Members (${joined.length})"),
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,
data: (members) => ListView(
children: [
AppBar(
scrolledUnderElevation: 0,
leading: Icon(Icons.people),
title: Text("Members (${members.length})"),
actionsPadding: EdgeInsets.only(right: 4),
actions: [
if (Scaffold.of(context).hasEndDrawer)
IconButton(
onPressed: Scaffold.of(context).closeEndDrawer,
icon: Icon(Icons.close),
),
],
),
...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/helpers/extensions/better_when.dart";
import "package:nexus/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/loading.dart";
class MentionOverlay extends ConsumerWidget {
@ -54,18 +55,12 @@ class MentionOverlay extends ConsumerWidget {
))
.map(
(member) => ListTile(
// leading: AvatarOrHash( TODO: Images
// ref
// .watch(
// AvatarController.provider(
// member.content["avatar_url"]
// .toString(),
// ),
// )
// .whenOrNull(data: (data) => data),
// member.content["displayname"].toString(),
// headers: room.client.headers,
// ),
leading: AvatarOrHash(
Uri.tryParse(
member.content["avatar_url"] ?? "",
),
member.content["displayname"] ?? "",
),
title: Text(
member.content["displayname"] as String? ??
member.authorId,
@ -93,12 +88,11 @@ class MentionOverlay extends ConsumerWidget {
))
.map(
(room) => ListTile(
// leading: AvatarOrHash( TODO: Images
// room.avatar,
// room.title,
// fallback: Icon(Icons.numbers),
// headers: room.roomData.client.headers,
// ),
leading: AvatarOrHash(
room.metadata?.avatar,
room.metadata?.name ?? "Unnamed Room",
fallback: Icon(Icons.numbers),
),
title: Text(room.metadata?.name ?? "Unnamed Room"),
subtitle: room.metadata?.topic == null
? null

View file

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

View file

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

View file

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

View file

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