refactor: Reimplement flutter matrix html locally
This commit is contained in:
parent
d330ca0ff7
commit
796a6fdae1
9 changed files with 431 additions and 291 deletions
|
|
@ -1,31 +1,26 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_matrix_html/flutter_html.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_highlighter/flutter_highlighter.dart';
|
||||
import 'package:flutter_highlighter/themes/shades-of-purple.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_math_fork/flutter_math.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
import '../../../config/setting_keys.dart';
|
||||
import '../../../utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import '../../../utils/url_launcher.dart';
|
||||
|
||||
class HtmlMessage extends StatelessWidget {
|
||||
final String html;
|
||||
final int? maxLines;
|
||||
final Room room;
|
||||
final TextStyle? defaultTextStyle;
|
||||
final TextStyle? linkStyle;
|
||||
final double? emoteSize;
|
||||
final Color? textColor;
|
||||
|
||||
const HtmlMessage({
|
||||
Key? key,
|
||||
required this.html,
|
||||
this.maxLines,
|
||||
required this.room,
|
||||
this.defaultTextStyle,
|
||||
this.linkStyle,
|
||||
this.emoteSize,
|
||||
this.textColor,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
|
@ -46,101 +41,315 @@ class HtmlMessage extends StatelessWidget {
|
|||
'',
|
||||
);
|
||||
|
||||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||
|
||||
// there is no need to pre-validate the html, as we validate it while rendering
|
||||
|
||||
final matrix = Matrix.of(context);
|
||||
|
||||
final themeData = Theme.of(context);
|
||||
return Html(
|
||||
data: renderHtml,
|
||||
defaultTextStyle: defaultTextStyle,
|
||||
emoteSize: emoteSize,
|
||||
linkStyle: linkStyle ??
|
||||
themeData.textTheme.bodyMedium!.copyWith(
|
||||
color: themeData.colorScheme.secondary,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: themeData.colorScheme.secondary,
|
||||
),
|
||||
shrinkToFit: true,
|
||||
maxLines: maxLines,
|
||||
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
|
||||
onPillTap: (url) => UrlLauncher(context, url).launchUrl(),
|
||||
getMxcUrl: (
|
||||
String mxc,
|
||||
double? width,
|
||||
double? height, {
|
||||
bool? animated = false,
|
||||
}) {
|
||||
final ratio = MediaQuery.of(context).devicePixelRatio;
|
||||
return Uri.parse(mxc)
|
||||
.getThumbnail(
|
||||
matrix.client,
|
||||
width: (width ?? 800) * ratio,
|
||||
height: (height ?? 800) * ratio,
|
||||
method: ThumbnailMethod.scale,
|
||||
animated: AppConfig.autoplayImages ? animated : false,
|
||||
)
|
||||
.toString();
|
||||
style: {
|
||||
'*': Style(
|
||||
color: textColor,
|
||||
margin: Margins.all(0),
|
||||
fontSize: FontSize(fontSize),
|
||||
),
|
||||
'a': Style(color: textColor?.withAlpha(150)),
|
||||
'h1': Style(fontSize: FontSize(fontSize * 3)),
|
||||
'h2': Style(fontSize: FontSize(fontSize * 2.5)),
|
||||
'h3': Style(fontSize: FontSize(fontSize * 2)),
|
||||
'h4': Style(fontSize: FontSize(fontSize * 1.75)),
|
||||
'h5': Style(fontSize: FontSize(fontSize * 1.5)),
|
||||
'h6': Style(fontSize: FontSize(fontSize * 1.25)),
|
||||
// Otherwise list tiles are all in the same row$$
|
||||
'li': Style(display: Display.block),
|
||||
},
|
||||
onImageTap: (url) => UrlLauncher(context, url).launchUrl(),
|
||||
setCodeLanguage: (String key, String value) async {
|
||||
await matrix.store.setItem('${SettingKeys.codeLanguage}.$key', value);
|
||||
},
|
||||
getCodeLanguage: (String key) async {
|
||||
return await matrix.store.getItem('${SettingKeys.codeLanguage}.$key');
|
||||
},
|
||||
getPillInfo: (String url) async {
|
||||
final identityParts = url.parseIdentifierIntoParts();
|
||||
final identifier = identityParts?.primaryIdentifier;
|
||||
if (identifier == null) {
|
||||
return {};
|
||||
}
|
||||
if (identifier.sigil == '@') {
|
||||
// we have a user pill
|
||||
final user = room.getState('m.room.member', identifier);
|
||||
if (user != null) {
|
||||
return user.content;
|
||||
}
|
||||
// there might still be a profile...
|
||||
final profile = await room.client.getProfileFromUserId(identifier);
|
||||
return {
|
||||
'displayname': profile.displayName,
|
||||
'avatar_url': profile.avatarUrl.toString(),
|
||||
};
|
||||
}
|
||||
if (identifier.sigil == '#') {
|
||||
// we have an alias pill
|
||||
for (final r in room.client.rooms) {
|
||||
final state = r.getState('m.room.canonical_alias');
|
||||
if (state != null &&
|
||||
((state.content['alias'] is String &&
|
||||
state.content['alias'] == identifier) ||
|
||||
(state.content['alt_aliases'] is List &&
|
||||
state.content['alt_aliases'].contains(identifier)))) {
|
||||
// we have a room!
|
||||
return {
|
||||
'displayname':
|
||||
r.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
'avatar_url': r.getState('m.room.avatar')?.content['url'],
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (identifier.sigil == '!') {
|
||||
// we have a room ID pill
|
||||
final r = room.client.getRoomById(identifier);
|
||||
if (r == null) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
'displayname':
|
||||
r.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
'avatar_url': r.getState('m.room.avatar')?.content['url'],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
extensions: [
|
||||
UserPillExtension(context, room),
|
||||
RoomPillExtension(context, room.client),
|
||||
CodeExtension(fontSize: fontSize),
|
||||
MatrixMathExtension(
|
||||
style: TextStyle(fontSize: fontSize, color: textColor),
|
||||
),
|
||||
],
|
||||
onLinkTap: (url, _, __) => UrlLauncher(context, url).launchUrl(),
|
||||
onlyRenderTheseTags: const {
|
||||
...allowedHtmlTags,
|
||||
// Needed to make it work properly
|
||||
'body',
|
||||
'html',
|
||||
},
|
||||
shrinkWrap: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// Keep in sync with: https://spec.matrix.org/v1.6/client-server-api/#mroommessage-msgtypes
|
||||
static const Set<String> allowedHtmlTags = {
|
||||
'font',
|
||||
'del',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'blockquote',
|
||||
'p',
|
||||
'a',
|
||||
'ul',
|
||||
'ol',
|
||||
'sup',
|
||||
'sub',
|
||||
'li',
|
||||
'b',
|
||||
'i',
|
||||
'u',
|
||||
'strong',
|
||||
'em',
|
||||
'strike',
|
||||
'code',
|
||||
'hr',
|
||||
'br',
|
||||
'div',
|
||||
'table',
|
||||
'thead',
|
||||
'tbody',
|
||||
'tr',
|
||||
'th',
|
||||
'td',
|
||||
'caption',
|
||||
'pre',
|
||||
'span',
|
||||
'img',
|
||||
'details',
|
||||
'summary'
|
||||
};
|
||||
}
|
||||
|
||||
class MatrixMathExtension extends HtmlExtension {
|
||||
final TextStyle? style;
|
||||
|
||||
MatrixMathExtension({this.style});
|
||||
@override
|
||||
Set<String> get supportedTags => {'div'};
|
||||
|
||||
@override
|
||||
bool matches(ExtensionContext context) {
|
||||
if (context.elementName != 'div') return false;
|
||||
final mathData = context.element?.attributes['data-mx-maths'];
|
||||
return mathData != null;
|
||||
}
|
||||
|
||||
@override
|
||||
InlineSpan build(
|
||||
ExtensionContext context,
|
||||
Map<StyledElement, InlineSpan> Function() parseChildren,
|
||||
) {
|
||||
final data = context.element?.attributes['data-mx-maths'] ?? '';
|
||||
return WidgetSpan(
|
||||
child: Math.tex(
|
||||
data,
|
||||
textStyle: style,
|
||||
onErrorFallback: (e) {
|
||||
Logs().d('Flutter math parse error', e);
|
||||
return Text(
|
||||
data,
|
||||
style: style,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CodeExtension extends HtmlExtension {
|
||||
final double fontSize;
|
||||
|
||||
CodeExtension({required this.fontSize});
|
||||
@override
|
||||
Set<String> get supportedTags => {'code'};
|
||||
|
||||
@override
|
||||
InlineSpan build(
|
||||
ExtensionContext context,
|
||||
Map<StyledElement, InlineSpan> Function() parseChildren,
|
||||
) =>
|
||||
WidgetSpan(
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: HighlightView(
|
||||
context.element?.text ?? '',
|
||||
language: context.element?.className
|
||||
.split(' ')
|
||||
.singleWhereOrNull(
|
||||
(className) => className.startsWith('language-'),
|
||||
)
|
||||
?.split('language-')
|
||||
.last ??
|
||||
'md',
|
||||
theme: shadesOfPurpleTheme,
|
||||
padding: const EdgeInsets.all(6),
|
||||
textStyle: TextStyle(fontSize: fontSize),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class UserPillExtension extends HtmlExtension {
|
||||
final Room room;
|
||||
final BuildContext context;
|
||||
|
||||
UserPillExtension(this.context, this.room);
|
||||
@override
|
||||
Set<String> get supportedTags => {'a'};
|
||||
|
||||
@override
|
||||
bool matches(ExtensionContext context) {
|
||||
if (context.elementName != 'a') return false;
|
||||
final href = context.element?.attributes['href'];
|
||||
if (href == null) return false;
|
||||
final uri = Uri.tryParse(href);
|
||||
if (uri == null || uri.host != 'matrix.to') return false;
|
||||
final userId = uri.fragment.split('/').last;
|
||||
if (userId.sigil != '@') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
InlineSpan build(
|
||||
ExtensionContext context,
|
||||
Map<StyledElement, InlineSpan> Function() parseChildren,
|
||||
) {
|
||||
final href = context.element!.attributes['href']!;
|
||||
final uri = Uri.parse(href);
|
||||
final userId = uri.fragment.split('/').last;
|
||||
final user = room.unsafeGetUserFromMemoryOrFallback(userId);
|
||||
return WidgetSpan(
|
||||
child: MatrixPill(
|
||||
name: user.calcDisplayname(),
|
||||
avatar: user.avatarUrl,
|
||||
uri: href,
|
||||
outerContext: this.context,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomPillExtension extends HtmlExtension {
|
||||
final Client client;
|
||||
final BuildContext context;
|
||||
|
||||
RoomPillExtension(this.context, this.client);
|
||||
@override
|
||||
Set<String> get supportedTags => {'a'};
|
||||
|
||||
@override
|
||||
bool matches(ExtensionContext context) {
|
||||
if (context.elementName != 'a') return false;
|
||||
final href = context.element?.attributes['href'];
|
||||
if (href == null) return false;
|
||||
final uri = Uri.tryParse(href);
|
||||
if (uri == null || uri.host != 'matrix.to') return false;
|
||||
final roomId = Uri.decodeComponent(uri.fragment.split('/').last);
|
||||
if (!{'#', '!'}.contains(roomId.sigil)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
InlineSpan build(
|
||||
ExtensionContext context,
|
||||
Map<StyledElement, InlineSpan> Function() parseChildren,
|
||||
) {
|
||||
final href = context.element!.attributes['href']!;
|
||||
final uri = Uri.parse(href);
|
||||
final roomId = Uri.decodeComponent(uri.fragment.split('/').last);
|
||||
|
||||
final room = roomId.sigil == '!'
|
||||
? client.getRoomById(roomId)
|
||||
: client.getRoomByAlias(roomId);
|
||||
if (room != null) {
|
||||
return WidgetSpan(
|
||||
child: MatrixPill(
|
||||
name: room.getLocalizedDisplayname(),
|
||||
avatar: room.avatar,
|
||||
uri: href,
|
||||
outerContext: this.context,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return WidgetSpan(
|
||||
child: FutureBuilder<QueryPublicRoomsResponse>(
|
||||
future: client.queryPublicRooms(
|
||||
server: roomId.domain,
|
||||
filter: PublicRoomQueryFilter(
|
||||
genericSearchTerm: roomId,
|
||||
),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final room = snapshot.data;
|
||||
return MatrixPill(
|
||||
name: room?.chunk.singleOrNull?.name ?? roomId,
|
||||
avatar: room?.chunk.singleOrNull?.avatarUrl,
|
||||
uri: href,
|
||||
outerContext: this.context,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixPill extends StatelessWidget {
|
||||
final String name;
|
||||
final BuildContext outerContext;
|
||||
final Uri? avatar;
|
||||
final String uri;
|
||||
|
||||
const MatrixPill({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.outerContext,
|
||||
this.avatar,
|
||||
required this.uri,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: UrlLauncher(outerContext, uri).launchUrl,
|
||||
child: Material(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
side: BorderSide(
|
||||
color: Theme.of(outerContext).colorScheme.onPrimaryContainer,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
color: Theme.of(outerContext).colorScheme.primaryContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: avatar,
|
||||
name: name,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: Theme.of(outerContext).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix_link_text/link_text.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/events/video_player.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
|
|
@ -150,23 +150,10 @@ class MessageContent extends StatelessWidget {
|
|||
if (event.messageType == MessageTypes.Emote) {
|
||||
html = '* $html';
|
||||
}
|
||||
final bigEmotes = event.onlyEmotes &&
|
||||
event.numberEmotes > 0 &&
|
||||
event.numberEmotes <= 10;
|
||||
return HtmlMessage(
|
||||
html: html,
|
||||
defaultTextStyle: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: textColor.withAlpha(150),
|
||||
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: textColor.withAlpha(150),
|
||||
),
|
||||
textColor: textColor,
|
||||
room: event.room,
|
||||
emoteSize: bigEmotes ? fontSize * 3 : fontSize * 1.5,
|
||||
);
|
||||
}
|
||||
// else we fall through to the normal message rendering
|
||||
|
|
@ -242,13 +229,13 @@ class MessageContent extends StatelessWidget {
|
|||
hideReply: true,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
return LinkText(
|
||||
return Linkify(
|
||||
text: snapshot.data ??
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
hideReply: true,
|
||||
),
|
||||
textStyle: TextStyle(
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
||||
decoration:
|
||||
|
|
@ -260,10 +247,7 @@ class MessageContent extends StatelessWidget {
|
|||
decoration: TextDecoration.underline,
|
||||
decorationColor: textColor.withAlpha(150),
|
||||
),
|
||||
beforeLaunch: (url) {
|
||||
UrlLauncher(context, url.toString()).launchUrl();
|
||||
return false;
|
||||
},
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
import 'html_message.dart';
|
||||
|
||||
class ReplyContent extends StatelessWidget {
|
||||
final Event replyEvent;
|
||||
|
|
@ -26,47 +25,23 @@ class ReplyContent extends StatelessWidget {
|
|||
final displayEvent =
|
||||
timeline != null ? replyEvent.getDisplayEvent(timeline) : replyEvent;
|
||||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||
if (AppConfig.renderHtml &&
|
||||
[EventTypes.Message, EventTypes.Encrypted]
|
||||
.contains(displayEvent.type) &&
|
||||
[MessageTypes.Text, MessageTypes.Notice, MessageTypes.Emote]
|
||||
.contains(displayEvent.messageType) &&
|
||||
!displayEvent.redacted &&
|
||||
displayEvent.content['format'] == 'org.matrix.custom.html' &&
|
||||
displayEvent.content['formatted_body'] is String) {
|
||||
String? html = displayEvent.content['formatted_body'];
|
||||
if (displayEvent.messageType == MessageTypes.Emote) {
|
||||
html = '* $html';
|
||||
}
|
||||
replyBody = HtmlMessage(
|
||||
html: html!,
|
||||
defaultTextStyle: TextStyle(
|
||||
color: ownMessage
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
maxLines: 1,
|
||||
room: displayEvent.room,
|
||||
emoteSize: fontSize * 1.5,
|
||||
);
|
||||
} else {
|
||||
replyBody = Text(
|
||||
displayEvent.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: false,
|
||||
hideReply: true,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: ownMessage
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
replyBody = Text(
|
||||
displayEvent.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: false,
|
||||
hideReply: true,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: ownMessage
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
);
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix_link_text/link_text.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
|
|
@ -101,7 +101,7 @@ class PinnedEvents extends StatelessWidget {
|
|||
hideReply: true,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
return LinkText(
|
||||
return Linkify(
|
||||
text: snapshot.data ??
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
|
|
@ -109,7 +109,7 @@ class PinnedEvents extends StatelessWidget {
|
|||
hideReply: true,
|
||||
),
|
||||
maxLines: 2,
|
||||
textStyle: TextStyle(
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
|
@ -126,10 +126,8 @@ class PinnedEvents extends StatelessWidget {
|
|||
decorationColor:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
beforeLaunch: (url) {
|
||||
UrlLauncher(context, url.toString()).launchUrl();
|
||||
return false;
|
||||
},
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix_link_text/link_text.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
|
|
@ -125,13 +125,13 @@ class ChatDetailsView extends StatelessWidget {
|
|||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: LinkText(
|
||||
child: Linkify(
|
||||
text: room.topic.isEmpty
|
||||
? L10n.of(context)!.addGroupDescription
|
||||
: room.topic,
|
||||
linkStyle:
|
||||
const TextStyle(color: Colors.blueAccent),
|
||||
textStyle: TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
|
|
@ -142,11 +142,8 @@ class ChatDetailsView extends StatelessWidget {
|
|||
.bodyMedium!
|
||||
.color,
|
||||
),
|
||||
beforeLaunch: (url) {
|
||||
UrlLauncher(context, url.toString())
|
||||
.launchUrl();
|
||||
return false;
|
||||
},
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import 'package:flutter/services.dart';
|
|||
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix_link_text/link_text.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
|
|
@ -273,15 +273,13 @@ class StoryView extends StatelessWidget {
|
|||
controller.storyThemeData.alignmentY.toDouble() / 100,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: LinkText(
|
||||
child: Linkify(
|
||||
text: controller.loadingMode
|
||||
? L10n.of(context)!.loadingPleaseWait
|
||||
: event.content.tryGet<String>('body') ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
beforeLaunch: (url) {
|
||||
UrlLauncher(context, url.toString()).launchUrl();
|
||||
return false;
|
||||
},
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.blue.shade50,
|
||||
|
|
@ -291,7 +289,7 @@ class StoryView extends StatelessWidget {
|
|||
? null
|
||||
: textShadows,
|
||||
),
|
||||
textStyle: TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.white,
|
||||
shadows: event.messageType == MessageTypes.Text
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix_link_text/link_text.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
|
|
@ -153,17 +153,15 @@ class PublicRoomBottomSheet extends StatelessWidget {
|
|||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
subtitle: LinkText(
|
||||
subtitle: Linkify(
|
||||
text: profile!.topic!,
|
||||
linkStyle: const TextStyle(color: Colors.blueAccent),
|
||||
textStyle: TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||
),
|
||||
beforeLaunch: (url) {
|
||||
UrlLauncher(context, url.toString()).launchUrl();
|
||||
return false;
|
||||
},
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue