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": {}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
|
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 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;
|
||||
|
@ -22,7 +22,7 @@ abstract class FluffyThemes {
|
||||
|
||||
static const fallbackTextStyle = TextStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontFamilyFallback: ['NotoEmoji'],
|
||||
fontFamilyFallback: [AppConfig.emojiFontName],
|
||||
);
|
||||
|
||||
static var fallbackTextTheme = const TextTheme(
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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) =>
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -62,7 +62,7 @@ class ImageViewerView extends StatelessWidget {
|
||||
event: controller.widget.event,
|
||||
fit: BoxFit.contain,
|
||||
isThumbnail: false,
|
||||
animated: true,
|
||||
forceAnimation: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
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,
|
||||
placeholder: (_) => textWidget,
|
||||
cacheKey: mxContent.toString(),
|
||||
watermarkSize: fontSize,
|
||||
watermarkColor: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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!,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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);
|
||||
|
28
pubspec.lock
28
pubspec.lock
@ -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:
|
||||
|
28
pubspec.yaml
28
pubspec.yaml
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user