mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2025-02-07 02:34:17 +01:00
feat: add animated emoji support
- implement animated emoji support in both HTML and Linkify message type - fix some missing font glyphs - trim message input Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
This commit is contained in:
parent
1911004d05
commit
2e87050544
@ -2449,6 +2449,8 @@
|
|||||||
"oldDisplayName": {}
|
"oldDisplayName": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"autoplayAnimations": "Automatically play animations",
|
||||||
|
"defaultEmojiTone": "Default emoji tone",
|
||||||
"newSpaceDescription": "Spaces allows you to consolidate your chats and build private or public communities.",
|
"newSpaceDescription": "Spaces allows you to consolidate your chats and build private or public communities.",
|
||||||
"encryptThisChat": "Encrypt this chat",
|
"encryptThisChat": "Encrypt this chat",
|
||||||
"endToEndEncryption": "End to end encryption",
|
"endToEndEncryption": "End to end encryption",
|
||||||
|
BIN
fonts/NotoColorEmojiCompat.ttf
Normal file
BIN
fonts/NotoColorEmojiCompat.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
fonts/NotoSansSymbols-VariableFont_wght.ttf
Normal file
BIN
fonts/NotoSansSymbols-VariableFont_wght.ttf
Normal file
Binary file not shown.
@ -47,7 +47,6 @@ abstract class AppConfig {
|
|||||||
static bool hideUnimportantStateEvents = true;
|
static bool hideUnimportantStateEvents = true;
|
||||||
static bool showDirectChatsInSpaces = true;
|
static bool showDirectChatsInSpaces = true;
|
||||||
static bool separateChatTypes = false;
|
static bool separateChatTypes = false;
|
||||||
static bool autoplayImages = true;
|
|
||||||
static bool sendOnEnter = false;
|
static bool sendOnEnter = false;
|
||||||
static bool experimentalVoip = false;
|
static bool experimentalVoip = false;
|
||||||
static const bool hideTypingUsernames = false;
|
static const bool hideTypingUsernames = false;
|
||||||
@ -63,7 +62,7 @@ abstract class AppConfig {
|
|||||||
static const String pushNotificationsGatewayUrl =
|
static const String pushNotificationsGatewayUrl =
|
||||||
'https://push.fluffychat.im/_matrix/push/v1/notify';
|
'https://push.fluffychat.im/_matrix/push/v1/notify';
|
||||||
static const String pushNotificationsPusherFormat = 'event_id_only';
|
static const String pushNotificationsPusherFormat = 'event_id_only';
|
||||||
static const String emojiFontName = 'Noto Emoji';
|
static const String emojiFontName = 'Noto Color Emoji';
|
||||||
static const String emojiFontUrl =
|
static const String emojiFontUrl =
|
||||||
'https://github.com/googlefonts/noto-emoji/';
|
'https://github.com/googlefonts/noto-emoji/';
|
||||||
static const double borderRadius = 16.0;
|
static const double borderRadius = 16.0;
|
||||||
|
@ -22,7 +22,7 @@ abstract class FluffyThemes {
|
|||||||
|
|
||||||
static const fallbackTextStyle = TextStyle(
|
static const fallbackTextStyle = TextStyle(
|
||||||
fontFamily: 'Roboto',
|
fontFamily: 'Roboto',
|
||||||
fontFamilyFallback: ['NotoEmoji'],
|
fontFamilyFallback: [AppConfig.emojiFontName],
|
||||||
);
|
);
|
||||||
|
|
||||||
static var fallbackTextTheme = const TextTheme(
|
static var fallbackTextTheme = const TextTheme(
|
||||||
|
@ -142,7 +142,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
minLines: 2,
|
minLines: 2,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
style: const TextStyle(fontFamily: 'RobotoMono'),
|
style: const TextStyle(fontFamily: 'Roboto Mono'),
|
||||||
controller: TextEditingController(text: key),
|
controller: TextEditingController(text: key),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
contentPadding: EdgeInsets.all(16),
|
contentPadding: EdgeInsets.all(16),
|
||||||
@ -272,7 +272,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
? null
|
? null
|
||||||
: [AutofillHints.password],
|
: [AutofillHints.password],
|
||||||
controller: _recoveryKeyTextEditingController,
|
controller: _recoveryKeyTextEditingController,
|
||||||
style: const TextStyle(fontFamily: 'RobotoMono'),
|
style: const TextStyle(fontFamily: 'Roboto Mono'),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: const EdgeInsets.all(16),
|
contentPadding: const EdgeInsets.all(16),
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
|
@ -423,7 +423,7 @@ class ChatController extends State<ChatPageWithRoom> {
|
|||||||
|
|
||||||
// ignore: unawaited_futures
|
// ignore: unawaited_futures
|
||||||
room.sendTextEvent(
|
room.sendTextEvent(
|
||||||
sendController.text,
|
sendController.text.trim(),
|
||||||
inReplyTo: replyEvent,
|
inReplyTo: replyEvent,
|
||||||
editEventId: editEvent?.eventId,
|
editEventId: editEvent?.eventId,
|
||||||
parseCommands: parseCommands,
|
parseCommands: parseCommands,
|
||||||
|
@ -1,16 +1,24 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.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 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/pages/settings_chat/settings_chat.dart';
|
||||||
|
import 'package:fluffychat/widgets/animated_emoji_plain_text.dart';
|
||||||
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
||||||
class CuteContent extends StatefulWidget {
|
class CuteContent extends StatefulWidget {
|
||||||
final Event event;
|
final Event event;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
const CuteContent(this.event, {Key? key}) : super(key: key);
|
const CuteContent(
|
||||||
|
this.event, {
|
||||||
|
Key? key,
|
||||||
|
required this.color,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CuteContent> createState() => _CuteContentState();
|
State<CuteContent> createState() => _CuteContentState();
|
||||||
@ -18,17 +26,18 @@ class CuteContent extends StatefulWidget {
|
|||||||
|
|
||||||
class _CuteContentState extends State<CuteContent> {
|
class _CuteContentState extends State<CuteContent> {
|
||||||
static bool _isOverlayShown = false;
|
static bool _isOverlayShown = false;
|
||||||
|
bool initialized = false;
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
if (AppConfig.autoplayImages && !_isOverlayShown) {
|
|
||||||
addOverlay();
|
|
||||||
}
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (initialized == false) {
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
if (Matrix.of(context).client.autoplayAnimatedContent ??
|
||||||
|
!kIsWeb && !_isOverlayShown) {
|
||||||
|
addOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
return FutureBuilder<User?>(
|
return FutureBuilder<User?>(
|
||||||
future: widget.event.fetchSenderUser(),
|
future: widget.event.fetchSenderUser(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
@ -40,9 +49,10 @@ class _CuteContentState extends State<CuteContent> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
TextLinkifyEmojify(
|
||||||
widget.event.text,
|
widget.event.text,
|
||||||
style: const TextStyle(fontSize: 150),
|
fontSize: 150,
|
||||||
|
textColor: widget.color,
|
||||||
),
|
),
|
||||||
if (label != null) Text(label)
|
if (label != null) Text(label)
|
||||||
],
|
],
|
||||||
@ -183,11 +193,14 @@ class _CuteOverlayContent extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox.square(
|
return SizedOverflowBox(
|
||||||
dimension: size,
|
size: const Size.square(size),
|
||||||
child: Text(
|
child: ClipRect(
|
||||||
emoji,
|
clipBehavior: Clip.hardEdge,
|
||||||
style: const TextStyle(fontSize: 48),
|
child: Text(
|
||||||
|
emoji,
|
||||||
|
style: const TextStyle(fontSize: 56),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' hide Element;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:dart_animated_emoji/dart_animated_emoji.dart';
|
||||||
import 'package:flutter_highlighter/flutter_highlighter.dart';
|
import 'package:flutter_highlighter/flutter_highlighter.dart';
|
||||||
import 'package:flutter_highlighter/themes/shades-of-purple.dart';
|
import 'package:flutter_highlighter/themes/shades-of-purple.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
@ -10,6 +11,7 @@ import 'package:linkify/linkify.dart';
|
|||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
|
import 'package:fluffychat/widgets/animated_emoji_plain_text.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||||
import '../../../utils/url_launcher.dart';
|
import '../../../utils/url_launcher.dart';
|
||||||
@ -18,12 +20,14 @@ class HtmlMessage extends StatelessWidget {
|
|||||||
final String html;
|
final String html;
|
||||||
final Room room;
|
final Room room;
|
||||||
final Color textColor;
|
final Color textColor;
|
||||||
|
final bool isEmojiOnly;
|
||||||
|
|
||||||
const HtmlMessage({
|
const HtmlMessage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.html,
|
required this.html,
|
||||||
required this.room,
|
required this.room,
|
||||||
this.textColor = Colors.black,
|
this.textColor = Colors.black,
|
||||||
|
this.isEmojiOnly = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -44,7 +48,9 @@ class HtmlMessage extends StatelessWidget {
|
|||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
|
|
||||||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
double fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||||
|
|
||||||
|
if (isEmojiOnly) fontSize *= 3;
|
||||||
|
|
||||||
final linkifiedRenderHtml = linkify(
|
final linkifiedRenderHtml = linkify(
|
||||||
renderHtml,
|
renderHtml,
|
||||||
@ -64,11 +70,21 @@ class HtmlMessage extends StatelessWidget {
|
|||||||
.join('')
|
.join('')
|
||||||
.replaceAll('\n', '');
|
.replaceAll('\n', '');
|
||||||
|
|
||||||
|
final emojifiedHtml = linkifiedRenderHtml.replaceAllMapped(
|
||||||
|
RegExp(
|
||||||
|
'(${AnimatedEmoji.all.reversed.map((e) => e.fallback).join('|')})',
|
||||||
|
),
|
||||||
|
(match) {
|
||||||
|
final emoji = linkifiedRenderHtml.substring(match.start, match.end);
|
||||||
|
return '<span data-fluffy-animated-emoji="$emoji">$emoji</span>';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final linkColor = textColor.withAlpha(150);
|
final linkColor = textColor.withAlpha(150);
|
||||||
|
|
||||||
// there is no need to pre-validate the html, as we validate it while rendering
|
// there is no need to pre-validate the html, as we validate it while rendering
|
||||||
return Html(
|
return Html(
|
||||||
data: linkifiedRenderHtml,
|
data: emojifiedHtml,
|
||||||
style: {
|
style: {
|
||||||
'*': Style(
|
'*': Style(
|
||||||
color: textColor,
|
color: textColor,
|
||||||
@ -136,8 +152,15 @@ class HtmlMessage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const TableHtmlExtension(),
|
const TableHtmlExtension(),
|
||||||
SpoilerExtension(textColor: textColor),
|
SpoilerExtension(textColor: textColor),
|
||||||
const ImageExtension(),
|
ImageExtension(
|
||||||
|
isEmojiOnly: isEmojiOnly,
|
||||||
|
watermarkColor: textColor,
|
||||||
|
),
|
||||||
FontColorExtension(),
|
FontColorExtension(),
|
||||||
|
AnimatedEmojiExtension(
|
||||||
|
isEmojiOnly: isEmojiOnly,
|
||||||
|
defaultTextColor: textColor,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
onLinkTap: (url, _, __) => UrlLauncher(context, url).launchUrl(),
|
onLinkTap: (url, _, __) => UrlLauncher(context, url).launchUrl(),
|
||||||
onlyRenderTheseTags: const {
|
onlyRenderTheseTags: const {
|
||||||
@ -245,8 +268,14 @@ class FontColorExtension extends HtmlExtension {
|
|||||||
|
|
||||||
class ImageExtension extends HtmlExtension {
|
class ImageExtension extends HtmlExtension {
|
||||||
final double defaultDimension;
|
final double defaultDimension;
|
||||||
|
final bool isEmojiOnly;
|
||||||
|
final Color watermarkColor;
|
||||||
|
|
||||||
const ImageExtension({this.defaultDimension = 64});
|
const ImageExtension({
|
||||||
|
this.defaultDimension = 64,
|
||||||
|
this.isEmojiOnly = false,
|
||||||
|
required this.watermarkColor,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<String> get supportedTags => {'img'};
|
Set<String> get supportedTags => {'img'};
|
||||||
@ -258,18 +287,34 @@ class ImageExtension extends HtmlExtension {
|
|||||||
return TextSpan(text: context.attributes['alt']);
|
return TextSpan(text: context.attributes['alt']);
|
||||||
}
|
}
|
||||||
|
|
||||||
final width = double.tryParse(context.attributes['width'] ?? '');
|
double? width, height;
|
||||||
final height = double.tryParse(context.attributes['height'] ?? '');
|
|
||||||
|
|
||||||
|
// in case it's an emoji only message or a custom emoji image,
|
||||||
|
// force the default font size
|
||||||
|
if (isEmojiOnly) {
|
||||||
|
width = height =
|
||||||
|
AppConfig.messageFontSize * AppConfig.fontSizeFactor * 3 * 1.2;
|
||||||
|
} else if (context.attributes.containsKey('data-mx-emoticon') ||
|
||||||
|
context.attributes.containsKey('data-mx-emoji')) {
|
||||||
|
// in case the image is a custom emote, get the surrounding font size
|
||||||
|
width = height = (tryGetParentFontSize(context) ??
|
||||||
|
FontSize(AppConfig.messageFontSize * AppConfig.fontSizeFactor))
|
||||||
|
.emValue;
|
||||||
|
} else {
|
||||||
|
width = double.tryParse(context.attributes['width'] ?? '');
|
||||||
|
height = double.tryParse(context.attributes['height'] ?? '');
|
||||||
|
}
|
||||||
return WidgetSpan(
|
return WidgetSpan(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: width ?? height ?? defaultDimension,
|
width: width ?? height ?? defaultDimension,
|
||||||
height: height ?? width ?? defaultDimension,
|
height: height ?? width ?? defaultDimension,
|
||||||
child: MxcImage(
|
child: MxcImage(
|
||||||
|
watermarkSize: (width ?? height ?? defaultDimension) / 2.5,
|
||||||
uri: mxcUrl,
|
uri: mxcUrl,
|
||||||
width: width ?? height ?? defaultDimension,
|
width: width ?? height ?? defaultDimension,
|
||||||
height: height ?? width ?? defaultDimension,
|
height: height ?? width ?? defaultDimension,
|
||||||
cacheKey: mxcUrl.toString(),
|
cacheKey: mxcUrl.toString(),
|
||||||
|
watermarkColor: watermarkColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -321,6 +366,7 @@ class MatrixMathExtension extends HtmlExtension {
|
|||||||
final TextStyle? style;
|
final TextStyle? style;
|
||||||
|
|
||||||
MatrixMathExtension({this.style});
|
MatrixMathExtension({this.style});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<String> get supportedTags => {'div'};
|
Set<String> get supportedTags => {'div'};
|
||||||
|
|
||||||
@ -350,10 +396,65 @@ class MatrixMathExtension extends HtmlExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AnimatedEmojiExtension extends HtmlExtension {
|
||||||
|
final bool isEmojiOnly;
|
||||||
|
final Color defaultTextColor;
|
||||||
|
|
||||||
|
const AnimatedEmojiExtension({
|
||||||
|
this.isEmojiOnly = false,
|
||||||
|
required this.defaultTextColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<String> get supportedTags => {'span'};
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool matches(ExtensionContext context) {
|
||||||
|
if (context.elementName != 'span') return false;
|
||||||
|
final emojiData = context.element?.attributes['data-fluffy-animated-emoji'];
|
||||||
|
return emojiData != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
InlineSpan build(
|
||||||
|
ExtensionContext context,
|
||||||
|
) {
|
||||||
|
final emojiText = context.element?.innerHtml;
|
||||||
|
try {
|
||||||
|
final emoji = AnimatedEmoji.all.firstWhere(
|
||||||
|
(element) => element.fallback == emojiText,
|
||||||
|
);
|
||||||
|
|
||||||
|
double size;
|
||||||
|
|
||||||
|
// in case it's an emoji only message, we can use the default emoji-only
|
||||||
|
// font size
|
||||||
|
if (isEmojiOnly) {
|
||||||
|
size = AppConfig.messageFontSize * AppConfig.fontSizeFactor * 3 * 1.125;
|
||||||
|
} else {
|
||||||
|
// otherwise try to gather the parenting element's font size.
|
||||||
|
final fontSize = (tryGetParentFontSize(context) ??
|
||||||
|
FontSize(AppConfig.messageFontSize * AppConfig.fontSizeFactor));
|
||||||
|
size = fontSize.emValue * 1.125;
|
||||||
|
}
|
||||||
|
return WidgetSpan(
|
||||||
|
child: AnimatedEmojiLottieView(
|
||||||
|
emoji: emoji,
|
||||||
|
size: size,
|
||||||
|
textColor: defaultTextColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return TextSpan(text: emojiText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CodeExtension extends HtmlExtension {
|
class CodeExtension extends HtmlExtension {
|
||||||
final double fontSize;
|
final double fontSize;
|
||||||
|
|
||||||
CodeExtension({required this.fontSize});
|
CodeExtension({required this.fontSize});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<String> get supportedTags => {'code'};
|
Set<String> get supportedTags => {'code'};
|
||||||
|
|
||||||
@ -391,6 +492,7 @@ class RoomPillExtension extends HtmlExtension {
|
|||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
|
|
||||||
RoomPillExtension(this.context, this.room);
|
RoomPillExtension(this.context, this.room);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<String> get supportedTags => {'a'};
|
Set<String> get supportedTags => {'a'};
|
||||||
|
|
||||||
@ -502,3 +604,15 @@ class MatrixPill extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FontSize? tryGetParentFontSize(ExtensionContext context) {
|
||||||
|
var currentElement = context.element;
|
||||||
|
while (currentElement?.parent != null) {
|
||||||
|
currentElement = currentElement?.parent;
|
||||||
|
final size = context.parser.style[(currentElement!.localName!)]?.fontSize;
|
||||||
|
if (size != null) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
@ -14,13 +14,14 @@ class ImageBubble extends StatelessWidget {
|
|||||||
final bool maxSize;
|
final bool maxSize;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
final bool thumbnailOnly;
|
final bool thumbnailOnly;
|
||||||
final bool animated;
|
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final void Function()? onTap;
|
final void Function()? onTap;
|
||||||
|
final Color? watermarkColor;
|
||||||
|
|
||||||
const ImageBubble(
|
const ImageBubble(
|
||||||
this.event, {
|
this.event, {
|
||||||
|
Key? key,
|
||||||
this.tapToView = true,
|
this.tapToView = true,
|
||||||
this.maxSize = true,
|
this.maxSize = true,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
@ -28,9 +29,8 @@ class ImageBubble extends StatelessWidget {
|
|||||||
this.thumbnailOnly = true,
|
this.thumbnailOnly = true,
|
||||||
this.width = 400,
|
this.width = 400,
|
||||||
this.height = 300,
|
this.height = 300,
|
||||||
this.animated = false,
|
|
||||||
this.onTap,
|
this.onTap,
|
||||||
Key? key,
|
this.watermarkColor,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
Widget _buildPlaceholder(BuildContext context) {
|
Widget _buildPlaceholder(BuildContext context) {
|
||||||
@ -94,13 +94,16 @@ class ImageBubble extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
child: MxcImage(
|
child: MxcImage(
|
||||||
|
key: ValueKey(event.eventId),
|
||||||
event: event,
|
event: event,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
fit: fit,
|
fit: fit,
|
||||||
animated: animated,
|
disableTapHandler: true,
|
||||||
isThumbnail: thumbnailOnly,
|
isThumbnail: thumbnailOnly,
|
||||||
placeholder: _buildPlaceholder,
|
placeholder: _buildPlaceholder,
|
||||||
|
watermarkSize: width / 2.5,
|
||||||
|
watermarkColor: watermarkColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:emoji_regex/emoji_regex.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pages/chat/events/video_player.dart';
|
import 'package:fluffychat/pages/chat/events/video_player.dart';
|
||||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
|
import 'package:fluffychat/widgets/animated_emoji_plain_text.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
import '../../../config/app_config.dart';
|
import '../../../config/app_config.dart';
|
||||||
@ -112,12 +113,16 @@ class MessageContent extends StatelessWidget {
|
|||||||
width: 400,
|
width: 400,
|
||||||
height: 300,
|
height: 300,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
watermarkColor: textColor,
|
||||||
);
|
);
|
||||||
case MessageTypes.Sticker:
|
case MessageTypes.Sticker:
|
||||||
if (event.redacted) continue textmessage;
|
if (event.redacted) continue textmessage;
|
||||||
return Sticker(event);
|
return Sticker(
|
||||||
|
event,
|
||||||
|
watermarkColor: textColor,
|
||||||
|
);
|
||||||
case CuteEventContent.eventType:
|
case CuteEventContent.eventType:
|
||||||
return CuteContent(event);
|
return CuteContent(event, color: textColor);
|
||||||
case MessageTypes.Audio:
|
case MessageTypes.Audio:
|
||||||
if (PlatformInfos.isMobile ||
|
if (PlatformInfos.isMobile ||
|
||||||
PlatformInfos.isMacOS ||
|
PlatformInfos.isMacOS ||
|
||||||
@ -154,6 +159,7 @@ class MessageContent extends StatelessWidget {
|
|||||||
html: html,
|
html: html,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
room: event.room,
|
room: event.room,
|
||||||
|
isEmojiOnly: event.onlyEmotes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// else we fall through to the normal message rendering
|
// else we fall through to the normal message rendering
|
||||||
@ -220,7 +226,12 @@ class MessageContent extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final bigEmotes = event.onlyEmotes &&
|
final bigEmotes = (event.onlyEmotes ||
|
||||||
|
emojiRegex()
|
||||||
|
.allMatches(event.text)
|
||||||
|
.map((e) => e[0])
|
||||||
|
.join() ==
|
||||||
|
event.text) &&
|
||||||
event.numberEmotes > 0 &&
|
event.numberEmotes > 0 &&
|
||||||
event.numberEmotes <= 10;
|
event.numberEmotes <= 10;
|
||||||
return FutureBuilder<String>(
|
return FutureBuilder<String>(
|
||||||
@ -229,26 +240,17 @@ class MessageContent extends StatelessWidget {
|
|||||||
hideReply: true,
|
hideReply: true,
|
||||||
),
|
),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return Linkify(
|
final text = snapshot.data ??
|
||||||
text: snapshot.data ??
|
event.calcLocalizedBodyFallback(
|
||||||
event.calcLocalizedBodyFallback(
|
MatrixLocals(L10n.of(context)!),
|
||||||
MatrixLocals(L10n.of(context)!),
|
hideReply: true,
|
||||||
hideReply: true,
|
);
|
||||||
),
|
return TextLinkifyEmojify(
|
||||||
style: TextStyle(
|
text,
|
||||||
color: textColor,
|
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
||||||
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
textDecoration:
|
||||||
decoration:
|
event.redacted ? TextDecoration.lineThrough : null,
|
||||||
event.redacted ? TextDecoration.lineThrough : null,
|
textColor: textColor,
|
||||||
),
|
|
||||||
options: const LinkifyOptions(humanize: false),
|
|
||||||
linkStyle: TextStyle(
|
|
||||||
color: textColor.withAlpha(150),
|
|
||||||
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
decorationColor: textColor.withAlpha(150),
|
|
||||||
),
|
|
||||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -125,6 +125,8 @@ class _Reaction extends StatelessWidget {
|
|||||||
uri: Uri.parse(reactionKey!),
|
uri: Uri.parse(reactionKey!),
|
||||||
width: 9999,
|
width: 9999,
|
||||||
height: fontSize,
|
height: fontSize,
|
||||||
|
watermarkColor: color,
|
||||||
|
watermarkSize: (fontSize ?? 12) / 1.5,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
|
@ -4,37 +4,30 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
|
|||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import '../../../config/app_config.dart';
|
|
||||||
import 'image_bubble.dart';
|
import 'image_bubble.dart';
|
||||||
|
|
||||||
class Sticker extends StatefulWidget {
|
class Sticker extends StatelessWidget {
|
||||||
final Event event;
|
final Event event;
|
||||||
|
final Color watermarkColor;
|
||||||
|
|
||||||
const Sticker(this.event, {Key? key}) : super(key: key);
|
const Sticker(this.event, {Key? key, required this.watermarkColor})
|
||||||
|
: super(key: key);
|
||||||
@override
|
|
||||||
StickerState createState() => StickerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class StickerState extends State<Sticker> {
|
|
||||||
bool? animated;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ImageBubble(
|
return ImageBubble(
|
||||||
widget.event,
|
event,
|
||||||
width: 400,
|
width: 400,
|
||||||
height: 400,
|
height: 400,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() => animated = true);
|
|
||||||
showOkAlertDialog(
|
showOkAlertDialog(
|
||||||
context: context,
|
context: context,
|
||||||
message: widget.event.body,
|
message: event.body,
|
||||||
okLabel: L10n.of(context)!.ok,
|
okLabel: L10n.of(context)!.ok,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
animated: animated ?? AppConfig.autoplayImages,
|
watermarkColor: watermarkColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import 'package:matrix/matrix.dart';
|
|||||||
import 'package:slugify/slugify.dart';
|
import 'package:slugify/slugify.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
|
import 'package:fluffychat/pages/settings_emotes/settings_emotes.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||||
import '../../widgets/avatar.dart';
|
import '../../widgets/avatar.dart';
|
||||||
@ -44,7 +45,10 @@ class InputBar extends StatelessWidget {
|
|||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
List<Map<String, String?>> getSuggestions(String text) {
|
List<Map<String, String?>> getSuggestions(
|
||||||
|
String text, {
|
||||||
|
fitzpatrick tone = fitzpatrick.None,
|
||||||
|
}) {
|
||||||
if (controller!.selection.baseOffset !=
|
if (controller!.selection.baseOffset !=
|
||||||
controller!.selection.extentOffset ||
|
controller!.selection.extentOffset ||
|
||||||
controller!.selection.baseOffset < 0) {
|
controller!.selection.baseOffset < 0) {
|
||||||
@ -116,12 +120,28 @@ class InputBar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// aside of emote packs, also propose normal (tm) unicode emojis
|
// aside of emote packs, also propose normal (tm) unicode emojis
|
||||||
final matchingUnicodeEmojis = Emoji.all()
|
|
||||||
.where(
|
final matchingUnicodeEmojis = List.from(
|
||||||
(element) => [element.name, ...element.keywords]
|
Emoji.all()
|
||||||
.any((element) => element.toLowerCase().contains(emoteSearch)),
|
// filter out duplicate skins in order to reduce the list length
|
||||||
)
|
.where(
|
||||||
.toList();
|
(element) => [
|
||||||
|
element.name,
|
||||||
|
...element.keywords,
|
||||||
|
].any(
|
||||||
|
(element) => element.toLowerCase().contains(emoteSearch),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// shorten the list by reducing redundant skin tones
|
||||||
|
.map((e) {
|
||||||
|
try {
|
||||||
|
// TODO: find a way to filter out different hair colors
|
||||||
|
return e.newSkin(tone);
|
||||||
|
} catch (_) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}).toSet(),
|
||||||
|
);
|
||||||
// sort by the index of the search term in the name in order to have
|
// sort by the index of the search term in the name in order to have
|
||||||
// best matches first
|
// best matches first
|
||||||
// (thanks for the hint by github.com/nextcloud/circles devs)
|
// (thanks for the hint by github.com/nextcloud/circles devs)
|
||||||
@ -394,6 +414,7 @@ class InputBar extends StatelessWidget {
|
|||||||
final useShortCuts = (PlatformInfos.isWeb ||
|
final useShortCuts = (PlatformInfos.isWeb ||
|
||||||
PlatformInfos.isDesktop ||
|
PlatformInfos.isDesktop ||
|
||||||
AppConfig.sendOnEnter);
|
AppConfig.sendOnEnter);
|
||||||
|
final tone = Matrix.of(context).client.defaultEmojiTone;
|
||||||
return Shortcuts(
|
return Shortcuts(
|
||||||
shortcuts: !useShortCuts
|
shortcuts: !useShortCuts
|
||||||
? {}
|
? {}
|
||||||
@ -456,7 +477,7 @@ class InputBar extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
textCapitalization: TextCapitalization.sentences,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
),
|
),
|
||||||
suggestionsCallback: getSuggestions,
|
suggestionsCallback: (q) => getSuggestions(q, tone: tone),
|
||||||
itemBuilder: (c, s) =>
|
itemBuilder: (c, s) =>
|
||||||
buildSuggestion(c, s, Matrix.of(context).client),
|
buildSuggestion(c, s, Matrix.of(context).client),
|
||||||
onSuggestionSelected: (Map<String, String?> suggestion) =>
|
onSuggestionSelected: (Map<String, String?> suggestion) =>
|
||||||
|
@ -4,13 +4,12 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/pages/chat/chat.dart';
|
import 'package:fluffychat/pages/chat/chat.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
import 'package:fluffychat/utils/url_launcher.dart';
|
import 'package:fluffychat/widgets/animated_emoji_plain_text.dart';
|
||||||
|
|
||||||
class PinnedEvents extends StatelessWidget {
|
class PinnedEvents extends StatelessWidget {
|
||||||
final ChatController controller;
|
final ChatController controller;
|
||||||
@ -101,34 +100,20 @@ class PinnedEvents extends StatelessWidget {
|
|||||||
hideReply: true,
|
hideReply: true,
|
||||||
),
|
),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return Linkify(
|
final text = snapshot.data ??
|
||||||
text: snapshot.data ??
|
event.calcLocalizedBodyFallback(
|
||||||
event.calcLocalizedBodyFallback(
|
MatrixLocals(L10n.of(context)!),
|
||||||
MatrixLocals(L10n.of(context)!),
|
withSenderNamePrefix: true,
|
||||||
withSenderNamePrefix: true,
|
hideReply: true,
|
||||||
hideReply: true,
|
);
|
||||||
),
|
return TextLinkifyEmojify(
|
||||||
options: const LinkifyOptions(humanize: false),
|
text,
|
||||||
maxLines: 2,
|
fontSize: fontSize,
|
||||||
style: TextStyle(
|
textColor:
|
||||||
color:
|
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
textDecoration: event.redacted
|
||||||
overflow: TextOverflow.ellipsis,
|
? TextDecoration.lineThrough
|
||||||
fontSize: fontSize,
|
: null,
|
||||||
decoration: event.redacted
|
|
||||||
? TextDecoration.lineThrough
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
linkStyle: TextStyle(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
fontSize: fontSize,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
decorationColor:
|
|
||||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
onOpen: (url) =>
|
|
||||||
UrlLauncher(context, url.url).launchUrl(),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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 'package:flutter_linkify/flutter_linkify.dart';
|
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
import 'package:vrouter/vrouter.dart';
|
import 'package:vrouter/vrouter.dart';
|
||||||
|
|
||||||
@ -10,12 +9,12 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
|||||||
import 'package:fluffychat/pages/chat_details/participant_list_item.dart';
|
import 'package:fluffychat/pages/chat_details/participant_list_item.dart';
|
||||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
|
import 'package:fluffychat/widgets/animated_emoji_plain_text.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||||
import 'package:fluffychat/widgets/content_banner.dart';
|
import 'package:fluffychat/widgets/content_banner.dart';
|
||||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
import '../../utils/url_launcher.dart';
|
|
||||||
|
|
||||||
class ChatDetailsView extends StatelessWidget {
|
class ChatDetailsView extends StatelessWidget {
|
||||||
final ChatDetailsController controller;
|
final ChatDetailsController controller;
|
||||||
@ -125,26 +124,15 @@ class ChatDetailsView extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16.0,
|
horizontal: 16.0,
|
||||||
),
|
),
|
||||||
child: Linkify(
|
child: TextLinkifyEmojify(
|
||||||
text: room.topic.isEmpty
|
room.topic.isEmpty
|
||||||
? L10n.of(context)!.addGroupDescription
|
? L10n.of(context)!.addGroupDescription
|
||||||
: room.topic,
|
: room.topic,
|
||||||
options: const LinkifyOptions(humanize: false),
|
fontSize: 14,
|
||||||
linkStyle:
|
textColor: Theme.of(context)
|
||||||
const TextStyle(color: Colors.blueAccent),
|
.textTheme
|
||||||
style: TextStyle(
|
.bodyMedium!
|
||||||
fontSize: 14,
|
.color!,
|
||||||
color: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium!
|
|
||||||
.color,
|
|
||||||
decorationColor: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium!
|
|
||||||
.color,
|
|
||||||
),
|
|
||||||
onOpen: (url) =>
|
|
||||||
UrlLauncher(context, url.url).launchUrl(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
@ -169,7 +169,7 @@ class ChatEncryptionSettingsView extends StatelessWidget {
|
|||||||
deviceKeys[i].ed25519Key?.beautified ??
|
deviceKeys[i].ed25519Key?.beautified ??
|
||||||
L10n.of(context)!.unknownEncryptionAlgorithm,
|
L10n.of(context)!.unknownEncryptionAlgorithm,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'RobotoMono',
|
fontFamily: 'Roboto Mono',
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -62,7 +62,7 @@ class ImageViewerView extends StatelessWidget {
|
|||||||
event: controller.widget.event,
|
event: controller.widget.event,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
isThumbnail: false,
|
isThumbnail: false,
|
||||||
animated: true,
|
forceAnimation: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
import 'settings_chat_view.dart';
|
import 'settings_chat_view.dart';
|
||||||
|
|
||||||
class SettingsChat extends StatefulWidget {
|
class SettingsChat extends StatefulWidget {
|
||||||
@ -12,4 +16,47 @@ class SettingsChat extends StatefulWidget {
|
|||||||
class SettingsChatController extends State<SettingsChat> {
|
class SettingsChatController extends State<SettingsChat> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => SettingsChatView(this);
|
Widget build(BuildContext context) => SettingsChatView(this);
|
||||||
|
|
||||||
|
bool get autoplayAnimations =>
|
||||||
|
Matrix.of(context).client.autoplayAnimatedContent ?? !kIsWeb;
|
||||||
|
|
||||||
|
Future<void> setAutoplayAnimations(bool value) async {
|
||||||
|
try {
|
||||||
|
final client = Matrix.of(context).client;
|
||||||
|
await client.setAutoplayAnimatedContent(value);
|
||||||
|
} catch (e) {
|
||||||
|
Logs().w('Error storing animation preferences.', e);
|
||||||
|
} finally {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoplayAnimatedContentExtension on Client {
|
||||||
|
static const _elementWebKey = 'im.vector.web.settings';
|
||||||
|
|
||||||
|
/// returns whether user preferences configured to autoplay motion
|
||||||
|
/// message content such as gifs, webp, apng, videos or animations.
|
||||||
|
bool? get autoplayAnimatedContent {
|
||||||
|
if (!accountData.containsKey(_elementWebKey)) return null;
|
||||||
|
try {
|
||||||
|
final elementWebData = accountData[_elementWebKey]?.content;
|
||||||
|
return elementWebData?['autoplayGifs'] as bool?;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setAutoplayAnimatedContent(bool autoplay) async {
|
||||||
|
final elementWebData = accountData[_elementWebKey]?.content ?? {};
|
||||||
|
elementWebData['autoplayGifs'] = autoplay;
|
||||||
|
final uid = userID;
|
||||||
|
if (uid != null) {
|
||||||
|
await setAccountData(
|
||||||
|
uid,
|
||||||
|
_elementWebKey,
|
||||||
|
elementWebData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import 'package:vrouter/vrouter.dart';
|
|||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/config/setting_keys.dart';
|
import 'package:fluffychat/config/setting_keys.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
|
||||||
import 'package:fluffychat/utils/voip/callkeep_manager.dart';
|
import 'package:fluffychat/utils/voip/callkeep_manager.dart';
|
||||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
@ -58,13 +57,11 @@ class SettingsChatView extends StatelessWidget {
|
|||||||
storeKey: SettingKeys.hideUnimportantStateEvents,
|
storeKey: SettingKeys.hideUnimportantStateEvents,
|
||||||
defaultValue: AppConfig.hideUnimportantStateEvents,
|
defaultValue: AppConfig.hideUnimportantStateEvents,
|
||||||
),
|
),
|
||||||
if (PlatformInfos.isMobile)
|
SwitchListTile.adaptive(
|
||||||
SettingsSwitchListTile.adaptive(
|
title: Text(L10n.of(context)!.autoplayAnimations),
|
||||||
title: L10n.of(context)!.autoplayImages,
|
value: controller.autoplayAnimations,
|
||||||
onChanged: (b) => AppConfig.autoplayImages = b,
|
onChanged: controller.setAutoplayAnimations,
|
||||||
storeKey: SettingKeys.autoplayImages,
|
),
|
||||||
defaultValue: AppConfig.autoplayImages,
|
|
||||||
),
|
|
||||||
const Divider(),
|
const Divider(),
|
||||||
SettingsSwitchListTile.adaptive(
|
SettingsSwitchListTile.adaptive(
|
||||||
title: L10n.of(context)!.sendOnEnter,
|
title: L10n.of(context)!.sendOnEnter,
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:emojis/emoji.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
@ -21,8 +22,10 @@ class EmotesSettings extends StatefulWidget {
|
|||||||
|
|
||||||
class EmotesSettingsController extends State<EmotesSettings> {
|
class EmotesSettingsController extends State<EmotesSettings> {
|
||||||
String? get roomId => VRouter.of(context).pathParameters['roomid'];
|
String? get roomId => VRouter.of(context).pathParameters['roomid'];
|
||||||
|
|
||||||
Room? get room =>
|
Room? get room =>
|
||||||
roomId != null ? Matrix.of(context).client.getRoomById(roomId!) : null;
|
roomId != null ? Matrix.of(context).client.getRoomById(roomId!) : null;
|
||||||
|
|
||||||
String? get stateKey => VRouter.of(context).pathParameters['state_key'];
|
String? get stateKey => VRouter.of(context).pathParameters['state_key'];
|
||||||
|
|
||||||
bool showSave = false;
|
bool showSave = false;
|
||||||
@ -44,6 +47,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImagePackContent? _pack;
|
ImagePackContent? _pack;
|
||||||
|
|
||||||
ImagePackContent? get pack {
|
ImagePackContent? get pack {
|
||||||
if (_pack != null) {
|
if (_pack != null) {
|
||||||
return _pack;
|
return _pack;
|
||||||
@ -258,8 +262,65 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fitzpatrick get defaultTone => Matrix.of(context).client.defaultEmojiTone;
|
||||||
|
|
||||||
|
Future<void> setDefaultTone(fitzpatrick value) async {
|
||||||
|
try {
|
||||||
|
final client = Matrix.of(context).client;
|
||||||
|
await client.setDefaultEmojiTone(value);
|
||||||
|
} catch (e) {
|
||||||
|
Logs().w('Error storing animation preferences.', e);
|
||||||
|
} finally {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return EmotesSettingsView(this);
|
return EmotesSettingsView(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension DefaultEmojiTone on Client {
|
||||||
|
static const _emoteConfigKey = 'im.fluffychat.emote_config';
|
||||||
|
|
||||||
|
/// returns whether user preferences configured to autoplay motion
|
||||||
|
/// message content such as gifs, webp, apng, videos or animations.
|
||||||
|
fitzpatrick get defaultEmojiTone {
|
||||||
|
if (!accountData.containsKey(_emoteConfigKey)) return fitzpatrick.None;
|
||||||
|
try {
|
||||||
|
final elementWebData = accountData[_emoteConfigKey]?.content;
|
||||||
|
final encoded = elementWebData?['tone'] as String?;
|
||||||
|
switch (encoded) {
|
||||||
|
case 'light':
|
||||||
|
return fitzpatrick.light;
|
||||||
|
case 'mediumLight':
|
||||||
|
return fitzpatrick.mediumLight;
|
||||||
|
case 'medium':
|
||||||
|
return fitzpatrick.medium;
|
||||||
|
case 'mediumDark':
|
||||||
|
return fitzpatrick.mediumDark;
|
||||||
|
case 'dark':
|
||||||
|
return fitzpatrick.dark;
|
||||||
|
default:
|
||||||
|
return fitzpatrick.None;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return fitzpatrick.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setDefaultEmojiTone(fitzpatrick tone) async {
|
||||||
|
final elementWebData = accountData[_emoteConfigKey]?.content ?? {};
|
||||||
|
final name = tone == fitzpatrick.None ? null : tone.name;
|
||||||
|
elementWebData['tone'] = name;
|
||||||
|
final uid = userID;
|
||||||
|
if (uid != null) {
|
||||||
|
await setAccountData(
|
||||||
|
uid,
|
||||||
|
_emoteConfigKey,
|
||||||
|
elementWebData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'package:dart_animated_emoji/dart_animated_emoji.dart';
|
||||||
|
import 'package:emojis/emoji.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:lottie/lottie.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
|
import 'package:fluffychat/widgets/animated_emoji_plain_text.dart';
|
||||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||||
import '../../widgets/matrix.dart';
|
import '../../widgets/matrix.dart';
|
||||||
import 'settings_emotes.dart';
|
import 'settings_emotes.dart';
|
||||||
|
|
||||||
|
const colorPickerSize = 32.0;
|
||||||
|
|
||||||
class EmotesSettingsView extends StatelessWidget {
|
class EmotesSettingsView extends StatelessWidget {
|
||||||
final EmotesSettingsController controller;
|
final EmotesSettingsController controller;
|
||||||
|
|
||||||
@ -33,6 +39,64 @@ class EmotesSettingsView extends StatelessWidget {
|
|||||||
body: MaxWidthBody(
|
body: MaxWidthBody(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
if (controller.room == null) ...[
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10n.of(context)!.defaultEmojiTone),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: colorPickerSize + 24,
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
children: fitzpatrick.values
|
||||||
|
.map(
|
||||||
|
(tone) => Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(colorPickerSize),
|
||||||
|
onTap: () => controller.setDefaultTone(tone),
|
||||||
|
child: Material(
|
||||||
|
elevation: 6,
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(colorPickerSize),
|
||||||
|
child: SizedBox(
|
||||||
|
width: colorPickerSize,
|
||||||
|
height: colorPickerSize,
|
||||||
|
child: controller.defaultTone == tone
|
||||||
|
? Center(
|
||||||
|
child: Lottie.memory(
|
||||||
|
Uint8List.fromList(
|
||||||
|
AnimatedEmoji.all
|
||||||
|
.firstWhere(
|
||||||
|
(e) =>
|
||||||
|
e.fallback ==
|
||||||
|
Emoji.modify(
|
||||||
|
'\u{1f44b}',
|
||||||
|
tone,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.lottieAnimation
|
||||||
|
.codeUnits,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Text(
|
||||||
|
Emoji.modify('\u{1f44b}', tone),
|
||||||
|
style: const TextStyle(fontSize: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
],
|
||||||
if (!controller.readonly)
|
if (!controller.readonly)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@ -99,9 +163,9 @@ class EmotesSettingsView extends StatelessWidget {
|
|||||||
? Center(
|
? Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Text(
|
child: TextLinkifyEmojify(
|
||||||
L10n.of(context)!.noEmotesFound,
|
L10n.of(context)!.noEmotesFound,
|
||||||
style: const TextStyle(fontSize: 20),
|
fontSize: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -225,6 +289,7 @@ class _EmoteImage extends StatelessWidget {
|
|||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
|
forceAnimation: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
217
lib/widgets/animated_emoji_plain_text.dart
Normal file
217
lib/widgets/animated_emoji_plain_text.dart
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:dart_animated_emoji/dart_animated_emoji.dart';
|
||||||
|
import 'package:emoji_regex/emoji_regex.dart';
|
||||||
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||||
|
import 'package:lottie/lottie.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/pages/settings_chat/settings_chat.dart';
|
||||||
|
import 'package:fluffychat/utils/url_launcher.dart';
|
||||||
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
||||||
|
/// takes a text as input and parses out Animated Emojis adn Linkifys it
|
||||||
|
class TextLinkifyEmojify extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final double fontSize;
|
||||||
|
final Color? textColor;
|
||||||
|
final TextDecoration? textDecoration;
|
||||||
|
|
||||||
|
const TextLinkifyEmojify(
|
||||||
|
this.text, {
|
||||||
|
super.key,
|
||||||
|
required this.fontSize,
|
||||||
|
this.textColor,
|
||||||
|
this.textDecoration,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
String text = this.text;
|
||||||
|
final regex = emojiRegex();
|
||||||
|
|
||||||
|
final animate =
|
||||||
|
Matrix.of(context).client.autoplayAnimatedContent ?? !kIsWeb;
|
||||||
|
|
||||||
|
final parts = <Widget>[];
|
||||||
|
do {
|
||||||
|
// in order to prevent animated rendering of partial emojis in case
|
||||||
|
// the glyph is constructed from several code points, match on emojis in
|
||||||
|
// general and then check whether the entire glyph is animatable
|
||||||
|
final match = regex.allMatches(text).firstWhereOrNull(
|
||||||
|
(match) =>
|
||||||
|
AnimatedEmoji.all.any((emoji) => emoji.fallback == match[0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (match == null || match.start != 0) {
|
||||||
|
parts.add(_linkifyString(text.substring(0, match?.start), context));
|
||||||
|
}
|
||||||
|
if (match != null) {
|
||||||
|
final emoji = AnimatedEmoji.all.firstWhere(
|
||||||
|
(element) => element.fallback == match[0],
|
||||||
|
);
|
||||||
|
parts.add(_lottieBox(emoji, animate));
|
||||||
|
text = text.substring(match.end);
|
||||||
|
} else {
|
||||||
|
text = '';
|
||||||
|
}
|
||||||
|
} while (regex.hasMatch(text));
|
||||||
|
if (text.isNotEmpty) {
|
||||||
|
parts.add(_linkifyString(text, context));
|
||||||
|
}
|
||||||
|
if (parts.length == 1) {
|
||||||
|
return parts.single;
|
||||||
|
} else {
|
||||||
|
return Wrap(
|
||||||
|
alignment: WrapAlignment.start,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.start,
|
||||||
|
spacing: 2,
|
||||||
|
runSpacing: 2,
|
||||||
|
children: parts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _linkifyString(String text, BuildContext context) {
|
||||||
|
return Linkify(
|
||||||
|
text: text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: textColor,
|
||||||
|
fontSize: fontSize,
|
||||||
|
decoration: textDecoration,
|
||||||
|
),
|
||||||
|
options: const LinkifyOptions(humanize: false),
|
||||||
|
linkStyle: TextStyle(
|
||||||
|
color: textColor?.withAlpha(150),
|
||||||
|
fontSize: fontSize,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationColor: textColor?.withAlpha(150),
|
||||||
|
),
|
||||||
|
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _lottieBox(AnimatedEmoji emoji, bool animate) {
|
||||||
|
return AnimatedEmojiLottieView(
|
||||||
|
emoji: emoji,
|
||||||
|
size: fontSize * 1.25,
|
||||||
|
textColor: textColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnimatedEmojiLottieView extends StatelessWidget {
|
||||||
|
final AnimatedEmoji emoji;
|
||||||
|
final double size;
|
||||||
|
final Color? textColor;
|
||||||
|
|
||||||
|
const AnimatedEmojiLottieView({
|
||||||
|
super.key,
|
||||||
|
required this.emoji,
|
||||||
|
required this.size,
|
||||||
|
this.textColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => SizedBox.square(
|
||||||
|
dimension: size,
|
||||||
|
child: AnimationEnabledContainerView(
|
||||||
|
iconSize: size / 2.5,
|
||||||
|
builder: (animate) {
|
||||||
|
return Lottie.memory(
|
||||||
|
key: ValueKey(emoji.name + size.toString()),
|
||||||
|
Uint8List.fromList(emoji.lottieAnimation.codeUnits),
|
||||||
|
animate: animate,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
textColor: textColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef AnimatedChildBuilder = Widget Function(bool animate);
|
||||||
|
|
||||||
|
class AnimationEnabledContainerView extends StatefulWidget {
|
||||||
|
final AnimatedChildBuilder builder;
|
||||||
|
final double iconSize;
|
||||||
|
final Color? textColor;
|
||||||
|
final bool disableTapHandler;
|
||||||
|
|
||||||
|
const AnimationEnabledContainerView({
|
||||||
|
super.key,
|
||||||
|
required this.builder,
|
||||||
|
required this.iconSize,
|
||||||
|
this.textColor,
|
||||||
|
this.disableTapHandler = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AnimationEnabledContainerView> createState() =>
|
||||||
|
_AnimationEnabledContainerViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimationEnabledContainerViewState
|
||||||
|
extends State<AnimationEnabledContainerView> {
|
||||||
|
bool get autoplay =>
|
||||||
|
Matrix.of(context).client.autoplayAnimatedContent ?? true;
|
||||||
|
|
||||||
|
/// whether to animate though autoplay disabled
|
||||||
|
bool animating = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final autoplay = this.autoplay;
|
||||||
|
|
||||||
|
final box = widget.builder.call(autoplay || animating);
|
||||||
|
|
||||||
|
if (autoplay) return box;
|
||||||
|
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: startAnimation,
|
||||||
|
onHover: startAnimation,
|
||||||
|
onExit: stopAnimation,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: widget.disableTapHandler ? null : toggleAnimation,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
fit: StackFit.loose,
|
||||||
|
children: [
|
||||||
|
box,
|
||||||
|
if (!animating)
|
||||||
|
Icon(
|
||||||
|
Icons.gif,
|
||||||
|
size: widget.iconSize,
|
||||||
|
color: widget.textColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startAnimation(PointerEvent e) {
|
||||||
|
if (e.kind == PointerDeviceKind.mouse) {
|
||||||
|
setState(() => animating = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopAnimation(PointerEvent e) {
|
||||||
|
if (e.kind == PointerDeviceKind.mouse) {
|
||||||
|
setState(() => animating = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleAnimation() => setState(() => animating = !animating);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant AnimationEnabledContainerView oldWidget) {
|
||||||
|
if (oldWidget.builder != widget.builder ||
|
||||||
|
oldWidget.iconSize != widget.iconSize ||
|
||||||
|
oldWidget.textColor != widget.textColor) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
}
|
@ -66,6 +66,8 @@ class Avatar extends StatelessWidget {
|
|||||||
height: size,
|
height: size,
|
||||||
placeholder: (_) => textWidget,
|
placeholder: (_) => textWidget,
|
||||||
cacheKey: mxContent.toString(),
|
cacheKey: mxContent.toString(),
|
||||||
|
watermarkSize: fontSize,
|
||||||
|
watermarkColor: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -54,7 +54,6 @@ class ContentBanner extends StatelessWidget {
|
|||||||
: MxcImage(
|
: MxcImage(
|
||||||
key: Key(mxContent?.toString() ?? 'NoKey'),
|
key: Key(mxContent?.toString() ?? 'NoKey'),
|
||||||
uri: mxContent,
|
uri: mxContent,
|
||||||
animated: true,
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: placeholder,
|
placeholder: placeholder,
|
||||||
height: 400,
|
height: 400,
|
||||||
|
@ -22,6 +22,7 @@ import 'package:universal_html/html.dart' as html;
|
|||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:vrouter/vrouter.dart';
|
import 'package:vrouter/vrouter.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/pages/settings_chat/settings_chat.dart';
|
||||||
import 'package:fluffychat/utils/client_manager.dart';
|
import 'package:fluffychat/utils/client_manager.dart';
|
||||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
@ -486,9 +487,12 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
|||||||
store
|
store
|
||||||
.getItemBool(SettingKeys.separateChatTypes, AppConfig.separateChatTypes)
|
.getItemBool(SettingKeys.separateChatTypes, AppConfig.separateChatTypes)
|
||||||
.then((value) => AppConfig.separateChatTypes = value);
|
.then((value) => AppConfig.separateChatTypes = value);
|
||||||
store
|
store.getItemBool(SettingKeys.autoplayImages).then((value) async {
|
||||||
.getItemBool(SettingKeys.autoplayImages, AppConfig.autoplayImages)
|
if (value != client.autoplayAnimatedContent) {
|
||||||
.then((value) => AppConfig.autoplayImages = value);
|
await client.setAutoplayAnimatedContent(value);
|
||||||
|
}
|
||||||
|
await store.deleteItem(SettingKeys.autoplayImages);
|
||||||
|
});
|
||||||
store
|
store
|
||||||
.getItemBool(SettingKeys.sendOnEnter, AppConfig.sendOnEnter)
|
.getItemBool(SettingKeys.sendOnEnter, AppConfig.sendOnEnter)
|
||||||
.then((value) => AppConfig.sendOnEnter = value);
|
.then((value) => AppConfig.sendOnEnter = value);
|
||||||
|
@ -1,22 +1,32 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:async';
|
||||||
|
import 'dart:ui' as ui show Image;
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/themes.dart';
|
import 'package:fluffychat/config/themes.dart';
|
||||||
|
import 'package:fluffychat/pages/settings_chat/settings_chat.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
import 'animated_emoji_plain_text.dart';
|
||||||
|
|
||||||
|
enum AnimationState { userDefined, forced, disabled }
|
||||||
|
|
||||||
class MxcImage extends StatefulWidget {
|
class MxcImage extends StatefulWidget {
|
||||||
final Uri? uri;
|
final Uri? uri;
|
||||||
final Event? event;
|
final Event? event;
|
||||||
final double? width;
|
final double? width;
|
||||||
final double? height;
|
final double? height;
|
||||||
|
final double? watermarkSize;
|
||||||
|
final Color? watermarkColor;
|
||||||
|
final bool forceAnimation;
|
||||||
|
final bool disableTapHandler;
|
||||||
final BoxFit? fit;
|
final BoxFit? fit;
|
||||||
final bool isThumbnail;
|
final bool isThumbnail;
|
||||||
final bool animated;
|
|
||||||
final Duration retryDuration;
|
final Duration retryDuration;
|
||||||
final Duration animationDuration;
|
final Duration animationDuration;
|
||||||
final Curve animationCurve;
|
final Curve animationCurve;
|
||||||
@ -32,13 +42,16 @@ class MxcImage extends StatefulWidget {
|
|||||||
this.fit,
|
this.fit,
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
this.isThumbnail = true,
|
this.isThumbnail = true,
|
||||||
this.animated = false,
|
|
||||||
this.animationDuration = FluffyThemes.animationDuration,
|
this.animationDuration = FluffyThemes.animationDuration,
|
||||||
this.retryDuration = const Duration(seconds: 2),
|
this.retryDuration = const Duration(seconds: 2),
|
||||||
this.animationCurve = FluffyThemes.animationCurve,
|
this.animationCurve = FluffyThemes.animationCurve,
|
||||||
this.thumbnailMethod = ThumbnailMethod.scale,
|
this.thumbnailMethod = ThumbnailMethod.scale,
|
||||||
this.cacheKey,
|
this.cacheKey,
|
||||||
Key? key,
|
Key? key,
|
||||||
|
this.watermarkSize,
|
||||||
|
this.watermarkColor,
|
||||||
|
this.forceAnimation = false,
|
||||||
|
this.disableTapHandler = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -46,14 +59,42 @@ class MxcImage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MxcImageState extends State<MxcImage> {
|
class _MxcImageState extends State<MxcImage> {
|
||||||
static final Map<String, Uint8List> _imageDataCache = {};
|
static final Map<String, ImageFutureResponse> _imageDataCache = {};
|
||||||
Uint8List? _imageDataNoCache;
|
ImageFutureResponse? _imageDataNoCache;
|
||||||
Uint8List? get _imageData {
|
|
||||||
|
ImageFutureResponse? get _imageData {
|
||||||
final cacheKey = widget.cacheKey;
|
final cacheKey = widget.cacheKey;
|
||||||
return cacheKey == null ? _imageDataNoCache : _imageDataCache[cacheKey];
|
return cacheKey == null ? _imageDataNoCache : _imageDataCache[cacheKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
set _imageData(Uint8List? data) {
|
/// asynchronously
|
||||||
|
Future<ImageFutureResponse> removeImageAnimations(Uint8List data) async {
|
||||||
|
final provider = MemoryImage(data);
|
||||||
|
|
||||||
|
final codec = await instantiateImageCodecWithSize(
|
||||||
|
await ImmutableBuffer.fromUint8List(data),
|
||||||
|
);
|
||||||
|
if (codec.frameCount > 1) {
|
||||||
|
final frame = await codec.getNextFrame();
|
||||||
|
return ThumbnailImageResponse(
|
||||||
|
thumbnail: frame.image,
|
||||||
|
imageProvider: provider,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return ImageProviderFutureResponse(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ImageFutureResponse> _renderImageFrame(Uint8List data) async {
|
||||||
|
if (widget.forceAnimation ||
|
||||||
|
(Matrix.of(context).client.autoplayAnimatedContent ?? !kIsWeb)) {
|
||||||
|
return ImageProviderFutureResponse(MemoryImage(data));
|
||||||
|
} else {
|
||||||
|
return await removeImageAnimations(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set _imageData(ImageFutureResponse? data) {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
final cacheKey = widget.cacheKey;
|
final cacheKey = widget.cacheKey;
|
||||||
cacheKey == null
|
cacheKey == null
|
||||||
@ -80,7 +121,7 @@ class _MxcImageState extends State<MxcImage> {
|
|||||||
client,
|
client,
|
||||||
width: realWidth,
|
width: realWidth,
|
||||||
height: realHeight,
|
height: realHeight,
|
||||||
animated: widget.animated,
|
animated: true,
|
||||||
method: widget.thumbnailMethod,
|
method: widget.thumbnailMethod,
|
||||||
)
|
)
|
||||||
: uri.getDownloadLink(client);
|
: uri.getDownloadLink(client);
|
||||||
@ -90,9 +131,9 @@ class _MxcImageState extends State<MxcImage> {
|
|||||||
if (_isCached == null) {
|
if (_isCached == null) {
|
||||||
final cachedData = await client.database?.getFile(storeKey);
|
final cachedData = await client.database?.getFile(storeKey);
|
||||||
if (cachedData != null) {
|
if (cachedData != null) {
|
||||||
|
_imageData = await _renderImageFrame(cachedData);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_imageData = cachedData;
|
|
||||||
_isCached = true;
|
_isCached = true;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -109,10 +150,9 @@ class _MxcImageState extends State<MxcImage> {
|
|||||||
}
|
}
|
||||||
final remoteData = response.bodyBytes;
|
final remoteData = response.bodyBytes;
|
||||||
|
|
||||||
|
_imageData = await _renderImageFrame(remoteData);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {});
|
||||||
_imageData = remoteData;
|
|
||||||
});
|
|
||||||
await client.database?.storeFile(storeKey, remoteData, 0);
|
await client.database?.storeFile(storeKey, remoteData, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,10 +161,9 @@ class _MxcImageState extends State<MxcImage> {
|
|||||||
getThumbnail: widget.isThumbnail,
|
getThumbnail: widget.isThumbnail,
|
||||||
);
|
);
|
||||||
if (data.detectFileType is MatrixImageFile) {
|
if (data.detectFileType is MatrixImageFile) {
|
||||||
|
_imageData = await _renderImageFrame(data.bytes);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {});
|
||||||
_imageData = data.bytes;
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,26 +196,75 @@ class _MxcImageState extends State<MxcImage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final data = _imageData;
|
final data = _imageData;
|
||||||
|
|
||||||
|
Widget child;
|
||||||
|
if (data is ThumbnailImageResponse) {
|
||||||
|
child = AnimationEnabledContainerView(
|
||||||
|
builder: (bool animate) => animate
|
||||||
|
? _buildImageProvider(data.imageProvider)
|
||||||
|
: _buildFrameImage(data.thumbnail),
|
||||||
|
disableTapHandler: widget.disableTapHandler,
|
||||||
|
iconSize: widget.watermarkSize ?? 0,
|
||||||
|
textColor: widget.watermarkColor ?? Colors.transparent,
|
||||||
|
);
|
||||||
|
} else if (data is ImageProviderFutureResponse) {
|
||||||
|
child = _buildImageProvider(data.imageProvider);
|
||||||
|
} else {
|
||||||
|
child = const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
return AnimatedCrossFade(
|
return AnimatedCrossFade(
|
||||||
duration: widget.animationDuration,
|
duration: widget.animationDuration,
|
||||||
crossFadeState:
|
crossFadeState:
|
||||||
data == null ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
data == null ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||||
firstChild: placeholder(context),
|
firstChild: placeholder(context),
|
||||||
secondChild: data == null || data.isEmpty
|
secondChild: child,
|
||||||
? const SizedBox.shrink()
|
);
|
||||||
: Image.memory(
|
}
|
||||||
data,
|
|
||||||
width: widget.width,
|
Widget _buildFrameImage(ui.Image image) {
|
||||||
height: widget.height,
|
return RawImage(
|
||||||
fit: widget.fit,
|
key: ValueKey(image),
|
||||||
filterQuality: FilterQuality.medium,
|
image: image,
|
||||||
errorBuilder: (context, __, ___) {
|
width: widget.width,
|
||||||
_isCached = false;
|
height: widget.height,
|
||||||
_imageData = null;
|
fit: widget.fit,
|
||||||
WidgetsBinding.instance.addPostFrameCallback(_tryLoad);
|
filterQuality: FilterQuality.medium,
|
||||||
return placeholder(context);
|
);
|
||||||
},
|
}
|
||||||
),
|
|
||||||
|
Widget _buildImageProvider(ImageProvider image) {
|
||||||
|
return Image(
|
||||||
|
key: ValueKey(image),
|
||||||
|
image: image,
|
||||||
|
width: widget.width,
|
||||||
|
height: widget.height,
|
||||||
|
fit: widget.fit,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
errorBuilder: (context, __, ___) {
|
||||||
|
_isCached = false;
|
||||||
|
_imageData = null;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(_tryLoad);
|
||||||
|
return placeholder(context);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class ImageFutureResponse {
|
||||||
|
const ImageFutureResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageProviderFutureResponse extends ImageFutureResponse {
|
||||||
|
final ImageProvider imageProvider;
|
||||||
|
|
||||||
|
const ImageProviderFutureResponse(this.imageProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThumbnailImageResponse extends ImageProviderFutureResponse {
|
||||||
|
final ui.Image thumbnail;
|
||||||
|
|
||||||
|
const ThumbnailImageResponse({
|
||||||
|
required this.thumbnail,
|
||||||
|
required ImageProvider imageProvider,
|
||||||
|
}) : super(imageProvider);
|
||||||
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
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 'package:flutter_linkify/flutter_linkify.dart';
|
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
import 'package:vrouter/vrouter.dart';
|
import 'package:vrouter/vrouter.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/utils/url_launcher.dart';
|
import 'package:fluffychat/widgets/animated_emoji_plain_text.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
import '../utils/localized_exception_extension.dart';
|
import '../utils/localized_exception_extension.dart';
|
||||||
@ -153,16 +152,10 @@ class PublicRoomBottomSheet extends StatelessWidget {
|
|||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Linkify(
|
subtitle: TextLinkifyEmojify(
|
||||||
text: profile!.topic!,
|
profile!.topic!,
|
||||||
linkStyle: const TextStyle(color: Colors.blueAccent),
|
fontSize: 14,
|
||||||
style: TextStyle(
|
textColor: Theme.of(context).textTheme.bodyMedium!.color!,
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium!.color,
|
|
||||||
),
|
|
||||||
options: const LinkifyOptions(humanize: false),
|
|
||||||
onOpen: (url) =>
|
|
||||||
UrlLauncher(context, url.url).launchUrl(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -53,11 +53,11 @@ static void my_application_activate(GApplication* application) {
|
|||||||
if (use_header_bar) {
|
if (use_header_bar) {
|
||||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||||
gtk_header_bar_set_title(header_bar, "fluffychat");
|
gtk_header_bar_set_title(header_bar, "FluffyChat");
|
||||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||||
} else {
|
} else {
|
||||||
gtk_window_set_title(window, "fluffychat");
|
gtk_window_set_title(window, "FluffyChat");
|
||||||
}
|
}
|
||||||
|
|
||||||
gtk_window_set_default_size(window, 864, 600);
|
gtk_window_set_default_size(window, 864, 600);
|
||||||
|
28
pubspec.lock
28
pubspec.lock
@ -241,6 +241,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.5"
|
||||||
|
dart_animated_emoji:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: dart_animated_emoji
|
||||||
|
sha256: a5ef7770230046051fc666ad26ce092a7f7d10fb730ea76687c4a6d2f58d93c9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1"
|
||||||
dart_code_metrics:
|
dart_code_metrics:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -345,6 +353,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
|
emoji_regex:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: emoji_regex
|
||||||
|
sha256: "3a25dd4d16f98b6f76dc37cc9ae49b8511891ac4b87beac9443a1e9f4634b6c7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.5"
|
||||||
emojis:
|
emojis:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1094,6 +1110,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
|
lottie:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: lottie
|
||||||
|
sha256: "23522951540d20a57a60202ed7022e6376bed206a4eee1c347a91f58bd57eb9f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
macos_ui:
|
macos_ui:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1138,10 +1162,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: matrix
|
name: matrix
|
||||||
sha256: "246499026eff75252dc47aef509760eff1508d541a33c0a9964cc1377b2bfd4c"
|
sha256: ecee8d687224f0fe668a5b9a034e8c2a7b241f578c93a236e0e93a8c2382a458
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.22.0"
|
version: "0.20.5"
|
||||||
matrix_api_lite:
|
matrix_api_lite:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
28
pubspec.yaml
28
pubspec.yaml
@ -16,6 +16,7 @@ dependencies:
|
|||||||
collection: ^1.16.0
|
collection: ^1.16.0
|
||||||
connectivity_plus: ^4.0.1
|
connectivity_plus: ^4.0.1
|
||||||
cupertino_icons: any
|
cupertino_icons: any
|
||||||
|
dart_animated_emoji: ^0.0.1
|
||||||
desktop_drop: ^0.4.0
|
desktop_drop: ^0.4.0
|
||||||
desktop_lifecycle: ^0.1.0
|
desktop_lifecycle: ^0.1.0
|
||||||
desktop_notifications: ^0.6.3
|
desktop_notifications: ^0.6.3
|
||||||
@ -23,6 +24,7 @@ dependencies:
|
|||||||
dynamic_color: ^1.6.0
|
dynamic_color: ^1.6.0
|
||||||
emoji_picker_flutter: ^1.5.1
|
emoji_picker_flutter: ^1.5.1
|
||||||
emoji_proposal: ^0.0.1
|
emoji_proposal: ^0.0.1
|
||||||
|
emoji_regex: ^0.0.5
|
||||||
emojis: ^0.9.9
|
emojis: ^0.9.9
|
||||||
#fcm_shared_isolate: ^0.1.0
|
#fcm_shared_isolate: ^0.1.0
|
||||||
file_picker: ^5.3.0
|
file_picker: ^5.3.0
|
||||||
@ -62,7 +64,8 @@ dependencies:
|
|||||||
keyboard_shortcuts: ^0.1.4
|
keyboard_shortcuts: ^0.1.4
|
||||||
latlong2: ^0.8.1
|
latlong2: ^0.8.1
|
||||||
linkify: ^5.0.0
|
linkify: ^5.0.0
|
||||||
matrix: ^0.22.0
|
lottie: ^2.3.2
|
||||||
|
matrix: ^0.20.5
|
||||||
matrix_homeserver_recommendations: ^0.3.0
|
matrix_homeserver_recommendations: ^0.3.0
|
||||||
native_imaging: ^0.1.0
|
native_imaging: ^0.1.0
|
||||||
package_info_plus: ^4.0.0
|
package_info_plus: ^4.0.0
|
||||||
@ -119,6 +122,10 @@ flutter:
|
|||||||
- assets/js/package/
|
- assets/js/package/
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
|
# The roboto font must be named exactly this way.
|
||||||
|
#
|
||||||
|
# Issue : https://github.com/flutter/flutter/issues/77580#issuecomment-1112333700
|
||||||
|
# Source : https://github.com/flutter/engine/blob/3.10.2/lib/web_ui/lib/src/engine/canvaskit/fonts.dart#L133
|
||||||
- family: Roboto
|
- family: Roboto
|
||||||
fonts:
|
fonts:
|
||||||
- asset: fonts/Roboto/Roboto-Regular.ttf
|
- asset: fonts/Roboto/Roboto-Regular.ttf
|
||||||
@ -126,12 +133,27 @@ flutter:
|
|||||||
style: italic
|
style: italic
|
||||||
- asset: fonts/Roboto/Roboto-Bold.ttf
|
- asset: fonts/Roboto/Roboto-Bold.ttf
|
||||||
weight: 700
|
weight: 700
|
||||||
- family: RobotoMono
|
- family: Roboto Mono
|
||||||
fonts:
|
fonts:
|
||||||
- asset: fonts/Roboto/RobotoMono-Regular.ttf
|
- asset: fonts/Roboto/RobotoMono-Regular.ttf
|
||||||
- family: NotoEmoji
|
# These three Noto font families are hardcoded in the Flutter engine to be loaded as fallback
|
||||||
|
# from Google Fonts in case characters are supposed to be displayed that are not available in
|
||||||
|
# the provided fonts.
|
||||||
|
#
|
||||||
|
# The fonts may NOT be renamed in their family name we use in Dart.
|
||||||
|
#
|
||||||
|
# Source : https://github.com/flutter/engine/blob/3.10.2/lib/web_ui/lib/src/engine/canvaskit/font_fallback_data.dart
|
||||||
|
- family: Noto Color Emoji
|
||||||
fonts:
|
fonts:
|
||||||
- asset: fonts/NotoEmoji/NotoColorEmoji.ttf
|
- asset: fonts/NotoEmoji/NotoColorEmoji.ttf
|
||||||
|
- family: Noto Color Emoji Compat
|
||||||
|
fonts:
|
||||||
|
# This file is gathered from https://mvnrepository.com/artifact/androidx.emoji/emoji-bundled/
|
||||||
|
# Was a hazzle to find it.
|
||||||
|
- asset: fonts/NotoColorEmojiCompat.ttf
|
||||||
|
- family: Noto Sans Symbols
|
||||||
|
fonts:
|
||||||
|
- asset: fonts/NotoSansSymbols-VariableFont_wght.ttf
|
||||||
|
|
||||||
msix_config:
|
msix_config:
|
||||||
display_name: FluffyChat
|
display_name: FluffyChat
|
||||||
|
@ -148,9 +148,9 @@ diff --git a/pubspec.yaml b/pubspec.yaml
|
|||||||
index 6999d0b8..b2c9144f 100644
|
index 6999d0b8..b2c9144f 100644
|
||||||
--- a/pubspec.yaml
|
--- a/pubspec.yaml
|
||||||
+++ b/pubspec.yaml
|
+++ b/pubspec.yaml
|
||||||
@@ -26,7 +26,7 @@ dependencies:
|
@@ -27,7 +27,7 @@ dependencies:
|
||||||
emoji_picker_flutter: ^1.5.1
|
|
||||||
emoji_proposal: ^0.0.1
|
emoji_proposal: ^0.0.1
|
||||||
|
emoji_regex: ^0.0.5
|
||||||
emojis: ^0.9.9
|
emojis: ^0.9.9
|
||||||
- #fcm_shared_isolate: ^0.1.0
|
- #fcm_shared_isolate: ^0.1.0
|
||||||
+ fcm_shared_isolate: ^0.1.0
|
+ fcm_shared_isolate: ^0.1.0
|
||||||
|
Loading…
Reference in New Issue
Block a user