feat: Add option to not autoplay stickers and emotes
This commit is contained in:
parent
e8376b56f1
commit
7443309b15
|
@ -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",
|
||||||
|
|
|
@ -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/#/';
|
||||||
|
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ class ImageViewerView extends StatelessWidget {
|
||||||
maxSize: false,
|
maxSize: false,
|
||||||
radius: 0.0,
|
radius: 0.0,
|
||||||
thumbnailOnly: false,
|
thumbnailOnly: false,
|
||||||
|
animated: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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(),
|
||||||
|
imageBuilder: (context, imageProvider) => Image(
|
||||||
|
image: imageProvider,
|
||||||
|
frameBuilder: frameBuilder,
|
||||||
fit: widget.fit,
|
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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in New Issue