add profile popovers
This commit is contained in:
parent
7ee165b300
commit
0b9ddbfbc8
11 changed files with 302 additions and 149 deletions
|
|
@ -50,7 +50,7 @@ class AvatarOrHash extends ConsumerWidget {
|
|||
ref.watch(CrossCacheController.provider),
|
||||
headers: ref.headers,
|
||||
),
|
||||
fit: BoxFit.contain,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, _, _) => box,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -21,133 +21,135 @@ class Html extends ConsumerWidget {
|
|||
const Html(this.html, {this.textStyle, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => HtmlWidget(
|
||||
html,
|
||||
textStyle: textStyle,
|
||||
customWidgetBuilder: (element) {
|
||||
if (element.attributes.keys.contains("data-mx-profile-fallback")) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
Widget build(BuildContext context, WidgetRef ref) => SelectionArea(
|
||||
child: HtmlWidget(
|
||||
html,
|
||||
textStyle: textStyle,
|
||||
customWidgetBuilder: (element) {
|
||||
if (element.attributes.keys.contains("data-mx-profile-fallback")) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (element.attributes.keys.contains("data-mx-spoiler")) {
|
||||
return InlineCustomWidget(child: SpoilerText(text: element.text));
|
||||
}
|
||||
if (element.attributes.keys.contains("data-mx-spoiler")) {
|
||||
return InlineCustomWidget(child: SpoilerText(text: element.text));
|
||||
}
|
||||
|
||||
final height = int.tryParse(element.attributes["height"] ?? "") ?? 300;
|
||||
final width = int.tryParse(element.attributes["width"] ?? "");
|
||||
final src = Uri.tryParse(element.attributes["src"] ?? "")
|
||||
?.mxcToHttps(
|
||||
ref.watch(
|
||||
ClientStateController.provider.select(
|
||||
(value) => value?.homeserverUrl,
|
||||
),
|
||||
) ??
|
||||
"",
|
||||
)
|
||||
.toString();
|
||||
final height = int.tryParse(element.attributes["height"] ?? "") ?? 300;
|
||||
final width = int.tryParse(element.attributes["width"] ?? "");
|
||||
final src = Uri.tryParse(element.attributes["src"] ?? "")
|
||||
?.mxcToHttps(
|
||||
ref.watch(
|
||||
ClientStateController.provider.select(
|
||||
(value) => value?.homeserverUrl,
|
||||
),
|
||||
) ??
|
||||
"",
|
||||
)
|
||||
.toString();
|
||||
|
||||
return switch (element.localName) {
|
||||
"code" =>
|
||||
element.parent?.localName == "pre"
|
||||
? element.outerHtml.contains("<br class=\"fake-break\">")
|
||||
? Html(
|
||||
"""<pre>${element.outerHtml.replaceAll("<br class=\"fake-break\">", "\n")}</pre>""",
|
||||
)
|
||||
: CodeBlock(
|
||||
element.text,
|
||||
lang: element.className.replaceAll("language-", ""),
|
||||
)
|
||||
: null,
|
||||
return switch (element.localName) {
|
||||
"code" =>
|
||||
element.parent?.localName == "pre"
|
||||
? element.outerHtml.contains("<br class=\"fake-break\">")
|
||||
? Html(
|
||||
"""<pre>${element.outerHtml.replaceAll("<br class=\"fake-break\">", "\n")}</pre>""",
|
||||
)
|
||||
: CodeBlock(
|
||||
element.text,
|
||||
lang: element.className.replaceAll("language-", ""),
|
||||
)
|
||||
: null,
|
||||
|
||||
"blockquote" => Quoted(Html(element.innerHtml)),
|
||||
"blockquote" => Quoted(Html(element.innerHtml)),
|
||||
|
||||
"a" =>
|
||||
element.attributes["href"]?.mention == null
|
||||
? null
|
||||
: InlineCustomWidget(child: MentionChip(element.text)),
|
||||
"a" =>
|
||||
element.attributes["href"]?.mention == null
|
||||
? null
|
||||
: InlineCustomWidget(child: MentionChip(element.text)),
|
||||
|
||||
"img" =>
|
||||
src == null
|
||||
? SizedBox.shrink()
|
||||
: InlineCustomWidget(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: ExpandableImage(
|
||||
src,
|
||||
child: Image(
|
||||
image: CachedNetworkImage(
|
||||
src,
|
||||
ref.watch(CrossCacheController.provider),
|
||||
headers: ref.headers,
|
||||
),
|
||||
errorBuilder: (_, error, _) => Text(
|
||||
"Image Failed to Load",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
"img" =>
|
||||
src == null
|
||||
? SizedBox.shrink()
|
||||
: InlineCustomWidget(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: ExpandableImage(
|
||||
src,
|
||||
child: Image(
|
||||
image: CachedNetworkImage(
|
||||
src,
|
||||
ref.watch(CrossCacheController.provider),
|
||||
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(),
|
||||
),
|
||||
height: height.toDouble(),
|
||||
width: width?.toDouble(),
|
||||
loadingBuilder: (_, child, loadingProgress) =>
|
||||
loadingProgress == null
|
||||
? child
|
||||
: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
("del" ||
|
||||
"h1" ||
|
||||
"h2" ||
|
||||
"h3" ||
|
||||
"h4" ||
|
||||
"h5" ||
|
||||
"h6" ||
|
||||
"p" ||
|
||||
"ul" ||
|
||||
"ol" ||
|
||||
"sup" ||
|
||||
"sub" ||
|
||||
"li" ||
|
||||
"b" ||
|
||||
"i" ||
|
||||
"u" ||
|
||||
"strong" ||
|
||||
"em" ||
|
||||
"s" ||
|
||||
"code" ||
|
||||
"hr" ||
|
||||
"br" ||
|
||||
"div" ||
|
||||
"table" ||
|
||||
"thead" ||
|
||||
"tbody" ||
|
||||
"tr" ||
|
||||
"th" ||
|
||||
"td" ||
|
||||
"caption" ||
|
||||
"pre" ||
|
||||
"span" ||
|
||||
"details" ||
|
||||
"summary") =>
|
||||
null,
|
||||
("del" ||
|
||||
"h1" ||
|
||||
"h2" ||
|
||||
"h3" ||
|
||||
"h4" ||
|
||||
"h5" ||
|
||||
"h6" ||
|
||||
"p" ||
|
||||
"ul" ||
|
||||
"ol" ||
|
||||
"sup" ||
|
||||
"sub" ||
|
||||
"li" ||
|
||||
"b" ||
|
||||
"i" ||
|
||||
"u" ||
|
||||
"strong" ||
|
||||
"em" ||
|
||||
"s" ||
|
||||
"code" ||
|
||||
"hr" ||
|
||||
"br" ||
|
||||
"div" ||
|
||||
"table" ||
|
||||
"thead" ||
|
||||
"tbody" ||
|
||||
"tr" ||
|
||||
"th" ||
|
||||
"td" ||
|
||||
"caption" ||
|
||||
"pre" ||
|
||||
"span" ||
|
||||
"details" ||
|
||||
"summary") =>
|
||||
null,
|
||||
|
||||
_ => SizedBox.shrink(),
|
||||
};
|
||||
},
|
||||
customStylesBuilder: (element) => {
|
||||
"width": "auto",
|
||||
...Map.fromEntries(
|
||||
element.attributes
|
||||
.mapTo<MapEntry<String, String>?>(
|
||||
(key, value) => switch (key) {
|
||||
"data-mx-color" => MapEntry("color", value),
|
||||
"data-mx-bg-color" => MapEntry("background-color", value),
|
||||
_ => null,
|
||||
},
|
||||
)
|
||||
.nonNulls,
|
||||
),
|
||||
},
|
||||
onTapUrl: (url) =>
|
||||
ref.watch(LaunchHelper.provider).launchUrl(Uri.parse(url)),
|
||||
_ => SizedBox.shrink(),
|
||||
};
|
||||
},
|
||||
customStylesBuilder: (element) => {
|
||||
"width": "auto",
|
||||
...Map.fromEntries(
|
||||
element.attributes
|
||||
.mapTo<MapEntry<String, String>?>(
|
||||
(key, value) => switch (key) {
|
||||
"data-mx-color" => MapEntry("color", value),
|
||||
"data-mx-bg-color" => MapEntry("background-color", value),
|
||||
_ => null,
|
||||
},
|
||||
)
|
||||
.nonNulls,
|
||||
),
|
||||
},
|
||||
onTapUrl: (url) =>
|
||||
ref.watch(LaunchHelper.provider).launchUrl(Uri.parse(url)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/author_controller.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/helpers/extensions/show_user_popover.dart";
|
||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||
|
||||
class MessageAvatar extends ConsumerWidget {
|
||||
|
|
@ -14,10 +15,16 @@ class MessageAvatar extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) => ref
|
||||
.watch(AuthorController.provider(message))
|
||||
.betterWhen(
|
||||
data: (membership) => AvatarOrHash(
|
||||
membership.avatarUrl,
|
||||
membership.displayName,
|
||||
height: height,
|
||||
data: (membership) => InkWell(
|
||||
onTapDown: (details) => context.showUserPopover(
|
||||
membership,
|
||||
globalPosition: details.globalPosition,
|
||||
),
|
||||
child: AvatarOrHash(
|
||||
membership.avatarUrl,
|
||||
membership.displayName,
|
||||
height: height,
|
||||
),
|
||||
),
|
||||
loading: () =>
|
||||
AvatarOrHash(null, message.authorId.substring(1), height: height),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/author_controller.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/helpers/extensions/show_user_popover.dart";
|
||||
|
||||
class MessageDisplayname extends ConsumerWidget {
|
||||
final Message message;
|
||||
|
|
@ -13,10 +14,16 @@ class MessageDisplayname extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) => ref
|
||||
.watch(AuthorController.provider(message))
|
||||
.betterWhen(
|
||||
data: (membership) => Text(
|
||||
"${membership.displayName}${message.metadata?["pmp"] == null ? "" : " (via ${message.authorId})"}",
|
||||
style: style,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
data: (membership) => InkWell(
|
||||
onTapDown: (details) => context.showUserPopover(
|
||||
membership,
|
||||
globalPosition: details.globalPosition,
|
||||
),
|
||||
child: Text(
|
||||
"${membership.displayName}${message.metadata?["pmp"] == null ? "" : " (via ${message.authorId})"}",
|
||||
style: style,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
loading: () => Text(""),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import "package:flutter/material.dart";
|
|||
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/helpers/extensions/show_user_popover.dart";
|
||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||
|
||||
class MemberList extends ConsumerWidget {
|
||||
|
|
@ -35,23 +36,24 @@ class MemberList extends ConsumerWidget {
|
|||
child: ListView(
|
||||
children: members
|
||||
.map(
|
||||
(member) => ListTile(
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
Dialog(child: Text("TODO: Open member popover")),
|
||||
(member) => InkWell(
|
||||
onTapDown: (details) => context.showUserPopover(
|
||||
member,
|
||||
globalPosition: details.globalPosition,
|
||||
),
|
||||
leading: AvatarOrHash(
|
||||
member.avatarUrl,
|
||||
member.displayName,
|
||||
),
|
||||
title: Text(
|
||||
member.displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
member.userId,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: ListTile(
|
||||
leading: AvatarOrHash(
|
||||
member.avatarUrl,
|
||||
member.displayName,
|
||||
),
|
||||
title: Text(
|
||||
member.displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
member.userId,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
91
lib/widgets/chat_page/user_popover.dart
Normal file
91
lib/widgets/chat_page/user_popover.dart
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/profile_controller.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/models/membership.dart";
|
||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||
|
||||
class UserPopover extends ConsumerWidget {
|
||||
final Membership member;
|
||||
const UserPopover(this.member, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
return IntrinsicWidth(
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 16,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AvatarOrHash(member.avatarUrl, member.displayName, height: 80),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(
|
||||
member.displayName,
|
||||
style: textTheme.headlineSmall,
|
||||
),
|
||||
SelectableText(member.userId, style: textTheme.titleSmall),
|
||||
SizedBox(height: 4),
|
||||
ref
|
||||
.watch(ProfileController.provider(member.userId))
|
||||
.betterWhen(
|
||||
loading: SizedBox.shrink,
|
||||
data: (profile) => Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
for (final pronoun in profile.pronouns.where(
|
||||
(pronoun) => pronoun.language == "en",
|
||||
))
|
||||
Chip(label: Text(pronoun.summary)),
|
||||
if (profile.timezone != null)
|
||||
Chip(label: Text(profile.timezone!)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
FilledButton.icon(onPressed: null, label: Text("Message")),
|
||||
FilledButton.icon(
|
||||
onPressed: null,
|
||||
label: Text("Kick"),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.error,
|
||||
),
|
||||
foregroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.onError,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: null,
|
||||
label: Text("Ban"),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.errorContainer,
|
||||
),
|
||||
foregroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
].map((e) => Expanded(child: e)).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue