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:
TheOneWithTheBraid 2023-05-27 21:56:27 +02:00 committed by The one with the braid
parent 1911004d05
commit 2e87050544
33 changed files with 838 additions and 197 deletions

View File

@ -2449,6 +2449,8 @@
"oldDisplayName": {}
}
},
"autoplayAnimations": "Automatically play animations",
"defaultEmojiTone": "Default emoji tone",
"newSpaceDescription": "Spaces allows you to consolidate your chats and build private or public communities.",
"encryptThisChat": "Encrypt this chat",
"endToEndEncryption": "End to end encryption",

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -47,7 +47,6 @@ abstract class AppConfig {
static bool hideUnimportantStateEvents = true;
static bool showDirectChatsInSpaces = true;
static bool separateChatTypes = false;
static bool autoplayImages = true;
static bool sendOnEnter = false;
static bool experimentalVoip = false;
static const bool hideTypingUsernames = false;
@ -63,7 +62,7 @@ abstract class AppConfig {
static const String pushNotificationsGatewayUrl =
'https://push.fluffychat.im/_matrix/push/v1/notify';
static const String pushNotificationsPusherFormat = 'event_id_only';
static const String emojiFontName = 'Noto Emoji';
static const String emojiFontName = 'Noto Color Emoji';
static const String emojiFontUrl =
'https://github.com/googlefonts/noto-emoji/';
static const double borderRadius = 16.0;

View File

@ -22,7 +22,7 @@ abstract class FluffyThemes {
static const fallbackTextStyle = TextStyle(
fontFamily: 'Roboto',
fontFamilyFallback: ['NotoEmoji'],
fontFamilyFallback: [AppConfig.emojiFontName],
);
static var fallbackTextTheme = const TextTheme(

View File

@ -142,7 +142,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
minLines: 2,
maxLines: 4,
readOnly: true,
style: const TextStyle(fontFamily: 'RobotoMono'),
style: const TextStyle(fontFamily: 'Roboto Mono'),
controller: TextEditingController(text: key),
decoration: const InputDecoration(
contentPadding: EdgeInsets.all(16),
@ -272,7 +272,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
? null
: [AutofillHints.password],
controller: _recoveryKeyTextEditingController,
style: const TextStyle(fontFamily: 'RobotoMono'),
style: const TextStyle(fontFamily: 'Roboto Mono'),
decoration: InputDecoration(
contentPadding: const EdgeInsets.all(16),
hintStyle: TextStyle(

View File

@ -423,7 +423,7 @@ class ChatController extends State<ChatPageWithRoom> {
// ignore: unawaited_futures
room.sendTextEvent(
sendController.text,
sendController.text.trim(),
inReplyTo: replyEvent,
editEventId: editEvent?.eventId,
parseCommands: parseCommands,

View File

@ -1,16 +1,24 @@
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.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 {
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
State<CuteContent> createState() => _CuteContentState();
@ -18,17 +26,18 @@ class CuteContent extends StatefulWidget {
class _CuteContentState extends State<CuteContent> {
static bool _isOverlayShown = false;
@override
void initState() {
if (AppConfig.autoplayImages && !_isOverlayShown) {
addOverlay();
}
super.initState();
}
bool initialized = false;
@override
Widget build(BuildContext context) {
if (initialized == false) {
initialized = true;
if (Matrix.of(context).client.autoplayAnimatedContent ??
!kIsWeb && !_isOverlayShown) {
addOverlay();
}
}
return FutureBuilder<User?>(
future: widget.event.fetchSenderUser(),
builder: (context, snapshot) {
@ -40,9 +49,10 @@ class _CuteContentState extends State<CuteContent> {
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
TextLinkifyEmojify(
widget.event.text,
style: const TextStyle(fontSize: 150),
fontSize: 150,
textColor: widget.color,
),
if (label != null) Text(label)
],
@ -183,11 +193,14 @@ class _CuteOverlayContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: size,
child: Text(
emoji,
style: const TextStyle(fontSize: 48),
return SizedOverflowBox(
size: const Size.square(size),
child: ClipRect(
clipBehavior: Clip.hardEdge,
child: Text(
emoji,
style: const TextStyle(fontSize: 56),
),
),
);
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide Element;
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/themes/shades-of-purple.dart';
import 'package:flutter_html/flutter_html.dart';
@ -10,6 +11,7 @@ import 'package:linkify/linkify.dart';
import 'package:matrix/matrix.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/mxc_image.dart';
import '../../../utils/url_launcher.dart';
@ -18,12 +20,14 @@ class HtmlMessage extends StatelessWidget {
final String html;
final Room room;
final Color textColor;
final bool isEmojiOnly;
const HtmlMessage({
Key? key,
required this.html,
required this.room,
this.textColor = Colors.black,
this.isEmojiOnly = false,
}) : super(key: key);
@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(
renderHtml,
@ -64,11 +70,21 @@ class HtmlMessage extends StatelessWidget {
.join('')
.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);
// there is no need to pre-validate the html, as we validate it while rendering
return Html(
data: linkifiedRenderHtml,
data: emojifiedHtml,
style: {
'*': Style(
color: textColor,
@ -136,8 +152,15 @@ class HtmlMessage extends StatelessWidget {
),
const TableHtmlExtension(),
SpoilerExtension(textColor: textColor),
const ImageExtension(),
ImageExtension(
isEmojiOnly: isEmojiOnly,
watermarkColor: textColor,
),
FontColorExtension(),
AnimatedEmojiExtension(
isEmojiOnly: isEmojiOnly,
defaultTextColor: textColor,
),
],
onLinkTap: (url, _, __) => UrlLauncher(context, url).launchUrl(),
onlyRenderTheseTags: const {
@ -245,8 +268,14 @@ class FontColorExtension extends HtmlExtension {
class ImageExtension extends HtmlExtension {
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
Set<String> get supportedTags => {'img'};
@ -258,18 +287,34 @@ class ImageExtension extends HtmlExtension {
return TextSpan(text: context.attributes['alt']);
}
final width = double.tryParse(context.attributes['width'] ?? '');
final height = double.tryParse(context.attributes['height'] ?? '');
double? width, 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(
child: SizedBox(
width: width ?? height ?? defaultDimension,
height: height ?? width ?? defaultDimension,
child: MxcImage(
watermarkSize: (width ?? height ?? defaultDimension) / 2.5,
uri: mxcUrl,
width: width ?? height ?? defaultDimension,
height: height ?? width ?? defaultDimension,
cacheKey: mxcUrl.toString(),
watermarkColor: watermarkColor,
),
),
);
@ -321,6 +366,7 @@ class MatrixMathExtension extends HtmlExtension {
final TextStyle? style;
MatrixMathExtension({this.style});
@override
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 {
final double fontSize;
CodeExtension({required this.fontSize});
@override
Set<String> get supportedTags => {'code'};
@ -391,6 +492,7 @@ class RoomPillExtension extends HtmlExtension {
final BuildContext context;
RoomPillExtension(this.context, this.room);
@override
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;
}

View File

@ -14,13 +14,14 @@ class ImageBubble extends StatelessWidget {
final bool maxSize;
final Color? backgroundColor;
final bool thumbnailOnly;
final bool animated;
final double width;
final double height;
final void Function()? onTap;
final Color? watermarkColor;
const ImageBubble(
this.event, {
Key? key,
this.tapToView = true,
this.maxSize = true,
this.backgroundColor,
@ -28,9 +29,8 @@ class ImageBubble extends StatelessWidget {
this.thumbnailOnly = true,
this.width = 400,
this.height = 300,
this.animated = false,
this.onTap,
Key? key,
this.watermarkColor,
}) : super(key: key);
Widget _buildPlaceholder(BuildContext context) {
@ -94,13 +94,16 @@ class ImageBubble extends StatelessWidget {
)
: null,
child: MxcImage(
key: ValueKey(event.eventId),
event: event,
width: width,
height: height,
fit: fit,
animated: animated,
disableTapHandler: true,
isThumbnail: thumbnailOnly,
placeholder: _buildPlaceholder,
watermarkSize: width / 2.5,
watermarkColor: watermarkColor,
),
),
),

View File

@ -1,13 +1,14 @@
import 'package:flutter/material.dart';
import 'package:emoji_regex/emoji_regex.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat/events/video_player.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/date_time_extension.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/matrix.dart';
import '../../../config/app_config.dart';
@ -112,12 +113,16 @@ class MessageContent extends StatelessWidget {
width: 400,
height: 300,
fit: BoxFit.cover,
watermarkColor: textColor,
);
case MessageTypes.Sticker:
if (event.redacted) continue textmessage;
return Sticker(event);
return Sticker(
event,
watermarkColor: textColor,
);
case CuteEventContent.eventType:
return CuteContent(event);
return CuteContent(event, color: textColor);
case MessageTypes.Audio:
if (PlatformInfos.isMobile ||
PlatformInfos.isMacOS ||
@ -154,6 +159,7 @@ class MessageContent extends StatelessWidget {
html: html,
textColor: textColor,
room: event.room,
isEmojiOnly: event.onlyEmotes,
);
}
// 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 <= 10;
return FutureBuilder<String>(
@ -229,26 +240,17 @@ class MessageContent extends StatelessWidget {
hideReply: true,
),
builder: (context, snapshot) {
return Linkify(
text: snapshot.data ??
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
hideReply: true,
),
style: TextStyle(
color: textColor,
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration:
event.redacted ? TextDecoration.lineThrough : null,
),
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(),
final text = snapshot.data ??
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
hideReply: true,
);
return TextLinkifyEmojify(
text,
fontSize: bigEmotes ? fontSize * 3 : fontSize,
textDecoration:
event.redacted ? TextDecoration.lineThrough : null,
textColor: textColor,
);
},
);

View File

@ -125,6 +125,8 @@ class _Reaction extends StatelessWidget {
uri: Uri.parse(reactionKey!),
width: 9999,
height: fontSize,
watermarkColor: color,
watermarkSize: (fontSize ?? 12) / 1.5,
),
const SizedBox(width: 4),
Text(

View File

@ -4,37 +4,30 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import '../../../config/app_config.dart';
import 'image_bubble.dart';
class Sticker extends StatefulWidget {
class Sticker extends StatelessWidget {
final Event event;
final Color watermarkColor;
const Sticker(this.event, {Key? key}) : super(key: key);
@override
StickerState createState() => StickerState();
}
class StickerState extends State<Sticker> {
bool? animated;
const Sticker(this.event, {Key? key, required this.watermarkColor})
: super(key: key);
@override
Widget build(BuildContext context) {
return ImageBubble(
widget.event,
event,
width: 400,
height: 400,
fit: BoxFit.contain,
onTap: () {
setState(() => animated = true);
showOkAlertDialog(
context: context,
message: widget.event.body,
message: event.body,
okLabel: L10n.of(context)!.ok,
);
},
animated: animated ?? AppConfig.autoplayImages,
watermarkColor: watermarkColor,
);
}
}

View File

@ -8,6 +8,7 @@ import 'package:matrix/matrix.dart';
import 'package:slugify/slugify.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/widgets/mxc_image.dart';
import '../../widgets/avatar.dart';
@ -44,7 +45,10 @@ class InputBar extends StatelessWidget {
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 !=
controller!.selection.extentOffset ||
controller!.selection.baseOffset < 0) {
@ -116,12 +120,28 @@ class InputBar extends StatelessWidget {
}
}
// aside of emote packs, also propose normal (tm) unicode emojis
final matchingUnicodeEmojis = Emoji.all()
.where(
(element) => [element.name, ...element.keywords]
.any((element) => element.toLowerCase().contains(emoteSearch)),
)
.toList();
final matchingUnicodeEmojis = List.from(
Emoji.all()
// filter out duplicate skins in order to reduce the list length
.where(
(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
// best matches first
// (thanks for the hint by github.com/nextcloud/circles devs)
@ -394,6 +414,7 @@ class InputBar extends StatelessWidget {
final useShortCuts = (PlatformInfos.isWeb ||
PlatformInfos.isDesktop ||
AppConfig.sendOnEnter);
final tone = Matrix.of(context).client.defaultEmojiTone;
return Shortcuts(
shortcuts: !useShortCuts
? {}
@ -456,7 +477,7 @@ class InputBar extends StatelessWidget {
},
textCapitalization: TextCapitalization.sentences,
),
suggestionsCallback: getSuggestions,
suggestionsCallback: (q) => getSuggestions(q, tone: tone),
itemBuilder: (c, s) =>
buildSuggestion(c, s, Matrix.of(context).client),
onSuggestionSelected: (Map<String, String?> suggestion) =>

View File

@ -4,13 +4,12 @@ import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.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 {
final ChatController controller;
@ -101,34 +100,20 @@ class PinnedEvents extends StatelessWidget {
hideReply: true,
),
builder: (context, snapshot) {
return Linkify(
text: snapshot.data ??
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: true,
hideReply: true,
),
options: const LinkifyOptions(humanize: false),
maxLines: 2,
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
overflow: TextOverflow.ellipsis,
fontSize: fontSize,
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(),
final text = snapshot.data ??
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: true,
hideReply: true,
);
return TextLinkifyEmojify(
text,
fontSize: fontSize,
textColor:
Theme.of(context).colorScheme.onSurfaceVariant,
textDecoration: event.redacted
? TextDecoration.lineThrough
: null,
);
},
),

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.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/utils/fluffy_share.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/chat_settings_popup_menu.dart';
import 'package:fluffychat/widgets/content_banner.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/url_launcher.dart';
class ChatDetailsView extends StatelessWidget {
final ChatDetailsController controller;
@ -125,26 +124,15 @@ class ChatDetailsView extends StatelessWidget {
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: Linkify(
text: room.topic.isEmpty
child: TextLinkifyEmojify(
room.topic.isEmpty
? L10n.of(context)!.addGroupDescription
: room.topic,
options: const LinkifyOptions(humanize: false),
linkStyle:
const TextStyle(color: Colors.blueAccent),
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
.bodyMedium!
.color,
decorationColor: Theme.of(context)
.textTheme
.bodyMedium!
.color,
),
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
fontSize: 14,
textColor: Theme.of(context)
.textTheme
.bodyMedium!
.color!,
),
),
const SizedBox(height: 8),

View File

@ -169,7 +169,7 @@ class ChatEncryptionSettingsView extends StatelessWidget {
deviceKeys[i].ed25519Key?.beautified ??
L10n.of(context)!.unknownEncryptionAlgorithm,
style: TextStyle(
fontFamily: 'RobotoMono',
fontFamily: 'Roboto Mono',
color: Theme.of(context).colorScheme.secondary,
),
),

View File

@ -62,7 +62,7 @@ class ImageViewerView extends StatelessWidget {
event: controller.widget.event,
fit: BoxFit.contain,
isThumbnail: false,
animated: true,
forceAnimation: true,
),
),
),

View File

@ -1,5 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'settings_chat_view.dart';
class SettingsChat extends StatefulWidget {
@ -12,4 +16,47 @@ class SettingsChat extends StatefulWidget {
class SettingsChatController extends State<SettingsChat> {
@override
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,
);
}
}
}

View File

@ -6,7 +6,6 @@ import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.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/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -58,13 +57,11 @@ class SettingsChatView extends StatelessWidget {
storeKey: SettingKeys.hideUnimportantStateEvents,
defaultValue: AppConfig.hideUnimportantStateEvents,
),
if (PlatformInfos.isMobile)
SettingsSwitchListTile.adaptive(
title: L10n.of(context)!.autoplayImages,
onChanged: (b) => AppConfig.autoplayImages = b,
storeKey: SettingKeys.autoplayImages,
defaultValue: AppConfig.autoplayImages,
),
SwitchListTile.adaptive(
title: Text(L10n.of(context)!.autoplayAnimations),
value: controller.autoplayAnimations,
onChanged: controller.setAutoplayAnimations,
),
const Divider(),
SettingsSwitchListTile.adaptive(
title: L10n.of(context)!.sendOnEnter,

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:emojis/emoji.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
@ -21,8 +22,10 @@ class EmotesSettings extends StatefulWidget {
class EmotesSettingsController extends State<EmotesSettings> {
String? get roomId => VRouter.of(context).pathParameters['roomid'];
Room? get room =>
roomId != null ? Matrix.of(context).client.getRoomById(roomId!) : null;
String? get stateKey => VRouter.of(context).pathParameters['state_key'];
bool showSave = false;
@ -44,6 +47,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
}
ImagePackContent? _pack;
ImagePackContent? get pack {
if (_pack != null) {
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
Widget build(BuildContext context) {
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,
);
}
}
}

View File

@ -1,15 +1,21 @@
import 'package:flutter/material.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:lottie/lottie.dart';
import 'package:matrix/matrix.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/mxc_image.dart';
import '../../widgets/matrix.dart';
import 'settings_emotes.dart';
const colorPickerSize = 32.0;
class EmotesSettingsView extends StatelessWidget {
final EmotesSettingsController controller;
@ -33,6 +39,64 @@ class EmotesSettingsView extends StatelessWidget {
body: MaxWidthBody(
child: Column(
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)
Container(
padding: const EdgeInsets.symmetric(
@ -99,9 +163,9 @@ class EmotesSettingsView extends StatelessWidget {
? Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
child: TextLinkifyEmojify(
L10n.of(context)!.noEmotesFound,
style: const TextStyle(fontSize: 20),
fontSize: 20,
),
),
)
@ -225,6 +289,7 @@ class _EmoteImage extends StatelessWidget {
fit: BoxFit.contain,
width: size,
height: size,
forceAnimation: true,
),
);
}

View 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);
}
}

View File

@ -66,6 +66,8 @@ class Avatar extends StatelessWidget {
height: size,
placeholder: (_) => textWidget,
cacheKey: mxContent.toString(),
watermarkSize: fontSize,
watermarkColor: Colors.white,
),
),
);

View File

@ -54,7 +54,6 @@ class ContentBanner extends StatelessWidget {
: MxcImage(
key: Key(mxContent?.toString() ?? 'NoKey'),
uri: mxContent,
animated: true,
fit: BoxFit.cover,
placeholder: placeholder,
height: 400,

View File

@ -22,6 +22,7 @@ import 'package:universal_html/html.dart' as html;
import 'package:url_launcher/url_launcher_string.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/localized_exception_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
@ -486,9 +487,12 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
store
.getItemBool(SettingKeys.separateChatTypes, AppConfig.separateChatTypes)
.then((value) => AppConfig.separateChatTypes = value);
store
.getItemBool(SettingKeys.autoplayImages, AppConfig.autoplayImages)
.then((value) => AppConfig.autoplayImages = value);
store.getItemBool(SettingKeys.autoplayImages).then((value) async {
if (value != client.autoplayAnimatedContent) {
await client.setAutoplayAnimatedContent(value);
}
await store.deleteItem(SettingKeys.autoplayImages);
});
store
.getItemBool(SettingKeys.sendOnEnter, AppConfig.sendOnEnter)
.then((value) => AppConfig.sendOnEnter = value);

View File

@ -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:http/http.dart' as http;
import 'package:matrix/matrix.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/widgets/matrix.dart';
import 'animated_emoji_plain_text.dart';
enum AnimationState { userDefined, forced, disabled }
class MxcImage extends StatefulWidget {
final Uri? uri;
final Event? event;
final double? width;
final double? height;
final double? watermarkSize;
final Color? watermarkColor;
final bool forceAnimation;
final bool disableTapHandler;
final BoxFit? fit;
final bool isThumbnail;
final bool animated;
final Duration retryDuration;
final Duration animationDuration;
final Curve animationCurve;
@ -32,13 +42,16 @@ class MxcImage extends StatefulWidget {
this.fit,
this.placeholder,
this.isThumbnail = true,
this.animated = false,
this.animationDuration = FluffyThemes.animationDuration,
this.retryDuration = const Duration(seconds: 2),
this.animationCurve = FluffyThemes.animationCurve,
this.thumbnailMethod = ThumbnailMethod.scale,
this.cacheKey,
Key? key,
this.watermarkSize,
this.watermarkColor,
this.forceAnimation = false,
this.disableTapHandler = false,
}) : super(key: key);
@override
@ -46,14 +59,42 @@ class MxcImage extends StatefulWidget {
}
class _MxcImageState extends State<MxcImage> {
static final Map<String, Uint8List> _imageDataCache = {};
Uint8List? _imageDataNoCache;
Uint8List? get _imageData {
static final Map<String, ImageFutureResponse> _imageDataCache = {};
ImageFutureResponse? _imageDataNoCache;
ImageFutureResponse? get _imageData {
final cacheKey = widget.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;
final cacheKey = widget.cacheKey;
cacheKey == null
@ -80,7 +121,7 @@ class _MxcImageState extends State<MxcImage> {
client,
width: realWidth,
height: realHeight,
animated: widget.animated,
animated: true,
method: widget.thumbnailMethod,
)
: uri.getDownloadLink(client);
@ -90,9 +131,9 @@ class _MxcImageState extends State<MxcImage> {
if (_isCached == null) {
final cachedData = await client.database?.getFile(storeKey);
if (cachedData != null) {
_imageData = await _renderImageFrame(cachedData);
if (!mounted) return;
setState(() {
_imageData = cachedData;
_isCached = true;
});
return;
@ -109,10 +150,9 @@ class _MxcImageState extends State<MxcImage> {
}
final remoteData = response.bodyBytes;
_imageData = await _renderImageFrame(remoteData);
if (!mounted) return;
setState(() {
_imageData = remoteData;
});
setState(() {});
await client.database?.storeFile(storeKey, remoteData, 0);
}
@ -121,10 +161,9 @@ class _MxcImageState extends State<MxcImage> {
getThumbnail: widget.isThumbnail,
);
if (data.detectFileType is MatrixImageFile) {
_imageData = await _renderImageFrame(data.bytes);
if (!mounted) return;
setState(() {
_imageData = data.bytes;
});
setState(() {});
return;
}
}
@ -157,26 +196,75 @@ class _MxcImageState extends State<MxcImage> {
Widget build(BuildContext context) {
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(
duration: widget.animationDuration,
crossFadeState:
data == null ? CrossFadeState.showFirst : CrossFadeState.showSecond,
firstChild: placeholder(context),
secondChild: data == null || data.isEmpty
? const SizedBox.shrink()
: Image.memory(
data,
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);
},
),
secondChild: child,
);
}
Widget _buildFrameImage(ui.Image image) {
return RawImage(
key: ValueKey(image),
image: image,
width: widget.width,
height: widget.height,
fit: widget.fit,
filterQuality: FilterQuality.medium,
);
}
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);
}

View File

@ -1,12 +1,11 @@
import 'package:flutter/material.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:matrix/matrix.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/matrix.dart';
import '../utils/localized_exception_extension.dart';
@ -153,16 +152,10 @@ class PublicRoomBottomSheet extends StatelessWidget {
color: Theme.of(context).colorScheme.secondary,
),
),
subtitle: Linkify(
text: profile!.topic!,
linkStyle: const TextStyle(color: Colors.blueAccent),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium!.color,
),
options: const LinkifyOptions(humanize: false),
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
subtitle: TextLinkifyEmojify(
profile!.topic!,
fontSize: 14,
textColor: Theme.of(context).textTheme.bodyMedium!.color!,
),
),
],

View File

@ -53,11 +53,11 @@ static void my_application_activate(GApplication* application) {
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
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_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "fluffychat");
gtk_window_set_title(window, "FluffyChat");
}
gtk_window_set_default_size(window, 864, 600);

View File

@ -241,6 +241,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct dev"
description:
@ -345,6 +353,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@ -1094,6 +1110,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -1138,10 +1162,10 @@ packages:
dependency: "direct main"
description:
name: matrix
sha256: "246499026eff75252dc47aef509760eff1508d541a33c0a9964cc1377b2bfd4c"
sha256: ecee8d687224f0fe668a5b9a034e8c2a7b241f578c93a236e0e93a8c2382a458
url: "https://pub.dev"
source: hosted
version: "0.22.0"
version: "0.20.5"
matrix_api_lite:
dependency: transitive
description:

View File

@ -16,6 +16,7 @@ dependencies:
collection: ^1.16.0
connectivity_plus: ^4.0.1
cupertino_icons: any
dart_animated_emoji: ^0.0.1
desktop_drop: ^0.4.0
desktop_lifecycle: ^0.1.0
desktop_notifications: ^0.6.3
@ -23,6 +24,7 @@ dependencies:
dynamic_color: ^1.6.0
emoji_picker_flutter: ^1.5.1
emoji_proposal: ^0.0.1
emoji_regex: ^0.0.5
emojis: ^0.9.9
#fcm_shared_isolate: ^0.1.0
file_picker: ^5.3.0
@ -62,7 +64,8 @@ dependencies:
keyboard_shortcuts: ^0.1.4
latlong2: ^0.8.1
linkify: ^5.0.0
matrix: ^0.22.0
lottie: ^2.3.2
matrix: ^0.20.5
matrix_homeserver_recommendations: ^0.3.0
native_imaging: ^0.1.0
package_info_plus: ^4.0.0
@ -119,6 +122,10 @@ flutter:
- assets/js/package/
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
fonts:
- asset: fonts/Roboto/Roboto-Regular.ttf
@ -126,12 +133,27 @@ flutter:
style: italic
- asset: fonts/Roboto/Roboto-Bold.ttf
weight: 700
- family: RobotoMono
- family: Roboto Mono
fonts:
- 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:
- 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:
display_name: FluffyChat

View File

@ -148,9 +148,9 @@ diff --git a/pubspec.yaml b/pubspec.yaml
index 6999d0b8..b2c9144f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -26,7 +26,7 @@ dependencies:
emoji_picker_flutter: ^1.5.1
@@ -27,7 +27,7 @@ dependencies:
emoji_proposal: ^0.0.1
emoji_regex: ^0.0.5
emojis: ^0.9.9
- #fcm_shared_isolate: ^0.1.0
+ fcm_shared_isolate: ^0.1.0