forked from Nexus/nexus
parent
29840d595a
commit
dbc42b0ec2
6 changed files with 267 additions and 75 deletions
|
|
@ -9,6 +9,7 @@ import "package:nexus/models/composer_draft.dart";
|
||||||
import "package:nexus/models/composer_params.dart";
|
import "package:nexus/models/composer_params.dart";
|
||||||
import "package:nexus/models/content/message.dart";
|
import "package:nexus/models/content/message.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
|
import "package:nexus/models/text_command.dart";
|
||||||
|
|
||||||
class ComposerDraftController(final ComposerParams composer)
|
class ComposerDraftController(final ComposerParams composer)
|
||||||
extends Notifier<ComposerDraft> {
|
extends Notifier<ComposerDraft> {
|
||||||
|
|
@ -35,19 +36,22 @@ class ComposerDraftController(final ComposerParams composer)
|
||||||
.read(RoomsController.provider)[composer.roomId]
|
.read(RoomsController.provider)[composer.roomId]
|
||||||
?.withLatestEdit(rowId);
|
?.withLatestEdit(rowId);
|
||||||
final content = event?.content;
|
final content = event?.content;
|
||||||
final text =
|
// Edit sources start with text commands, like "/me "
|
||||||
event?.localContent?.editSource ??
|
final (:commands, :text) = TextCommand.parse(
|
||||||
switch (content) {
|
event?.localContent?.editSource ??
|
||||||
TextMessageContent(:final body) ||
|
switch (content) {
|
||||||
ImageMessageContent(:final body) ||
|
TextMessageContent(:final body) ||
|
||||||
VideoMessageContent(:final body) ||
|
ImageMessageContent(:final body) ||
|
||||||
AudioMessageContent(:final body) ||
|
VideoMessageContent(:final body) ||
|
||||||
FileMessageContent(:final body) => body,
|
AudioMessageContent(:final body) ||
|
||||||
_ => "",
|
FileMessageContent(:final body) => body,
|
||||||
};
|
_ => "",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
state = ComposerDraft(
|
state = ComposerDraft(
|
||||||
text: text,
|
text: text,
|
||||||
|
textCommands: commands,
|
||||||
relatedEvent: rowId,
|
relatedEvent: rowId,
|
||||||
relationType: .edit,
|
relationType: .edit,
|
||||||
beforeEdit:
|
beforeEdit:
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import "package:nexus/models/content/image_pack.dart";
|
||||||
import "package:nexus/models/content/message.dart";
|
import "package:nexus/models/content/message.dart";
|
||||||
import "package:nexus/models/content/poll_start.dart";
|
import "package:nexus/models/content/poll_start.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
|
import "package:nexus/models/text_command.dart";
|
||||||
|
|
||||||
part "composer_draft.freezed.dart";
|
part "composer_draft.freezed.dart";
|
||||||
|
|
||||||
|
|
@ -14,6 +15,7 @@ class const ComposerDraft({
|
||||||
// Formatted like ":id#name#", see the composer's tagTextFormatter
|
// Formatted like ":id#name#", see the composer's tagTextFormatter
|
||||||
final String text = "",
|
final String text = "",
|
||||||
final IList<Tag> tags = const .empty(),
|
final IList<Tag> tags = const .empty(),
|
||||||
|
final IList<TextCommand> textCommands = const .empty(),
|
||||||
final int? relatedEvent,
|
final int? relatedEvent,
|
||||||
final RelationType relationType = .reply,
|
final RelationType relationType = .reply,
|
||||||
|
|
||||||
|
|
|
||||||
48
lib/models/text_command.dart
Normal file
48
lib/models/text_command.dart
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import "package:collection/collection.dart";
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
|
||||||
|
enum TextCommandGroup { messageType, format }
|
||||||
|
|
||||||
|
// Prefixes gomuks parses out of sent text, in the order it expects them
|
||||||
|
enum TextCommand(final String description, {final TextCommandGroup? group}) {
|
||||||
|
me("Send as an emote", group: .messageType),
|
||||||
|
notice("Send as a notice", group: .messageType),
|
||||||
|
unencrypted("Send unencrypted, even if the room is encrypted"),
|
||||||
|
rawinputbody("Use the text as the body as-is, rather than from the HTML"),
|
||||||
|
plain("Send without any formatting", group: .format),
|
||||||
|
html("Send with only HTML, without markdown", group: .format),
|
||||||
|
htmlmd("Send with both HTML and markdown", group: .format),
|
||||||
|
rainbow("Send in rainbow colors, with markdown", group: .format);
|
||||||
|
|
||||||
|
String get prefix => "/$name";
|
||||||
|
|
||||||
|
bool completes(String text) =>
|
||||||
|
text.startsWith("/") && prefix.startsWith(text.toLowerCase());
|
||||||
|
|
||||||
|
// Replaces any command from the same group, as gomuks only uses one
|
||||||
|
IList<TextCommand> addTo(IList<TextCommand> commands) => .new(
|
||||||
|
commands
|
||||||
|
.where(
|
||||||
|
(other) => other != this && (group == null || other.group != group),
|
||||||
|
)
|
||||||
|
.toIList()
|
||||||
|
.add(this)
|
||||||
|
.sortedBy((command) => command.index),
|
||||||
|
);
|
||||||
|
|
||||||
|
static String prefixOf(IList<TextCommand> commands) =>
|
||||||
|
commands.map((command) => "${command.prefix} ").join();
|
||||||
|
|
||||||
|
static ({IList<TextCommand> commands, String text}) parse(
|
||||||
|
String text, [
|
||||||
|
IList<TextCommand> commands = const .empty(),
|
||||||
|
]) => switch (values.firstWhereOrNull(
|
||||||
|
(command) => text.toLowerCase().startsWith("${command.prefix} "),
|
||||||
|
)) {
|
||||||
|
final command? => parse(
|
||||||
|
text.substring(command.prefix.length + 1),
|
||||||
|
command.addTo(commands),
|
||||||
|
),
|
||||||
|
null => (commands: commands, text: text),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -8,13 +8,16 @@ import "package:nexus/controllers/bot_argument_suggestions.dart";
|
||||||
import "package:nexus/controllers/bot_command_draft.dart";
|
import "package:nexus/controllers/bot_command_draft.dart";
|
||||||
import "package:nexus/models/bot_argument_suggestion.dart";
|
import "package:nexus/models/bot_argument_suggestion.dart";
|
||||||
import "package:nexus/models/bot_command.dart";
|
import "package:nexus/models/bot_command.dart";
|
||||||
|
import "package:nexus/models/text_command.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
class const BotCommandPanel(
|
class const BotCommandPanel(
|
||||||
final ComposerParams composer, {
|
final ComposerParams composer, {
|
||||||
required final IList<BotCommand> commands,
|
required final IList<BotCommand> commands,
|
||||||
|
required final IList<TextCommand> textCommands,
|
||||||
required final TextEditingController controller,
|
required final TextEditingController controller,
|
||||||
required final void Function(BotCommand command) onPick,
|
required final void Function(BotCommand command) onPick,
|
||||||
|
required final void Function(TextCommand command) onPickText,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends HookConsumerWidget {
|
}) extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
|
|
@ -49,13 +52,27 @@ class const BotCommandPanel(
|
||||||
);
|
);
|
||||||
|
|
||||||
final children = switch (ref.watch(draftProvider)) {
|
final children = switch (ref.watch(draftProvider)) {
|
||||||
null =>
|
null => [
|
||||||
commands
|
...textCommands
|
||||||
|
.where((command) => command.completes(text.value))
|
||||||
|
.map(
|
||||||
|
(command) => ListTile(
|
||||||
|
leading: AvatarOrHash(null, "gomuks", dimension: 32),
|
||||||
|
title: Text(command.prefix),
|
||||||
|
subtitle: Text(
|
||||||
|
"gomuks · ${command.description}",
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: .ellipsis,
|
||||||
|
),
|
||||||
|
onTap: () => onPickText(command),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...commands
|
||||||
.where((command) => command.completes(text.value))
|
.where((command) => command.completes(text.value))
|
||||||
.map(
|
.map(
|
||||||
(command) => commandTile(command, onTap: () => onPick(command)),
|
(command) => commandTile(command, onTap: () => onPick(command)),
|
||||||
)
|
),
|
||||||
.toList(),
|
],
|
||||||
final draft => [
|
final draft => [
|
||||||
commandTile(draft.command),
|
commandTile(draft.command),
|
||||||
if (draft.focusedParameter case final parameter?)
|
if (draft.focusedParameter case final parameter?)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import "package:nexus/helpers/extensions/with_latest_edit.dart";
|
||||||
import "package:nexus/models/bot_command.dart";
|
import "package:nexus/models/bot_command.dart";
|
||||||
import "package:nexus/models/bot_command_draft.dart";
|
import "package:nexus/models/bot_command_draft.dart";
|
||||||
import "package:nexus/models/composer_params.dart";
|
import "package:nexus/models/composer_params.dart";
|
||||||
|
import "package:nexus/models/text_command.dart";
|
||||||
import "package:nexus/widgets/composer/bot_command_form.dart";
|
import "package:nexus/widgets/composer/bot_command_form.dart";
|
||||||
import "package:nexus/widgets/composer/bot_command_panel.dart";
|
import "package:nexus/widgets/composer/bot_command_panel.dart";
|
||||||
import "package:nexus/widgets/emoji_picker.dart";
|
import "package:nexus/widgets/emoji_picker.dart";
|
||||||
|
|
@ -90,6 +91,7 @@ class const Composer(
|
||||||
:sticker,
|
:sticker,
|
||||||
:poll,
|
:poll,
|
||||||
:location,
|
:location,
|
||||||
|
:textCommands,
|
||||||
) = ref.watch(
|
) = ref.watch(
|
||||||
draftProvider.select(
|
draftProvider.select(
|
||||||
(draft) => (
|
(draft) => (
|
||||||
|
|
@ -99,6 +101,7 @@ class const Composer(
|
||||||
sticker: draft.sticker,
|
sticker: draft.sticker,
|
||||||
poll: draft.poll,
|
poll: draft.poll,
|
||||||
location: draft.location,
|
location: draft.location,
|
||||||
|
textCommands: draft.textCommands,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -269,11 +272,57 @@ class const Composer(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final botCommands = isEditing || hasReplacement || attachment != null
|
// Bot commands can't follow text commands, which gomuks sends as text
|
||||||
|
final botCommands =
|
||||||
|
isEditing ||
|
||||||
|
hasReplacement ||
|
||||||
|
attachment != null ||
|
||||||
|
textCommands.isNotEmpty
|
||||||
? const IList<BotCommand>.empty()
|
? const IList<BotCommand>.empty()
|
||||||
: ref.watch(BotCommandsController.provider(roomId)).value ??
|
: ref.watch(BotCommandsController.provider(roomId)).value ??
|
||||||
const .empty();
|
const .empty();
|
||||||
|
|
||||||
|
final availableTextCommands = hasReplacement || attachment != null
|
||||||
|
? const IList<TextCommand>.empty()
|
||||||
|
: TextCommand.values
|
||||||
|
.where((command) => !textCommands.contains(command))
|
||||||
|
.toIList();
|
||||||
|
|
||||||
|
void setTextCommands(IList<TextCommand> commands) =>
|
||||||
|
draftNotifier.update((draft) => draft.copyWith(textCommands: commands));
|
||||||
|
|
||||||
|
void pickTextCommand(TextCommand command) {
|
||||||
|
setTextCommands(command.addTo(textCommands));
|
||||||
|
controller.text = "";
|
||||||
|
node?.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeTextCommand(TextCommand command) {
|
||||||
|
setTextCommands(textCommands.remove(command));
|
||||||
|
node?.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns text commands typed at the start, like "/me ", into chips.
|
||||||
|
/// Returns whether there were any.
|
||||||
|
bool pickTypedTextCommands(String text) {
|
||||||
|
if (availableTextCommands.isEmpty) return false;
|
||||||
|
final parsed = TextCommand.parse(text, textCommands);
|
||||||
|
if (parsed.text == text) return false;
|
||||||
|
|
||||||
|
setTextCommands(parsed.commands);
|
||||||
|
final removed = text.length - parsed.text.length;
|
||||||
|
controller.value = .new(
|
||||||
|
text: parsed.text,
|
||||||
|
selection: .collapsed(
|
||||||
|
offset: (controller.selection.baseOffset - removed).clamp(
|
||||||
|
0,
|
||||||
|
parsed.text.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void pickBotCommand(BotCommand command) {
|
void pickBotCommand(BotCommand command) {
|
||||||
ref
|
ref
|
||||||
.read(botCommandProvider.notifier)
|
.read(botCommandProvider.notifier)
|
||||||
|
|
@ -290,7 +339,8 @@ class const Composer(
|
||||||
[] => null,
|
[] => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
void pickTypedBotCommand(String text) {
|
void pickTypedCommand(String text) {
|
||||||
|
if (pickTypedTextCommands(text)) return;
|
||||||
if (botCommands.any((command) => command.prefix.startsWith(text))) return;
|
if (botCommands.any((command) => command.prefix.startsWith(text))) return;
|
||||||
if (typedBotCommand(text) case final command?) pickBotCommand(command);
|
if (typedBotCommand(text) case final command?) pickBotCommand(command);
|
||||||
}
|
}
|
||||||
|
|
@ -352,10 +402,19 @@ class const Composer(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typedBotCommand(controller.text) ??
|
// In the order the command panel shows them
|
||||||
botCommands.firstWhereOrNull(
|
if (typedBotCommand(controller.text) case final command?) {
|
||||||
(command) => command.completes(controller.text),
|
return pickBotCommand(command);
|
||||||
)
|
}
|
||||||
|
if (availableTextCommands.firstWhereOrNull(
|
||||||
|
(command) => command.completes(controller.text),
|
||||||
|
)
|
||||||
|
case final command?) {
|
||||||
|
return pickTextCommand(command);
|
||||||
|
}
|
||||||
|
if (botCommands.firstWhereOrNull(
|
||||||
|
(command) => command.completes(controller.text),
|
||||||
|
)
|
||||||
case final command?) {
|
case final command?) {
|
||||||
return pickBotCommand(command);
|
return pickBotCommand(command);
|
||||||
}
|
}
|
||||||
|
|
@ -365,13 +424,14 @@ class const Composer(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onSend(
|
onSend(
|
||||||
controller.formattedText,
|
"${TextCommand.prefixOf(textCommands)}${controller.formattedText}",
|
||||||
shouldMention: shouldMention.value,
|
shouldMention: shouldMention.value,
|
||||||
tags: .new(controller.tags),
|
tags: .new(controller.tags),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Before dismissing, which restores the draft from before an edit
|
// Before dismissing, which restores the draft from before an edit
|
||||||
controller.text = "";
|
controller.text = "";
|
||||||
|
setTextCommands(const .empty());
|
||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,6 +446,25 @@ class const Composer(
|
||||||
return .ignored;
|
return .ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KeyEventResult onTextFieldKeyEvent(FocusNode node, KeyEvent event) {
|
||||||
|
if (textCommands.lastOrNull case final command?
|
||||||
|
when event is KeyDownEvent &&
|
||||||
|
event.logicalKey == .backspace &&
|
||||||
|
controller.selection.isCollapsed &&
|
||||||
|
controller.selection.baseOffset == 0) {
|
||||||
|
setTextCommands(textCommands.removeLast());
|
||||||
|
if (controller.text.isEmpty) {
|
||||||
|
controller.value = .new(
|
||||||
|
text: command.prefix,
|
||||||
|
selection: .collapsed(offset: command.prefix.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return .handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendOnEnter(node, event);
|
||||||
|
}
|
||||||
|
|
||||||
final style = TextStyle(
|
final style = TextStyle(
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
fontWeight: .bold,
|
fontWeight: .bold,
|
||||||
|
|
@ -419,8 +498,10 @@ class const Composer(
|
||||||
child: BotCommandPanel(
|
child: BotCommandPanel(
|
||||||
composer,
|
composer,
|
||||||
commands: botCommands,
|
commands: botCommands,
|
||||||
|
textCommands: availableTextCommands,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onPick: pickBotCommand,
|
onPick: pickBotCommand,
|
||||||
|
onPickText: pickTextCommand,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
|
|
@ -731,7 +812,7 @@ class const Composer(
|
||||||
":": style,
|
":": style,
|
||||||
},
|
},
|
||||||
builder: (context, key) => Focus(
|
builder: (context, key) => Focus(
|
||||||
onKeyEvent: sendOnEnter,
|
onKeyEvent: onTextFieldKeyEvent,
|
||||||
// Overrides the keyboard shortcut for pasting
|
// Overrides the keyboard shortcut for pasting
|
||||||
child: Actions(
|
child: Actions(
|
||||||
actions: {
|
actions: {
|
||||||
|
|
@ -739,60 +820,85 @@ class const Composer(
|
||||||
pasteFiles,
|
pasteFiles,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
child: TextField(
|
child: Row(
|
||||||
maxLines: 12,
|
spacing: 4,
|
||||||
minLines: 1,
|
children: [
|
||||||
textCapitalization: .sentences,
|
...textCommands.map(
|
||||||
onChanged: pickTypedBotCommand,
|
(command) => InputChip(
|
||||||
autofocus:
|
label: Text(command.prefix),
|
||||||
(Platform.isLinux ||
|
tooltip: command.description,
|
||||||
Platform.isMacOS ||
|
visualDensity: .compact,
|
||||||
Platform.isWindows),
|
materialTapTargetSize:
|
||||||
decoration: .new(
|
.shrinkWrap,
|
||||||
hintText: "Your message here...",
|
onDeleted: () =>
|
||||||
border: .none,
|
removeTextCommand(command),
|
||||||
),
|
|
||||||
controller: controller,
|
|
||||||
key: key,
|
|
||||||
focusNode: node,
|
|
||||||
// The toolbar's paste button doesn't use the shortcut's action, so replace it too
|
|
||||||
contextMenuBuilder: (_, editableText) =>
|
|
||||||
AdaptiveTextSelectionToolbar.buttonItems(
|
|
||||||
anchors: editableText
|
|
||||||
.contextMenuAnchors,
|
|
||||||
buttonItems: editableText
|
|
||||||
.contextMenuButtonItems
|
|
||||||
.map(
|
|
||||||
(item) =>
|
|
||||||
item.type == .paste
|
|
||||||
? item.copyWith(
|
|
||||||
onPressed: () async {
|
|
||||||
editableText
|
|
||||||
.hideToolbar();
|
|
||||||
if (!await pasteFiles()) {
|
|
||||||
item.onPressed
|
|
||||||
?.call();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: item,
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
// Images pasted from Android keyboards' clipboards are inserted as content
|
),
|
||||||
contentInsertionConfiguration: .new(
|
Expanded(
|
||||||
onContentInserted: (content) {
|
child: TextField(
|
||||||
if (attachment != null) return;
|
maxLines: 12,
|
||||||
|
minLines: 1,
|
||||||
|
textCapitalization: .sentences,
|
||||||
|
onChanged: pickTypedCommand,
|
||||||
|
autofocus:
|
||||||
|
(Platform.isLinux ||
|
||||||
|
Platform.isMacOS ||
|
||||||
|
Platform.isWindows),
|
||||||
|
decoration: .new(
|
||||||
|
hintText:
|
||||||
|
"Your message here...",
|
||||||
|
border: .none,
|
||||||
|
),
|
||||||
|
controller: controller,
|
||||||
|
key: key,
|
||||||
|
focusNode: node,
|
||||||
|
// The toolbar's paste button doesn't use the shortcut's action, so replace it too
|
||||||
|
contextMenuBuilder:
|
||||||
|
(
|
||||||
|
_,
|
||||||
|
editableText,
|
||||||
|
) => AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
anchors: editableText
|
||||||
|
.contextMenuAnchors,
|
||||||
|
buttonItems: editableText
|
||||||
|
.contextMenuButtonItems
|
||||||
|
.map(
|
||||||
|
(item) =>
|
||||||
|
item.type ==
|
||||||
|
.paste
|
||||||
|
? item.copyWith(
|
||||||
|
onPressed: () async {
|
||||||
|
editableText
|
||||||
|
.hideToolbar();
|
||||||
|
if (!await pasteFiles()) {
|
||||||
|
item.onPressed
|
||||||
|
?.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: item,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
// Images pasted from Android keyboards' clipboards are inserted as content
|
||||||
|
contentInsertionConfiguration: .new(
|
||||||
|
onContentInserted: (content) {
|
||||||
|
if (attachment != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (content.data
|
if (content.data
|
||||||
case final data?) {
|
case final data?) {
|
||||||
attachBytes(
|
attachBytes(
|
||||||
data,
|
data,
|
||||||
"image.${content.mimeType.split("/").last}",
|
"image.${content.mimeType.split("/").last}",
|
||||||
).onError(showError);
|
).onError(showError);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.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:html/dom.dart" as dom;
|
||||||
import "package:nexus/helpers/extensions/get_link_info.dart";
|
import "package:nexus/helpers/extensions/get_link_info.dart";
|
||||||
import "package:nexus/helpers/launch_helper.dart";
|
import "package:nexus/helpers/launch_helper.dart";
|
||||||
import "package:nexus/helpers/mxc_image.dart";
|
import "package:nexus/helpers/mxc_image.dart";
|
||||||
|
|
@ -65,6 +66,20 @@ class const Html(
|
||||||
Html(element.innerHtml, textStyle: textStyle, roomId: roomId),
|
Html(element.innerHtml, textStyle: textStyle, roomId: roomId),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
"font" => InlineCustomWidget(
|
||||||
|
child: Html(
|
||||||
|
(dom.Element.tag("span")
|
||||||
|
..attributes.addAll({
|
||||||
|
"data-mx-color": ?element.attributes["color"],
|
||||||
|
...element.attributes,
|
||||||
|
})
|
||||||
|
..innerHtml = element.innerHtml)
|
||||||
|
.outerHtml,
|
||||||
|
textStyle: textStyle,
|
||||||
|
roomId: roomId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
"a" =>
|
"a" =>
|
||||||
element.attributes["href"]?.linkInfo == null
|
element.attributes["href"]?.linkInfo == null
|
||||||
? null
|
? null
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue