feat: Add option to not autoplay stickers and emotes

This commit is contained in:
Sorunome 2021-08-07 19:36:42 +02:00
parent e8376b56f1
commit 7443309b15
No known key found for this signature in database
GPG Key ID: B19471D07FC9BE9C
11 changed files with 125 additions and 33 deletions

View File

@ -23,6 +23,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"autoplayImages": "Automatically play animated stickers and emotes",
"@autoplayImages": {
"type": "text",
"placeholder": {}
},
"chats": "Chats", "chats": "Chats",
"@chats": { "@chats": {
"type": "text", "type": "text",

View File

@ -29,6 +29,7 @@ abstract class AppConfig {
static bool renderHtml = true; static bool renderHtml = true;
static bool hideRedactedEvents = false; static bool hideRedactedEvents = false;
static bool hideUnknownEvents = true; static bool hideUnknownEvents = true;
static bool autoplayImages = true;
static const bool hideTypingUsernames = false; static const bool hideTypingUsernames = false;
static const bool hideAllStateEvents = false; static const bool hideAllStateEvents = false;
static const String inviteLinkPrefix = 'https://matrix.to/#/'; static const String inviteLinkPrefix = 'https://matrix.to/#/';

View File

@ -20,4 +20,5 @@ abstract class SettingKeys {
static const String ownStatusMessage = 'chat.fluffy.status_msg'; static const String ownStatusMessage = 'chat.fluffy.status_msg';
static const String dontAskForBootstrapKey = static const String dontAskForBootstrapKey =
'chat.fluffychat.dont_ask_bootstrap'; 'chat.fluffychat.dont_ask_bootstrap';
static const String autoplayImages = 'chat.fluffy.autoplay_images';
} }

View File

@ -51,6 +51,7 @@ class ImageViewerView extends StatelessWidget {
maxSize: false, maxSize: false,
radius: 0.0, radius: 0.0,
thumbnailOnly: false, thumbnailOnly: false,
animated: true,
), ),
), ),
), ),

View File

@ -49,6 +49,12 @@ class SettingsChatView extends StatelessWidget {
storeKey: SettingKeys.hideUnknownEvents, storeKey: SettingKeys.hideUnknownEvents,
defaultValue: AppConfig.hideUnknownEvents, defaultValue: AppConfig.hideUnknownEvents,
), ),
SettingsSwitchListTile(
title: L10n.of(context).autoplayImages,
onChanged: (b) => AppConfig.autoplayImages = b,
storeKey: SettingKeys.autoplayImages,
defaultValue: AppConfig.autoplayImages,
),
], ],
), ),
), ),

View File

@ -3,8 +3,10 @@ import 'package:flutter_matrix_html/flutter_html.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../utils/url_launcher.dart'; import '../../utils/url_launcher.dart';
import '../../config/app_config.dart';
import '../../config/setting_keys.dart'; import '../../config/setting_keys.dart';
import '../../utils/matrix_sdk_extensions.dart/matrix_locals.dart'; import '../../utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import '../../pages/image_viewer.dart';
import '../matrix.dart'; import '../matrix.dart';
@ -64,10 +66,22 @@ class HtmlMessage extends StatelessWidget {
width: (width ?? 800) * ratio, width: (width ?? 800) * ratio,
height: (height ?? 800) * ratio, height: (height ?? 800) * ratio,
method: ThumbnailMethod.scale, method: ThumbnailMethod.scale,
animated: animated, animated: AppConfig.autoplayImages ? animated : false,
) )
.toString(); .toString();
}, },
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))),
setCodeLanguage: (String key, String value) async { setCodeLanguage: (String key, String value) async {
await matrix.store.setItem('${SettingKeys.codeLanguage}.$key', value); await matrix.store.setItem('${SettingKeys.codeLanguage}.$key', value);
}, },

View File

@ -21,6 +21,7 @@ class ImageBubble extends StatefulWidget {
final Color backgroundColor; final Color backgroundColor;
final double radius; final double radius;
final bool thumbnailOnly; final bool thumbnailOnly;
final bool animated;
final double width; final double width;
final double height; final double height;
final void Function() onLoaded; final void Function() onLoaded;
@ -37,6 +38,7 @@ class ImageBubble extends StatefulWidget {
this.onLoaded, this.onLoaded,
this.width = 400, this.width = 400,
this.height = 300, this.height = 300,
this.animated = false,
this.onTap, this.onTap,
Key key, Key key,
}) : super(key: key); }) : super(key: key);
@ -53,6 +55,9 @@ class _ImageBubbleState extends State<ImageBubble> {
MatrixFile _file; MatrixFile _file;
MatrixFile _thumbnail; MatrixFile _thumbnail;
bool _requestedThumbnailOnFailure = false; bool _requestedThumbnailOnFailure = false;
// In case we have animated = false, this will hold the first frame so that we make
// sure that things are never animated
Widget _firstFrame;
// the mimetypes that we know how to render, from the flutter Image widget // the mimetypes that we know how to render, from the flutter Image widget
final _knownMimetypes = <String>{ final _knownMimetypes = <String>{
@ -108,6 +113,19 @@ class _ImageBubbleState extends State<ImageBubble> {
} }
} }
Widget frameBuilder(
BuildContext context, Widget child, int frame, bool sync) {
// as servers might return animated gifs as thumbnails and we want them to *not* play
// animated, we'll have to store the first frame in a variable and display that instead
if (widget.animated) {
return child;
}
if (frame == 0) {
_firstFrame = child;
}
return _firstFrame ?? child;
}
@override @override
void initState() { void initState() {
// add the custom renderers for other mimetypes // add the custom renderers for other mimetypes
@ -131,6 +149,7 @@ class _ImageBubbleState extends State<ImageBubble> {
fit: widget.fit, fit: widget.fit,
errorBuilder: (context, error, stacktrace) => errorBuilder: (context, error, stacktrace) =>
getErrorWidget(context, error), getErrorWidget(context, error),
animate: widget.animated,
), ),
network: (String url) => Lottie.network( network: (String url) => Lottie.network(
url, url,
@ -138,6 +157,7 @@ class _ImageBubbleState extends State<ImageBubble> {
fit: widget.fit, fit: widget.fit,
errorBuilder: (context, error, stacktrace) => errorBuilder: (context, error, stacktrace) =>
getErrorWidget(context, error), getErrorWidget(context, error),
animate: widget.animated,
), ),
); );
@ -147,9 +167,10 @@ class _ImageBubbleState extends State<ImageBubble> {
} }
thumbnailUrl = widget.event thumbnailUrl = widget.event
.getAttachmentUrl(getThumbnail: true, animated: true) .getAttachmentUrl(getThumbnail: true, animated: widget.animated)
?.toString(); ?.toString();
attachmentUrl = widget.event.getAttachmentUrl(animated: true)?.toString(); attachmentUrl =
widget.event.getAttachmentUrl(animated: widget.animated)?.toString();
if (thumbnailUrl == null) { if (thumbnailUrl == null) {
_requestFile(getThumbnail: true); _requestFile(getThumbnail: true);
} }
@ -263,6 +284,7 @@ class _ImageBubbleState extends State<ImageBubble> {
} }
return getErrorWidget(context, error); return getErrorWidget(context, error);
}, },
frameBuilder: frameBuilder,
); );
} }
} }
@ -292,11 +314,20 @@ class _ImageBubbleState extends State<ImageBubble> {
key: ValueKey(thumbnailUrl), key: ValueKey(thumbnailUrl),
imageUrl: thumbnailUrl, imageUrl: thumbnailUrl,
placeholder: (c, u) => getPlaceholderWidget(), placeholder: (c, u) => getPlaceholderWidget(),
fit: widget.fit, imageBuilder: (context, imageProvider) => Image(
image: imageProvider,
frameBuilder: frameBuilder,
fit: widget.fit,
),
); );
} }
return getPlaceholderWidget(); return getPlaceholderWidget();
}, },
imageBuilder: (context, imageProvider) => Image(
image: imageProvider,
frameBuilder: frameBuilder,
fit: widget.fit,
),
errorWidget: (context, url, error) { errorWidget: (context, url, error) {
if (widget.event.hasThumbnail && !_requestedThumbnailOnFailure) { if (widget.event.hasThumbnail && !_requestedThumbnailOnFailure) {
// the image failed to load but the event has a thumbnail attached....so we can // the image failed to load but the event has a thumbnail attached....so we can
@ -308,10 +339,11 @@ class _ImageBubbleState extends State<ImageBubble> {
.getAttachmentUrl( .getAttachmentUrl(
getThumbnail: true, getThumbnail: true,
useThumbnailMxcUrl: true, useThumbnailMxcUrl: true,
animated: true) animated: widget.animated)
?.toString(); ?.toString();
attachmentUrl = widget.event attachmentUrl = widget.event
.getAttachmentUrl(useThumbnailMxcUrl: true, animated: true) .getAttachmentUrl(
useThumbnailMxcUrl: true, animated: widget.animated)
?.toString(); ?.toString();
}); });
}); });
@ -346,7 +378,6 @@ class _ImageBubbleState extends State<ImageBubble> {
} }
return getErrorWidget(context, error); return getErrorWidget(context, error);
}, },
fit: widget.fit,
); );
} }
} }

View File

@ -6,7 +6,6 @@ import 'package:fluffychat/widgets/event_content/image_bubble.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/pages/key_verification_dialog.dart'; import 'package:fluffychat/pages/key_verification_dialog.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -21,6 +20,7 @@ import 'html_message.dart';
import '../matrix.dart'; import '../matrix.dart';
import 'message_download_content.dart'; import 'message_download_content.dart';
import 'map_bubble.dart'; import 'map_bubble.dart';
import 'sticker.dart';
class MessageContent extends StatelessWidget { class MessageContent extends StatelessWidget {
final Event event; final Event event;
@ -91,30 +91,7 @@ class MessageContent extends StatelessWidget {
return MessageDownloadContent(event, textColor); return MessageDownloadContent(event, textColor);
case MessageTypes.Sticker: case MessageTypes.Sticker:
if (event.showThumbnail) { if (event.showThumbnail) {
// stickers should default to a ratio of 1:1 return Sticker(event);
var ratio = 1.0;
// if a width and a height is specified for stickers, use those!
if (event.infoMap['w'] is int && event.infoMap['h'] is int) {
ratio = event.infoMap['w'] / event.infoMap['h'];
// make sure the ratio is within 0.9 - 2.0
if (ratio > 2.0) {
ratio = 2.0;
}
if (ratio < 0.9) {
ratio = 0.9;
}
}
return ImageBubble(
event,
width: 400,
height: 400 / ratio,
fit: ratio <= 1.0 ? BoxFit.contain : BoxFit.cover,
onTap: () => showOkAlertDialog(
context: context,
message: event.body,
okLabel: L10n.of(context).ok,
),
);
} }
return MessageDownloadContent(event, textColor); return MessageDownloadContent(event, textColor);
case MessageTypes.Audio: case MessageTypes.Audio:

View File

@ -0,0 +1,51 @@
import 'package:matrix/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import '../../config/app_config.dart';
import 'image_bubble.dart';
class Sticker extends StatefulWidget {
final Event event;
const Sticker(this.event, {Key key}) : super(key: key);
@override
_StickerState createState() => _StickerState();
}
class _StickerState extends State<Sticker> {
bool animated;
@override
Widget build(BuildContext context) {
// stickers should default to a ratio of 1:1
var ratio = 1.0;
// if a width and a height is specified for stickers, use those!
if (widget.event.infoMap['w'] is int && widget.event.infoMap['h'] is int) {
ratio = widget.event.infoMap['w'] / widget.event.infoMap['h'];
// make sure the ratio is within 0.9 - 2.0
if (ratio > 2.0) {
ratio = 2.0;
}
if (ratio < 0.9) {
ratio = 0.9;
}
}
return ImageBubble(
widget.event,
width: 400,
height: 400 / ratio,
fit: ratio <= 1.0 ? BoxFit.contain : BoxFit.cover,
onTap: () {
setState(() => animated = true);
showOkAlertDialog(
context: context,
message: widget.event.body,
okLabel: L10n.of(context).ok,
);
},
animated: animated ?? AppConfig.autoplayImages,
);
}
}

View File

@ -414,6 +414,9 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
.getItemBool( .getItemBool(
SettingKeys.hideUnknownEvents, AppConfig.hideUnknownEvents) SettingKeys.hideUnknownEvents, AppConfig.hideUnknownEvents)
.then((value) => AppConfig.hideUnknownEvents = value); .then((value) => AppConfig.hideUnknownEvents = value);
store
.getItemBool(SettingKeys.autoplayImages, AppConfig.autoplayImages)
.then((value) => AppConfig.autoplayImages = value);
} }
} }

View File

@ -24,7 +24,9 @@ class _SettingsSwitchListTileState extends State<SettingsSwitchListTile> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<bool>( return FutureBuilder<bool>(
future: Matrix.of(context).store.getItemBool(widget.storeKey), future: Matrix.of(context)
.store
.getItemBool(widget.storeKey, widget.defaultValue),
builder: (context, snapshot) => SwitchListTile( builder: (context, snapshot) => SwitchListTile(
value: snapshot.data ?? widget.defaultValue, value: snapshot.data ?? widget.defaultValue,
title: Text(widget.title), title: Text(widget.title),