2020-05-09 13:36:41 +02:00
|
|
|
import 'package:flutter/material.dart';
|
2021-10-26 18:50:34 +02:00
|
|
|
|
2021-07-24 18:04:13 +02:00
|
|
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
2021-10-26 18:50:34 +02:00
|
|
|
import 'package:flutter_matrix_html/flutter_html.dart';
|
|
|
|
import 'package:matrix/matrix.dart';
|
|
|
|
|
2021-11-09 21:32:16 +01:00
|
|
|
import 'package:fluffychat/widgets/matrix.dart';
|
|
|
|
import '../../../config/app_config.dart';
|
|
|
|
import '../../../config/setting_keys.dart';
|
|
|
|
import '../../../pages/image_viewer/image_viewer.dart';
|
|
|
|
import '../../../utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
|
|
|
import '../../../utils/url_launcher.dart';
|
2020-05-09 13:36:41 +02:00
|
|
|
|
|
|
|
class HtmlMessage extends StatelessWidget {
|
|
|
|
final String html;
|
2022-01-29 12:35:03 +01:00
|
|
|
final int? maxLines;
|
2020-05-14 07:43:21 +02:00
|
|
|
final Room room;
|
2022-01-29 12:35:03 +01:00
|
|
|
final TextStyle? defaultTextStyle;
|
|
|
|
final TextStyle? linkStyle;
|
|
|
|
final double? emoteSize;
|
2020-05-09 13:36:41 +02:00
|
|
|
|
2021-10-14 18:09:30 +02:00
|
|
|
const HtmlMessage({
|
2022-01-29 12:35:03 +01:00
|
|
|
Key? key,
|
|
|
|
required this.html,
|
2021-10-14 18:09:30 +02:00
|
|
|
this.maxLines,
|
2022-01-29 12:35:03 +01:00
|
|
|
required this.room,
|
2021-10-14 18:09:30 +02:00
|
|
|
this.defaultTextStyle,
|
|
|
|
this.linkStyle,
|
|
|
|
this.emoteSize,
|
|
|
|
}) : super(key: key);
|
2020-05-09 13:36:41 +02:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2020-07-05 18:41:27 +02:00
|
|
|
// riot-web is notorious for creating bad reply fallback events from invalid messages which, if
|
|
|
|
// not handled properly, can lead to impersination. As such, we strip the entire `<mx-reply>` tags
|
|
|
|
// here already, to prevent that from happening.
|
|
|
|
// We do *not* do this in an AST and just with simple regex here, as riot-web tends to create
|
|
|
|
// miss-matching tags, and this way we actually correctly identify what we want to strip and, well,
|
|
|
|
// strip it.
|
2020-12-27 16:47:03 +01:00
|
|
|
final renderHtml = html.replaceAll(
|
2021-10-14 18:09:30 +02:00
|
|
|
RegExp('<mx-reply>.*</mx-reply>',
|
2020-12-27 16:47:03 +01:00
|
|
|
caseSensitive: false, multiLine: false, dotAll: true),
|
|
|
|
'');
|
2020-07-05 18:41:27 +02:00
|
|
|
|
2020-05-09 13:36:41 +02:00
|
|
|
// there is no need to pre-validate the html, as we validate it while rendering
|
2020-05-22 12:21:16 +02:00
|
|
|
|
2020-10-28 10:15:06 +01:00
|
|
|
final matrix = Matrix.of(context);
|
|
|
|
|
2020-05-15 07:47:32 +02:00
|
|
|
final themeData = Theme.of(context);
|
2020-05-09 13:36:41 +02:00
|
|
|
return Html(
|
2020-07-05 18:41:27 +02:00
|
|
|
data: renderHtml,
|
2020-05-15 07:47:32 +02:00
|
|
|
defaultTextStyle: defaultTextStyle,
|
2020-09-20 11:35:28 +02:00
|
|
|
emoteSize: emoteSize,
|
2020-05-22 12:21:16 +02:00
|
|
|
linkStyle: linkStyle ??
|
2022-01-29 12:35:03 +01:00
|
|
|
themeData.textTheme.bodyText2!.copyWith(
|
2021-05-24 10:59:00 +02:00
|
|
|
color: themeData.colorScheme.secondary,
|
2020-05-22 12:21:16 +02:00
|
|
|
decoration: TextDecoration.underline,
|
|
|
|
),
|
2020-05-09 13:36:41 +02:00
|
|
|
shrinkToFit: true,
|
|
|
|
maxLines: maxLines,
|
2020-09-05 13:45:03 +02:00
|
|
|
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
|
|
|
|
onPillTap: (url) => UrlLauncher(context, url).launchUrl(),
|
2022-01-29 12:35:03 +01:00
|
|
|
getMxcUrl: (String mxc, double? width, double? height,
|
|
|
|
{bool? animated = false}) {
|
2020-05-09 13:36:41 +02:00
|
|
|
final ratio = MediaQuery.of(context).devicePixelRatio;
|
2021-04-21 14:19:54 +02:00
|
|
|
return Uri.parse(mxc)
|
2022-01-29 12:35:03 +01:00
|
|
|
.getThumbnail(
|
2021-04-21 14:19:54 +02:00
|
|
|
matrix.client,
|
|
|
|
width: (width ?? 800) * ratio,
|
|
|
|
height: (height ?? 800) * ratio,
|
|
|
|
method: ThumbnailMethod.scale,
|
2021-08-07 19:36:42 +02:00
|
|
|
animated: AppConfig.autoplayImages ? animated : false,
|
2021-04-21 14:19:54 +02:00
|
|
|
)
|
|
|
|
.toString();
|
2020-05-09 13:36:41 +02:00
|
|
|
},
|
2021-08-07 19:36:42 +02:00
|
|
|
onImageTap: (String mxc) => showDialog(
|
|
|
|
context: Matrix.of(context).navigatorContext,
|
|
|
|
useRootNavigator: false,
|
|
|
|
builder: (_) => ImageViewer(Event.fromJson({
|
|
|
|
'type': EventTypes.Message,
|
|
|
|
'content': <String, dynamic>{
|
|
|
|
'body': mxc,
|
|
|
|
'url': mxc,
|
|
|
|
'msgtype': MessageTypes.Image,
|
|
|
|
},
|
|
|
|
'event_id': 'fake_event',
|
|
|
|
}, room))),
|
2020-10-28 10:15:06 +01:00
|
|
|
setCodeLanguage: (String key, String value) async {
|
2020-11-07 12:00:41 +01:00
|
|
|
await matrix.store.setItem('${SettingKeys.codeLanguage}.$key', value);
|
2020-10-28 10:15:06 +01:00
|
|
|
},
|
|
|
|
getCodeLanguage: (String key) async {
|
2020-11-07 12:00:41 +01:00
|
|
|
return await matrix.store.getItem('${SettingKeys.codeLanguage}.$key');
|
2020-10-28 10:15:06 +01:00
|
|
|
},
|
2021-01-10 18:00:27 +01:00
|
|
|
getPillInfo: (String url) async {
|
|
|
|
final identityParts = url.parseIdentifierIntoParts();
|
|
|
|
final identifier = identityParts?.primaryIdentifier;
|
|
|
|
if (identifier == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if (identifier.sigil == '@') {
|
2020-05-14 07:43:21 +02:00
|
|
|
// 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);
|
2022-01-29 12:35:03 +01:00
|
|
|
return {
|
|
|
|
'displayname': profile.displayName,
|
|
|
|
'avatar_url': profile.avatarUrl.toString(),
|
|
|
|
};
|
2020-05-14 07:43:21 +02:00
|
|
|
}
|
2021-01-10 18:00:27 +01:00
|
|
|
if (identifier.sigil == '#') {
|
2020-05-14 07:43:21 +02:00
|
|
|
// we have an alias pill
|
|
|
|
for (final r in room.client.rooms) {
|
|
|
|
final state = r.getState('m.room.canonical_alias');
|
2020-05-22 12:21:16 +02:00
|
|
|
if (state != null &&
|
|
|
|
((state.content['alias'] is String &&
|
|
|
|
state.content['alias'] == identifier) ||
|
|
|
|
(state.content['alt_aliases'] is List &&
|
|
|
|
state.content['alt_aliases'].contains(identifier)))) {
|
2020-05-14 07:43:21 +02:00
|
|
|
// we have a room!
|
|
|
|
return {
|
2021-07-24 18:04:13 +02:00
|
|
|
'displayname':
|
2022-01-29 12:35:03 +01:00
|
|
|
r.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
2020-05-14 07:43:21 +02:00
|
|
|
'avatar_url': r.getState('m.room.avatar')?.content['url'],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2021-01-10 18:00:27 +01:00
|
|
|
if (identifier.sigil == '!') {
|
2020-05-14 07:43:21 +02:00
|
|
|
// we have a room ID pill
|
|
|
|
final r = room.client.getRoomById(identifier);
|
|
|
|
if (r == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return {
|
2021-07-24 18:04:13 +02:00
|
|
|
'displayname':
|
2022-01-29 12:35:03 +01:00
|
|
|
r.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
2020-05-14 07:43:21 +02:00
|
|
|
'avatar_url': r.getState('m.room.avatar')?.content['url'],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return null;
|
2022-01-29 12:35:03 +01:00
|
|
|
} as Future<Map<String, dynamic>> Function(String)?,
|
2020-05-09 13:36:41 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|