forked from Henry-Hiles/nexus
we got quotes 🔥
This commit is contained in:
parent
51d6e73c24
commit
11c03733cf
14 changed files with 159 additions and 124 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"cSpell.words": ["Displayname"]
|
"cSpell.words": ["Appbar", "Displayname"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
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/current_room_controller.dart";
|
import "package:nexus/controllers/current_room_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/to_message.dart";
|
import "package:nexus/helpers/extensions/event_to_message.dart";
|
||||||
|
|
||||||
class MessageController extends AsyncNotifier<TextMessage?> {
|
class MessageController extends AsyncNotifier<TextMessage?> {
|
||||||
final String id;
|
final String id;
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ 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/events_controller.dart";
|
import "package:nexus/controllers/events_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/to_message.dart";
|
import "package:nexus/helpers/extensions/event_to_message.dart";
|
||||||
import "package:nexus/helpers/extensions/to_messages.dart";
|
import "package:nexus/helpers/extensions/list_to_messages.dart";
|
||||||
|
|
||||||
class RoomChatController extends AsyncNotifier<ChatController> {
|
class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
final Room room;
|
final Room room;
|
||||||
|
|
|
||||||
8
lib/helpers/extensions/color_hex.dart
Normal file
8
lib/helpers/extensions/color_hex.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import "package:flutter/widgets.dart";
|
||||||
|
|
||||||
|
extension ColorHex on Color {
|
||||||
|
String get hex {
|
||||||
|
final rgb = toARGB32() & 0x00FFFFFF;
|
||||||
|
return "#${rgb.toRadixString(16).padLeft(6, "0")}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
import "package:matrix/matrix.dart";
|
import "package:matrix/matrix.dart";
|
||||||
|
|
||||||
extension ToMessage on Event {
|
extension EventToMessage on Event {
|
||||||
Future<Message?> toMessage({bool mustBeText = false}) async {
|
Future<Message?> toMessage({bool mustBeText = false}) async {
|
||||||
final replyId = relationshipType == RelationshipTypes.reply
|
final replyId = relationshipType == RelationshipTypes.reply
|
||||||
? relationshipEventId
|
? relationshipEventId
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
import "package:matrix/matrix.dart";
|
import "package:matrix/matrix.dart";
|
||||||
import "package:nexus/helpers/extensions/to_message.dart";
|
import "package:nexus/helpers/extensions/event_to_message.dart";
|
||||||
|
|
||||||
extension ToMessages on List<MatrixEvent> {
|
extension ListToMessages on List<MatrixEvent> {
|
||||||
Future<List<Message>> toMessages(Room room) async {
|
Future<List<Message>> toMessages(Room room) async {
|
||||||
final messages = await Future.wait(
|
final messages = await Future.wait(
|
||||||
map((event) => Event.fromMatrixEvent(event, room).toMessage()),
|
map((event) => Event.fromMatrixEvent(event, room).toMessage()),
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
extension ToTheme on ColorScheme {
|
extension SchemeToTheme on ColorScheme {
|
||||||
ThemeData get theme => ThemeData.from(colorScheme: this).copyWith(
|
ThemeData get theme => ThemeData.from(colorScheme: this).copyWith(
|
||||||
cardTheme: CardThemeData(color: primaryContainer),
|
cardTheme: CardThemeData(color: primaryContainer),
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
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/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/to_theme.dart";
|
import "package:nexus/helpers/extensions/scheme_to_theme.dart";
|
||||||
import "package:nexus/pages/chat_page.dart";
|
import "package:nexus/pages/chat_page.dart";
|
||||||
import "package:nexus/pages/login_page.dart";
|
import "package:nexus/pages/login_page.dart";
|
||||||
import "package:window_manager/window_manager.dart";
|
import "package:window_manager/window_manager.dart";
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,10 @@ class CodeBlock extends StatelessWidget {
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(minWidth: 250),
|
constraints: BoxConstraints(minWidth: 250),
|
||||||
padding: EdgeInsets.all(8),
|
padding: EdgeInsets.all(8),
|
||||||
child: SelectableText(code),
|
child: SelectableText(
|
||||||
|
code,
|
||||||
|
style: TextStyle(fontFamily: "monospace"),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
93
lib/widgets/chat_page/html/html.dart
Normal file
93
lib/widgets/chat_page/html/html.dart
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
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/helpers/launch_helper.dart";
|
||||||
|
import "package:nexus/widgets/chat_page/html/spoiler_text.dart";
|
||||||
|
import "package:nexus/widgets/chat_page/html/code_block.dart";
|
||||||
|
import "package:nexus/widgets/chat_page/quoted.dart";
|
||||||
|
|
||||||
|
class Html extends ConsumerWidget {
|
||||||
|
final String html;
|
||||||
|
const Html(this.html, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) => HtmlWidget(
|
||||||
|
html,
|
||||||
|
customWidgetBuilder: (element) {
|
||||||
|
if (element.attributes.keys.contains("data-mx-spoiler")) {
|
||||||
|
return SpoilerText(text: element.text);
|
||||||
|
}
|
||||||
|
return switch (element.localName) {
|
||||||
|
"mx-reply" => SizedBox.shrink(),
|
||||||
|
|
||||||
|
"code" => CodeBlock(
|
||||||
|
element.text,
|
||||||
|
lang: element.className.replaceAll("language-", ""),
|
||||||
|
),
|
||||||
|
|
||||||
|
"blockquote" => Quoted(Html(element.innerHtml)),
|
||||||
|
|
||||||
|
("del" ||
|
||||||
|
"h1" ||
|
||||||
|
"h2" ||
|
||||||
|
"h3" ||
|
||||||
|
"h4" ||
|
||||||
|
"h5" ||
|
||||||
|
"h6" ||
|
||||||
|
"p" ||
|
||||||
|
"a" ||
|
||||||
|
"ul" ||
|
||||||
|
"ol" ||
|
||||||
|
"sup" ||
|
||||||
|
"sub" ||
|
||||||
|
"li" ||
|
||||||
|
"b" ||
|
||||||
|
"i" ||
|
||||||
|
"u" ||
|
||||||
|
"strong" ||
|
||||||
|
"em" ||
|
||||||
|
"s" ||
|
||||||
|
"code" ||
|
||||||
|
"hr" ||
|
||||||
|
"br" ||
|
||||||
|
"div" ||
|
||||||
|
"table" ||
|
||||||
|
"thead" ||
|
||||||
|
"tbody" ||
|
||||||
|
"tr" ||
|
||||||
|
"th" ||
|
||||||
|
"td" ||
|
||||||
|
"caption" ||
|
||||||
|
"pre" ||
|
||||||
|
"span" ||
|
||||||
|
"img" ||
|
||||||
|
"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),
|
||||||
|
|
||||||
|
"edited" => MapEntry("display", "block"),
|
||||||
|
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.nonNulls,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
onTapUrl: (url) =>
|
||||||
|
ref.watch(LaunchHelper.provider).launchUrl(Uri.parse(url)),
|
||||||
|
);
|
||||||
|
}
|
||||||
16
lib/widgets/chat_page/quoted.dart
Normal file
16
lib/widgets/chat_page/quoted.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
class Quoted extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
const Quoted(this.child, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
left: BorderSide(width: 4, color: Theme.of(context).dividerColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(padding: EdgeInsets.only(left: 8), child: child),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
|
||||||
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,14 +14,11 @@ import "package:nexus/controllers/room_chat_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/get_headers.dart";
|
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||||
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
||||||
import "package:nexus/helpers/launch_helper.dart";
|
|
||||||
import "package:nexus/widgets/chat_page/chat_box.dart";
|
import "package:nexus/widgets/chat_page/chat_box.dart";
|
||||||
import "package:nexus/widgets/chat_page/code_block.dart";
|
import "package:nexus/widgets/chat_page/html/html.dart";
|
||||||
import "package:nexus/widgets/chat_page/member_list.dart";
|
import "package:nexus/widgets/chat_page/member_list.dart";
|
||||||
import "package:nexus/widgets/chat_page/room_appbar.dart";
|
import "package:nexus/widgets/chat_page/room_appbar.dart";
|
||||||
import "package:nexus/widgets/chat_page/spoiler_text.dart";
|
|
||||||
import "package:nexus/widgets/chat_page/top_widget.dart";
|
import "package:nexus/widgets/chat_page/top_widget.dart";
|
||||||
import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart";
|
|
||||||
import "package:nexus/widgets/form_text_input.dart";
|
import "package:nexus/widgets/form_text_input.dart";
|
||||||
import "package:nexus/widgets/loading.dart";
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
|
||||||
|
|
@ -175,8 +171,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
loadMoreBuilder: (_) => Loading(),
|
loadMoreBuilder: (_) => Loading(),
|
||||||
chatAnimatedListBuilder: (_, itemBuilder) =>
|
chatAnimatedListBuilder: (_, itemBuilder) =>
|
||||||
ChatAnimatedList(
|
ChatAnimatedList(
|
||||||
itemBuilder:
|
itemBuilder: itemBuilder,
|
||||||
itemBuilder, // TODO: Load earlier
|
|
||||||
onEndReached: notifier.loadOlder,
|
onEndReached: notifier.loadOlder,
|
||||||
onStartReached: () async {
|
onStartReached: () async {
|
||||||
notifier.markRead();
|
notifier.markRead();
|
||||||
|
|
@ -195,7 +190,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
required bool isSentByMe,
|
required bool isSentByMe,
|
||||||
MessageGroupStatus? groupStatus,
|
MessageGroupStatus? groupStatus,
|
||||||
}) => FlyerChatTextMessage(
|
}) => FlyerChatTextMessage(
|
||||||
customWidget: HtmlWidget(
|
customWidget: Html(
|
||||||
message.metadata?["formatted"]
|
message.metadata?["formatted"]
|
||||||
.replaceAllMapped(
|
.replaceAllMapped(
|
||||||
RegExp(
|
RegExp(
|
||||||
|
|
@ -208,76 +203,6 @@ class RoomChat extends HookConsumerWidget {
|
||||||
((message.editedAt != null)
|
((message.editedAt != null)
|
||||||
? "<sub edited>(edited)</sub>"
|
? "<sub edited>(edited)</sub>"
|
||||||
: ""),
|
: ""),
|
||||||
customWidgetBuilder: (element) {
|
|
||||||
if (element.localName == "mx-reply") {
|
|
||||||
return SizedBox.shrink();
|
|
||||||
}
|
|
||||||
if (element.localName == "code") {
|
|
||||||
if (element.parent?.localName ==
|
|
||||||
"pre") {
|
|
||||||
return CodeBlock(
|
|
||||||
element.text,
|
|
||||||
lang: element.className
|
|
||||||
.replaceAll("language-", ""),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (element.localName == "img") {
|
|
||||||
final src = Uri.tryParse(
|
|
||||||
element.attributes["src"] ?? "",
|
|
||||||
);
|
|
||||||
if (src?.scheme != "mxc") {
|
|
||||||
return SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Should do something like:
|
|
||||||
// return Image.network(
|
|
||||||
// src!.getThumbnailUri(
|
|
||||||
// room.roomData.client,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
|
||||||
return SizedBox.shrink();
|
|
||||||
}
|
|
||||||
if (element.attributes.keys.contains(
|
|
||||||
"data-mx-spoiler",
|
|
||||||
)) {
|
|
||||||
return SpoilerText(
|
|
||||||
text: element.text,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
|
|
||||||
"edited" => MapEntry(
|
|
||||||
"display",
|
|
||||||
"block",
|
|
||||||
),
|
|
||||||
_ => null,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.nonNulls,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
onTapUrl: (url) => ref
|
|
||||||
.watch(LaunchHelper.provider)
|
|
||||||
.launchUrl(Uri.parse(url)),
|
|
||||||
),
|
),
|
||||||
topWidget: TopWidget(
|
topWidget: TopWidget(
|
||||||
message,
|
message,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ 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/controllers/message_controller.dart";
|
import "package:nexus/controllers/message_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
|
import "package:nexus/widgets/chat_page/quoted.dart";
|
||||||
|
|
||||||
class TopWidget extends ConsumerWidget {
|
class TopWidget extends ConsumerWidget {
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
@ -54,18 +55,8 @@ class TopWidget extends ConsumerWidget {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
// TODO: Scroll to original message
|
// TODO: Scroll to original message
|
||||||
onTap: () => showAboutDialog(context: context),
|
onTap: () => showAboutDialog(context: context),
|
||||||
child: Container(
|
child: Quoted(
|
||||||
decoration: BoxDecoration(
|
Row(
|
||||||
border: Border(
|
|
||||||
left: BorderSide(
|
|
||||||
width: 4,
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(left: 8),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -94,7 +85,6 @@ class TopWidget extends ConsumerWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue