You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fluffychat/lib/pages/chat/events/message_content.dart

446 lines
15 KiB
Dart

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/video_player.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/utils/event_checkbox_extension.dart';
import '../../../config/app_config.dart';
import '../../../utils/platform_infos.dart';
import '../../../utils/url_launcher.dart';
import 'audio_player.dart';
import 'cute_events.dart';
import 'html_message.dart';
import 'image_bubble.dart';
import 'map_bubble.dart';
import 'message_download_content.dart';
class MessageContent extends StatelessWidget {
final Event event;
final Color textColor;
final Color linkColor;
final void Function(Event)? onInfoTab;
final BorderRadius borderRadius;
final Timeline timeline;
final bool selected;
// #Pangea
final PangeaMessageEvent? pangeaMessageEvent;
//question: are there any performance benefits to using booleans
//here rather than passing the choreographer? pangea rich text, a widget
//further down in the chain is also using pangeaController so its not constant
final bool immersionMode;
final MessageOverlayController? overlayController;
final ChatController controller;
final Event? nextEvent;
final Event? prevEvent;
final bool isTransitionAnimation;
final ReadingAssistanceMode? readingAssistanceMode;
// Pangea#
const MessageContent(
this.event, {
this.onInfoTab,
super.key,
required this.timeline,
required this.textColor,
required this.linkColor,
required this.borderRadius,
required this.selected,
// #Pangea
this.pangeaMessageEvent,
required this.immersionMode,
this.overlayController,
required this.controller,
this.nextEvent,
this.prevEvent,
this.isTransitionAnimation = false,
this.readingAssistanceMode,
// Pangea#
});
// #Pangea
// void _verifyOrRequestKey(BuildContext context) async {
// final l10n = L10n.of(context);
// if (event.content['can_request_session'] != true) {
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// content: Text(
// event.calcLocalizedBodyFallback(MatrixLocals(l10n)),
// ),
// ),
// );
// return;
// }
// final client = Matrix.of(context).client;
// if (client.isUnknownSession && client.encryption!.crossSigning.enabled) {
// final success = await BootstrapDialog(
// client: Matrix.of(context).client,
// ).show(context);
// if (success != true) return;
// }
// event.requestKey();
// final sender = event.senderFromMemoryOrFallback;
// await showAdaptiveBottomSheet(
// context: context,
// builder: (context) => Scaffold(
// appBar: AppBar(
// leading: CloseButton(onPressed: Navigator.of(context).pop),
// title: Text(
// l10n.whyIsThisMessageEncrypted,
// style: const TextStyle(fontSize: 16),
// ),
// ),
// body: SafeArea(
// child: ListView(
// padding: const EdgeInsets.all(16),
// children: [
// ListTile(
// contentPadding: EdgeInsets.zero,
// leading: Avatar(
// mxContent: sender.avatarUrl,
// name: sender.calcDisplayname(),
// presenceUserId: sender.stateKey,
// client: event.room.client,
// ),
// title: Text(sender.calcDisplayname()),
// subtitle: Text(event.originServerTs.localizedTime(context)),
// trailing: const Icon(Icons.lock_outlined),
// ),
// const Divider(),
// Text(
// event.calcLocalizedBodyFallback(
// MatrixLocals(l10n),
// ),
// ),
// ],
// ),
// ),
// ),
// );
// }
void onClick(PangeaToken token) {
token = pangeaMessageEvent?.messageDisplayRepresentation
?.getClosestNonPunctToken(token) ??
token;
if (overlayController != null) {
overlayController?.onClickOverlayMessageToken(token);
return;
} else {
Future.delayed(
const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
), () {
TtsController.tryToSpeak(
token.text.content,
langCode: pangeaMessageEvent!.messageDisplayLangCode,
);
});
}
controller.showToolbar(
pangeaMessageEvent!.event,
pangeaMessageEvent: pangeaMessageEvent,
selectedToken: token,
);
}
// Pangea#
@override
Widget build(BuildContext context) {
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
final buttonTextColor = textColor;
switch (event.type) {
case EventTypes.Message:
case EventTypes.Encrypted:
case EventTypes.Sticker:
switch (event.messageType) {
case MessageTypes.Image:
case MessageTypes.Sticker:
if (event.redacted) continue textmessage;
const maxSize = 256.0;
final w = event.content
.tryGetMap<String, Object?>('info')
?.tryGet<int>('w');
final h = event.content
.tryGetMap<String, Object?>('info')
?.tryGet<int>('h');
var width = maxSize;
var height = maxSize;
var fit = event.messageType == MessageTypes.Sticker
? BoxFit.contain
: BoxFit.cover;
if (w != null && h != null) {
fit = BoxFit.contain;
if (w > h) {
width = maxSize;
height = max(32, maxSize * (h / w));
} else {
height = maxSize;
width = max(32, maxSize * (w / h));
}
}
return ImageBubble(
event,
width: width,
height: height,
fit: fit,
borderRadius: borderRadius,
timeline: timeline,
textColor: textColor,
);
case CuteEventContent.eventType:
return CuteContent(event);
case MessageTypes.Audio:
if (PlatformInfos.isMobile ||
PlatformInfos.isMacOS ||
PlatformInfos.isWeb
// Disabled until https://github.com/bleonard252/just_audio_mpv/issues/3
// is fixed
// || PlatformInfos.isLinux
) {
return AudioPlayerWidget(
event,
color: textColor,
linkColor: linkColor,
fontSize: fontSize,
// #Pangea
chatController: controller,
eventId:
"${event.eventId}${overlayController != null ? '_overlay' : ''}",
roomId: event.room.id,
senderId: event.senderId,
autoplay: overlayController != null && isTransitionAnimation,
// Pangea#
);
}
return MessageDownloadContent(
event,
textColor: textColor,
linkColor: linkColor,
);
case MessageTypes.Video:
return EventVideoPlayer(
event,
textColor: textColor,
linkColor: linkColor,
timeline: timeline,
);
case MessageTypes.File:
return MessageDownloadContent(
event,
textColor: textColor,
linkColor: linkColor,
);
case MessageTypes.BadEncrypted:
// #Pangea
// case EventTypes.Encrypted:
// return _ButtonContent(
// textColor: buttonTextColor,
// onPressed: () => _verifyOrRequestKey(context),
// icon: '🔒',
// label: L10n.of(context).encrypted,
// fontSize: fontSize,
// );
// Pangea#
case MessageTypes.Location:
final geoUri =
Uri.tryParse(event.content.tryGet<String>('geo_uri')!);
if (geoUri != null && geoUri.scheme == 'geo') {
final latlong = geoUri.path
.split(';')
.first
.split(',')
.map((s) => double.tryParse(s))
.toList();
if (latlong.length == 2 &&
latlong.first != null &&
latlong.last != null) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
MapBubble(
latitude: latlong.first!,
longitude: latlong.last!,
),
const SizedBox(height: 6),
OutlinedButton.icon(
icon: Icon(Icons.location_on_outlined, color: textColor),
onPressed:
UrlLauncher(context, geoUri.toString()).launchUrl,
label: Text(
L10n.of(context).openInMaps,
style: TextStyle(color: textColor),
),
),
],
);
}
}
continue textmessage;
case MessageTypes.Text:
case MessageTypes.Notice:
case MessageTypes.Emote:
case MessageTypes.None:
textmessage:
default:
if (event.redacted) {
return FutureBuilder<User?>(
future: event.redactedBecause?.fetchSenderUser(),
builder: (context, snapshot) {
final reason =
event.redactedBecause?.content.tryGet<String>('reason');
final redactedBy = snapshot.data?.calcDisplayname() ??
event.redactedBecause?.senderId.localpart ??
L10n.of(context).user;
return _ButtonContent(
label: reason == null
? L10n.of(context).redactedBy(redactedBy)
: L10n.of(context).redactedByBecause(
redactedBy,
reason,
),
icon: '🗑️',
textColor: buttonTextColor.withAlpha(128),
onPressed: () => onInfoTab!(event),
fontSize: fontSize,
);
},
);
}
var html = AppConfig.renderHtml && event.isRichMessage
? event.formattedText
: event.body;
if (event.messageType == MessageTypes.Emote) {
html = '* $html';
}
final bigEmotes = event.onlyEmotes &&
event.numberEmotes > 0 &&
event.numberEmotes <= 3;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: HtmlMessage(
html: html,
textColor: textColor,
room: event.room,
fontSize: AppConfig.fontSizeFactor *
AppConfig.messageFontSize *
(bigEmotes ? 5 : 1),
limitHeight: !selected,
linkStyle: TextStyle(
color: linkColor,
fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
eventId: event.eventId,
checkboxCheckedEvents: event.aggregatedEvents(
timeline,
EventCheckboxRoomExtension.relationshipType,
),
// #Pangea
event: event,
overlayController: overlayController,
controller: controller,
pangeaMessageEvent: pangeaMessageEvent,
nextEvent: nextEvent,
prevEvent: prevEvent,
isHighlighted: overlayController?.isTokenHighlighted,
isSelected: overlayController?.isTokenSelected,
onClick: event.isActivityMessage ? null : onClick,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
// Pangea#
),
);
}
case EventTypes.CallInvite:
return FutureBuilder<User?>(
future: event.fetchSenderUser(),
builder: (context, snapshot) {
return _ButtonContent(
label: L10n.of(context).startedACall(
snapshot.data?.calcDisplayname() ??
event.senderFromMemoryOrFallback.calcDisplayname(),
),
icon: '📞',
textColor: buttonTextColor,
onPressed: () => onInfoTab!(event),
fontSize: fontSize,
);
},
);
default:
return FutureBuilder<User?>(
future: event.fetchSenderUser(),
builder: (context, snapshot) {
return _ButtonContent(
label: L10n.of(context).userSentUnknownEvent(
snapshot.data?.calcDisplayname() ??
event.senderFromMemoryOrFallback.calcDisplayname(),
event.type,
),
icon: '',
textColor: buttonTextColor,
onPressed: () => onInfoTab!(event),
fontSize: fontSize,
);
},
);
}
}
}
class _ButtonContent extends StatelessWidget {
final void Function() onPressed;
final String label;
final String icon;
final Color? textColor;
final double fontSize;
const _ButtonContent({
required this.label,
required this.icon,
required this.textColor,
required this.onPressed,
required this.fontSize,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: InkWell(
onTap: onPressed,
child: Text(
'$icon $label',
style: TextStyle(
color: textColor,
fontSize: fontSize,
),
),
),
);
}
}