Merge branch 'soru/emotes-and-stickers' into 'main'

chore: Update image pack file format to match that of the emote msc

See merge request famedly/fluffychat!433
This commit is contained in:
Krille Fear 2021-07-20 07:44:28 +00:00
commit 41b6a14211
5 changed files with 128 additions and 105 deletions

View File

@ -24,8 +24,6 @@ class EmoteEntry {
String emote;
String mxc;
EmoteEntry({this.emote, this.mxc});
String get emoteClean => emote.substring(1, emote.length - 1);
}
class EmotesSettingsController extends State<EmotesSettings> {
@ -39,54 +37,57 @@ class EmotesSettingsController extends State<EmotesSettings> {
TextEditingController newEmoteController = TextEditingController();
TextEditingController newMxcController = TextEditingController();
ImagePackContent _getPack(BuildContext context) {
final client = Matrix.of(context).client;
final event = (room != null
? room.getState('im.ponies.room_emotes', stateKey ?? '')
: client.accountData['im.ponies.user_emotes']) ??
BasicEvent.fromJson(<String, dynamic>{
'type': 'm.dummy',
'content': <String, dynamic>{},
});
// make sure we work on a *copy* of the event
return BasicEvent.fromJson(event.toJson()).parsedImagePackContent;
}
Future<void> _save(BuildContext context) async {
if (readonly) {
return;
}
final client = Matrix.of(context).client;
// be sure to preserve any data not in "short"
Map<String, dynamic> content;
if (room != null) {
content =
room.getState('im.ponies.room_emotes', stateKey ?? '')?.content ??
<String, dynamic>{};
} else {
content = client.accountData['im.ponies.user_emotes']?.content ??
<String, dynamic>{};
}
if (!(content['emoticons'] is Map)) {
content['emoticons'] = <String, dynamic>{};
}
final pack = _getPack(context);
// add / update changed emotes
final allowedShortcodes = <String>{};
for (final emote in emotes) {
allowedShortcodes.add(emote.emote);
if (!(content['emoticons'][emote.emote] is Map)) {
content['emoticons'][emote.emote] = <String, dynamic>{};
if (pack.images.containsKey(emote.emote)) {
pack.images[emote.emote].url = Uri.parse(emote.mxc);
} else {
pack.images[emote.emote] =
ImagePackImageContent.fromJson(<String, dynamic>{
'url': emote.mxc,
});
}
content['emoticons'][emote.emote]['url'] = emote.mxc;
}
// remove emotes no more needed
// we make the iterator .toList() here so that we don't get into trouble modifying the very
// thing we are iterating over
for (final shortcode in content['emoticons'].keys.toList()) {
for (final shortcode in pack.images.keys.toList()) {
if (!allowedShortcodes.contains(shortcode)) {
content['emoticons'].remove(shortcode);
pack.images.remove(shortcode);
}
}
// remove the old "short" key
content.remove('short');
if (room != null) {
await showFutureLoadingDialog(
context: context,
future: () => client.setRoomStateWithKey(
room.id, 'im.ponies.room_emotes', content, stateKey ?? ''),
room.id, 'im.ponies.room_emotes', pack.toJson(), stateKey ?? ''),
);
} else {
await showFutureLoadingDialog(
context: context,
future: () => client.setAccountData(
client.userID, 'im.ponies.user_emotes', content),
client.userID, 'im.ponies.user_emotes', pack.toJson()),
);
}
}
@ -126,14 +127,13 @@ class EmotesSettingsController extends State<EmotesSettings> {
});
void submitEmoteAction(
String s,
String emoteCode,
EmoteEntry emote,
TextEditingController controller,
) {
final emoteCode = ':$s:';
if (emotes.indexWhere((e) => e.emote == emoteCode && e.mxc != emote.mxc) !=
-1) {
controller.text = emote.emoteClean;
controller.text = emote.emote;
showOkAlertDialog(
useRootNavigator: false,
context: context,
@ -142,8 +142,8 @@ class EmotesSettingsController extends State<EmotesSettings> {
);
return;
}
if (!RegExp(r'^:[-\w]+:$').hasMatch(emoteCode)) {
controller.text = emote.emoteClean;
if (!RegExp(r'^[-\w]+$').hasMatch(emoteCode)) {
controller.text = emote.emote;
showOkAlertDialog(
useRootNavigator: false,
context: context,
@ -190,7 +190,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
);
return;
}
final emoteCode = ':${newEmoteController.text}:';
final emoteCode = '${newEmoteController.text}';
final mxc = newMxcController.text;
if (emotes.indexWhere((e) => e.emote == emoteCode && e.mxc != mxc) != -1) {
await showOkAlertDialog(
@ -201,7 +201,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
);
return;
}
if (!RegExp(r'^:[-\w]+:$').hasMatch(emoteCode)) {
if (!RegExp(r'^[-\w]+$').hasMatch(emoteCode)) {
await showOkAlertDialog(
useRootNavigator: false,
context: context,
@ -262,35 +262,10 @@ class EmotesSettingsController extends State<EmotesSettings> {
Widget build(BuildContext context) {
if (emotes == null) {
emotes = <EmoteEntry>[];
Map<String, dynamic> emoteSource;
if (room != null) {
emoteSource =
room.getState('im.ponies.room_emotes', stateKey ?? '')?.content;
} else {
emoteSource = Matrix.of(context)
.client
.accountData['im.ponies.user_emotes']
?.content;
}
if (emoteSource != null) {
if (emoteSource['emoticons'] is Map) {
emoteSource['emoticons'].forEach((key, value) {
if (key is String &&
value is Map &&
value['url'] is String &&
value['url'].startsWith('mxc://')) {
emotes.add(EmoteEntry(emote: key, mxc: value['url']));
}
});
} else if (emoteSource['short'] is Map) {
emoteSource['short'].forEach((key, value) {
if (key is String &&
value is String &&
value.startsWith('mxc://')) {
emotes.add(EmoteEntry(emote: key, mxc: value));
}
});
}
final pack = _getPack(context);
for (final entry in pack.images.entries) {
emotes
.add(EmoteEntry(emote: entry.key, mxc: entry.value.url.toString()));
}
}
return EmotesSettingsView(this);

View File

@ -1,8 +1,10 @@
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../widgets/matrix.dart';
import '../settings_emotes.dart';
@ -117,7 +119,9 @@ class EmotesSettingsView extends StatelessWidget {
final emote = controller.emotes[i];
final textEditingController =
TextEditingController();
textEditingController.text = emote.emoteClean;
textEditingController.text = emote.emote;
final useShortCuts = (PlatformInfos.isWeb ||
PlatformInfos.isDesktop);
return ListTile(
leading: Container(
width: 180.0,
@ -129,6 +133,28 @@ class EmotesSettingsView extends StatelessWidget {
color:
Theme.of(context).secondaryHeaderColor,
),
child: Shortcuts(
shortcuts: !useShortCuts
? {}
: {
LogicalKeySet(
LogicalKeyboardKey.enter):
SubmitLineIntent(),
},
child: Actions(
actions: !useShortCuts
? {}
: {
SubmitLineIntent:
CallbackAction(onInvoke: (i) {
controller.submitEmoteAction(
textEditingController.text,
emote,
textEditingController,
);
return null;
}),
},
child: TextField(
readOnly: controller.readonly,
controller: textEditingController,
@ -136,7 +162,8 @@ class EmotesSettingsView extends StatelessWidget {
minLines: 1,
maxLines: 1,
decoration: InputDecoration(
hintText: L10n.of(context).emoteShortcode,
hintText:
L10n.of(context).emoteShortcode,
prefixText: ': ',
suffixText: ':',
prefixStyle: TextStyle(
@ -161,6 +188,8 @@ class EmotesSettingsView extends StatelessWidget {
),
),
),
),
),
title: _EmoteImage(emote.mxc),
trailing: controller.readonly
? null
@ -232,3 +261,5 @@ class _EmoteImagePickerState extends State<_EmoteImagePicker> {
}
}
}
class SubmitLineIntent extends Intent {}

View File

@ -63,16 +63,18 @@ class InputBar extends StatelessWidget {
if (emojiMatch != null) {
final packSearch = emojiMatch[1];
final emoteSearch = emojiMatch[2].toLowerCase();
final emotePacks = room.emotePacks;
final emotePacks = room.getImagePacks(ImagePackUsage.emoticon);
if (packSearch == null || packSearch.isEmpty) {
for (final pack in emotePacks.entries) {
for (final emote in pack.value.entries) {
for (final emote in pack.value.images.entries) {
if (emote.key.toLowerCase().contains(emoteSearch)) {
ret.add({
'type': 'emote',
'name': emote.key,
'pack': pack.key,
'mxc': emote.value,
'pack_avatar_url': pack.value.pack.avatarUrl?.toString(),
'pack_display_name': pack.value.pack.displayName ?? pack.key,
'mxc': emote.value.url.toString(),
});
}
if (ret.length > maxResults) {
@ -84,13 +86,17 @@ class InputBar extends StatelessWidget {
}
}
} else if (emotePacks[packSearch] != null) {
for (final emote in emotePacks[packSearch].entries) {
for (final emote in emotePacks[packSearch].images.entries) {
if (emote.key.toLowerCase().contains(emoteSearch)) {
ret.add({
'type': 'emote',
'name': emote.key,
'pack': packSearch,
'mxc': emote.value,
'pack_avatar_url':
emotePacks[packSearch].pack.avatarUrl?.toString(),
'pack_display_name':
emotePacks[packSearch].pack.displayName ?? packSearch,
'mxc': emote.value.url.toString(),
});
}
if (ret.length > maxResults) {
@ -239,8 +245,15 @@ class InputBar extends StatelessWidget {
child: Align(
alignment: Alignment.centerRight,
child: Opacity(
opacity: 0.5,
child: Text(suggestion['pack']),
opacity: suggestion['pack_avatar_url'] != null ? 0.8 : 0.5,
child: suggestion['pack_avatar_url'] != null
? Avatar(
Uri.parse(suggestion['pack_avatar_url']),
suggestion['pack_display_name'],
size: size * 0.9,
client: client,
)
: Text(suggestion['pack_display_name']),
),
),
),
@ -289,12 +302,12 @@ class InputBar extends StatelessWidget {
var isUnique = true;
final insertEmote = suggestion['name'];
final insertPack = suggestion['pack'];
final emotePacks = room.emotePacks;
final emotePacks = room.getImagePacks(ImagePackUsage.emoticon);
for (final pack in emotePacks.entries) {
if (pack.key == insertPack) {
continue;
}
for (final emote in pack.value.entries) {
for (final emote in pack.value.images.entries) {
if (emote.key == insertEmote) {
isUnique = false;
break;
@ -304,10 +317,7 @@ class InputBar extends StatelessWidget {
break;
}
}
insertText = (isUnique
? insertEmote
: ':$insertPack~${insertEmote.substring(1)}') +
' ';
insertText = ':${isUnique ? '' : insertPack + '~'}$insertEmote: ';
startText = replaceText.replaceAllMapped(
RegExp(r'(\s|^)(:(?:[-\w]+~)?[-\w]+)$'),
(Match m) => '${m[1]}$insertText',

View File

@ -647,14 +647,14 @@ packages:
name: matrix
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.7"
version: "0.1.8"
matrix_api_lite:
dependency: transitive
description:
name: matrix_api_lite
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
version: "0.3.5"
matrix_link_text:
dependency: transitive
description:
@ -1110,6 +1110,13 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
slugify:
dependency: transitive
description:
name: slugify
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
source_map_stack_trace:
dependency: transitive
description:

View File

@ -48,7 +48,7 @@ dependencies:
intl: any
localstorage: ^4.0.0+1
lottie: ^1.1.0
matrix: ^0.1.7
matrix: ^0.1.8
native_imaging:
git:
url: https://gitlab.com/famedly/libraries/native_imaging.git