feat: Add general image pack handling as per MSC2545
This also deprecates the old ways to access just emoticons, as the MSC now covers both emoticons and stickers!
This commit is contained in:
parent
ee287a09b9
commit
a1594fd9ac
|
|
@ -22,6 +22,7 @@ library matrix;
|
|||
export 'package:matrix_api_lite/matrix_api_lite.dart';
|
||||
export 'src/utils/room_update.dart';
|
||||
export 'src/utils/event_update.dart';
|
||||
export 'src/utils/image_pack_extension.dart';
|
||||
export 'src/utils/device_keys_list.dart';
|
||||
export 'src/utils/matrix_file.dart';
|
||||
export 'src/utils/matrix_id_string_extension.dart';
|
||||
|
|
|
|||
|
|
@ -545,88 +545,9 @@ class Room {
|
|||
);
|
||||
|
||||
/// return all current emote packs for this room
|
||||
Map<String, Map<String, String>> get emotePacks {
|
||||
final packs = <String, Map<String, String>>{};
|
||||
final normalizeEmotePackName = (String name) {
|
||||
name = name.replaceAll(' ', '-');
|
||||
name = name.replaceAll(RegExp(r'[^\w-]'), '');
|
||||
return name.toLowerCase();
|
||||
};
|
||||
final allMxcs = <String>{}; // for easy dedupint
|
||||
final addEmotePack = (String packName, Map<String, dynamic> content,
|
||||
[String packNameOverride]) {
|
||||
if (!(content['emoticons'] is Map) && !(content['short'] is Map)) {
|
||||
return;
|
||||
}
|
||||
if (content['pack'] is Map && content['pack']['short'] is String) {
|
||||
packName = content['pack']['short'];
|
||||
}
|
||||
if (packNameOverride != null && packNameOverride.isNotEmpty) {
|
||||
packName = packNameOverride;
|
||||
}
|
||||
packName = normalizeEmotePackName(packName);
|
||||
if (!packs.containsKey(packName)) {
|
||||
packs[packName] = <String, String>{};
|
||||
}
|
||||
if (content['emoticons'] is Map) {
|
||||
content['emoticons'].forEach((key, value) {
|
||||
if (key is String &&
|
||||
value is Map &&
|
||||
value['url'] is String &&
|
||||
value['url'].startsWith('mxc://')) {
|
||||
if (allMxcs.add(value['url'])) {
|
||||
packs[packName][key] = value['url'];
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
content['short'].forEach((key, value) {
|
||||
if (key is String && value is String && value.startsWith('mxc://')) {
|
||||
if (allMxcs.add(value)) {
|
||||
packs[packName][key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
// first add all the user emotes
|
||||
final userEmotes = client.accountData['im.ponies.user_emotes'];
|
||||
if (userEmotes != null) {
|
||||
addEmotePack('user', userEmotes.content);
|
||||
}
|
||||
// next add all the external emote rooms
|
||||
final emoteRooms = client.accountData['im.ponies.emote_rooms'];
|
||||
if (emoteRooms != null && emoteRooms.content['rooms'] is Map) {
|
||||
for (final roomEntry in emoteRooms.content['rooms'].entries) {
|
||||
final roomId = roomEntry.key;
|
||||
final room = client.getRoomById(roomId);
|
||||
if (room != null && roomEntry.value is Map) {
|
||||
for (final stateKeyEntry in roomEntry.value.entries) {
|
||||
final stateKey = stateKeyEntry.key;
|
||||
final event = room.getState('im.ponies.room_emotes', stateKey);
|
||||
if (event != null && stateKeyEntry.value is Map) {
|
||||
addEmotePack(
|
||||
(room.canonicalAlias?.isEmpty ?? true)
|
||||
? room.id
|
||||
: room.canonicalAlias,
|
||||
event.content,
|
||||
stateKeyEntry.value['name']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// finally add all the room emotes
|
||||
final allRoomEmotes = states['im.ponies.room_emotes'];
|
||||
if (allRoomEmotes != null) {
|
||||
for (final entry in allRoomEmotes.entries) {
|
||||
final stateKey = entry.key;
|
||||
final event = entry.value;
|
||||
addEmotePack(stateKey.isEmpty ? 'room' : stateKey, event.content);
|
||||
}
|
||||
}
|
||||
return packs;
|
||||
}
|
||||
@deprecated
|
||||
Map<String, Map<String, String>> get emotePacks =>
|
||||
getImagePacksFlat(ImagePackUsage.emoticon);
|
||||
|
||||
/// Sends a normal text message to this room. Returns the event ID generated
|
||||
/// by the server for this message.
|
||||
|
|
@ -635,7 +556,7 @@ class Room {
|
|||
Event inReplyTo,
|
||||
String editEventId,
|
||||
bool parseMarkdown = true,
|
||||
Map<String, Map<String, String>> emotePacks,
|
||||
@deprecated Map<String, Map<String, String>> emotePacks,
|
||||
bool parseCommands = true,
|
||||
String msgtype = MessageTypes.Text}) {
|
||||
if (parseCommands) {
|
||||
|
|
@ -651,7 +572,8 @@ class Room {
|
|||
for (final user in getParticipants()) user.mention: user.id
|
||||
};
|
||||
final html = markdown(event['body'],
|
||||
emotePacks: emotePacks ?? this.emotePacks, mentionMap: mentionMap);
|
||||
emotePacks: getImagePacksFlat(ImagePackUsage.emoticon),
|
||||
mentionMap: mentionMap);
|
||||
// if the decoded html is the same as the body, there is no need in sending a formatted message
|
||||
if (HtmlUnescape().convert(html.replaceAll(RegExp(r'<br />\n?'), '\n')) !=
|
||||
event['body']) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:slugify/slugify.dart';
|
||||
import 'package:matrix_api_lite/matrix_api_lite.dart';
|
||||
|
||||
import '../room.dart';
|
||||
|
||||
extension ImagePackRoomExtension on Room {
|
||||
/// Get all the active image packs for the specified [usage], mapped by their slug
|
||||
Map<String, ImagePackContent> getImagePacks([ImagePackUsage usage]) {
|
||||
final allMxcs = <Uri>{}; // used for easy deduplication
|
||||
final packs = <String, ImagePackContent>{};
|
||||
final addImagePack = (BasicEvent event, {Room room, String slug}) {
|
||||
if (event == null) return;
|
||||
final imagePack = event.parsedImagePackContent;
|
||||
final finalSlug = slugify(slug ?? 'pack');
|
||||
for (final entry in imagePack.images.entries) {
|
||||
final image = entry.value;
|
||||
if (allMxcs.contains(image.url)) {
|
||||
continue;
|
||||
}
|
||||
final imageUsage = image.usage ?? imagePack.pack.usage;
|
||||
if (usage != null &&
|
||||
imageUsage != null &&
|
||||
!imageUsage.contains(usage)) {
|
||||
continue;
|
||||
}
|
||||
if (!packs.containsKey(finalSlug)) {
|
||||
packs[finalSlug] = ImagePackContent.fromJson(<String, dynamic>{});
|
||||
packs[finalSlug].pack.displayName = imagePack.pack.displayName ??
|
||||
room?.displayname ??
|
||||
finalSlug ??
|
||||
'';
|
||||
packs[finalSlug].pack.avatarUrl =
|
||||
imagePack.pack.avatarUrl ?? room?.avatar;
|
||||
packs[finalSlug].pack.attribution = imagePack.pack.attribution;
|
||||
}
|
||||
packs[finalSlug].images[entry.key] = image;
|
||||
allMxcs.add(image.url);
|
||||
}
|
||||
};
|
||||
// first we add the user image pack
|
||||
addImagePack(client.accountData['im.ponies.user_emotes'], slug: 'user');
|
||||
// next we add all the external image packs
|
||||
final packRooms = client.accountData['im.ponies.emote_rooms'];
|
||||
if (packRooms != null && packRooms.content['rooms'] is Map) {
|
||||
for (final roomEntry in packRooms.content['rooms'].entries) {
|
||||
final roomId = roomEntry.key;
|
||||
final room = client.getRoomById(roomId);
|
||||
if (room != null && roomEntry.value is Map) {
|
||||
for (final stateKeyEntry in roomEntry.value.entries) {
|
||||
final stateKey = stateKeyEntry.key;
|
||||
final fallbackSlug =
|
||||
'${room.displayname}-${stateKey.isNotEmpty ? '$stateKey-' : ''}${room.id}';
|
||||
addImagePack(room.getState('im.ponies.room_emotes', stateKey),
|
||||
room: room, slug: fallbackSlug);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// finally we add all of this rooms state
|
||||
final allRoomEmotes = states['im.ponies.room_emotes'];
|
||||
if (allRoomEmotes != null) {
|
||||
for (final entry in allRoomEmotes.entries) {
|
||||
addImagePack(entry.value,
|
||||
room: this,
|
||||
slug: entry.value.stateKey.isEmpty ? 'room' : entry.value.stateKey);
|
||||
}
|
||||
}
|
||||
return packs;
|
||||
}
|
||||
|
||||
/// Get a flat view of all the image packs of a specified [usage], that is a map of all
|
||||
/// slugs to a map of the image code to their mxc url
|
||||
Map<String, Map<String, String>> getImagePacksFlat([ImagePackUsage usage]) =>
|
||||
getImagePacks(usage).map((k, v) =>
|
||||
MapEntry(k, v.images.map((k, v) => MapEntry(k, v.url.toString()))));
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ class EmoteSyntax extends InlineSyntax {
|
|||
@override
|
||||
bool onMatch(InlineParser parser, Match match) {
|
||||
final pack = match[1] ?? '';
|
||||
final emote = ':${match[2]}:';
|
||||
final emote = match[2];
|
||||
String mxc;
|
||||
if (pack.isEmpty) {
|
||||
// search all packs
|
||||
|
|
@ -84,8 +84,8 @@ class EmoteSyntax extends InlineSyntax {
|
|||
final element = Element.empty('img');
|
||||
element.attributes['data-mx-emoticon'] = '';
|
||||
element.attributes['src'] = htmlAttrEscape.convert(mxc);
|
||||
element.attributes['alt'] = htmlAttrEscape.convert(emote);
|
||||
element.attributes['title'] = htmlAttrEscape.convert(emote);
|
||||
element.attributes['alt'] = htmlAttrEscape.convert(':$emote:');
|
||||
element.attributes['title'] = htmlAttrEscape.convert(':$emote:');
|
||||
element.attributes['height'] = '32';
|
||||
element.attributes['vertical-align'] = 'middle';
|
||||
parser.addNode(element);
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ dependencies:
|
|||
base58check: ^2.0.0
|
||||
olm: ^2.0.0
|
||||
isolate: ^2.0.3
|
||||
matrix_api_lite: ^0.3.3
|
||||
matrix_api_lite: ^0.3.5
|
||||
hive: ^2.0.4
|
||||
ffi: ^1.0.0
|
||||
js: ^0.6.3
|
||||
slugify: ^2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
pedantic: ^1.11.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'fake_client.dart';
|
||||
|
||||
void main() {
|
||||
group('Image Pack', () {
|
||||
Client client;
|
||||
Room room;
|
||||
Room room2;
|
||||
var sortOrder = 0.0;
|
||||
|
||||
test('setupClient', () async {
|
||||
client = await getClient();
|
||||
room = Room(id: '!1234:fakeServer.notExisting', client: client);
|
||||
room2 = Room(id: '!abcd:fakeServer.notExisting', client: client);
|
||||
room.setState(Event(
|
||||
type: 'm.room.power_levels',
|
||||
content: {},
|
||||
room: room,
|
||||
stateKey: '',
|
||||
));
|
||||
room.setState(Event(
|
||||
type: 'm.room.member',
|
||||
content: {'membership': 'join'},
|
||||
room: room,
|
||||
stateKey: client.userID,
|
||||
));
|
||||
room2.setState(Event(
|
||||
type: 'm.room.power_levels',
|
||||
content: {},
|
||||
room: room,
|
||||
stateKey: '',
|
||||
));
|
||||
room2.setState(Event(
|
||||
type: 'm.room.member',
|
||||
content: {'membership': 'join'},
|
||||
room: room,
|
||||
stateKey: client.userID,
|
||||
));
|
||||
client.rooms.add(room);
|
||||
client.rooms.add(room2);
|
||||
});
|
||||
|
||||
test('Single room', () async {
|
||||
room.setState(Event(
|
||||
type: 'im.ponies.room_emotes',
|
||||
content: {
|
||||
'images': {
|
||||
'room_plain': {'url': 'mxc://room_plain'}
|
||||
}
|
||||
},
|
||||
room: room,
|
||||
stateKey: '',
|
||||
sortOrder: sortOrder++,
|
||||
));
|
||||
final packs = room.getImagePacks();
|
||||
expect(packs.length, 1);
|
||||
expect(packs['room'].images.length, 1);
|
||||
expect(packs['room'].images['room_plain'].url.toString(),
|
||||
'mxc://room_plain');
|
||||
var packsFlat = room.getImagePacksFlat();
|
||||
expect(packsFlat, {
|
||||
'room': {'room_plain': 'mxc://room_plain'}
|
||||
});
|
||||
room.setState(Event(
|
||||
type: 'im.ponies.room_emotes',
|
||||
content: {
|
||||
'images': {
|
||||
'emote': {
|
||||
'url': 'mxc://emote',
|
||||
'usage': ['emoticon']
|
||||
},
|
||||
'sticker': {
|
||||
'url': 'mxc://sticker',
|
||||
'usage': ['sticker']
|
||||
},
|
||||
}
|
||||
},
|
||||
room: room,
|
||||
stateKey: '',
|
||||
sortOrder: sortOrder++,
|
||||
));
|
||||
packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
|
||||
expect(packsFlat, {
|
||||
'room': {'emote': 'mxc://emote'}
|
||||
});
|
||||
packsFlat = room.getImagePacksFlat(ImagePackUsage.sticker);
|
||||
expect(packsFlat, {
|
||||
'room': {'sticker': 'mxc://sticker'}
|
||||
});
|
||||
room.setState(Event(
|
||||
type: 'im.ponies.room_emotes',
|
||||
content: {
|
||||
'images': {
|
||||
'emote': {'url': 'mxc://emote'},
|
||||
'sticker': {'url': 'mxc://sticker'},
|
||||
},
|
||||
'pack': {
|
||||
'usage': ['emoticon'],
|
||||
}
|
||||
},
|
||||
room: room,
|
||||
stateKey: '',
|
||||
sortOrder: sortOrder++,
|
||||
));
|
||||
packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
|
||||
expect(packsFlat, {
|
||||
'room': {'emote': 'mxc://emote', 'sticker': 'mxc://sticker'}
|
||||
});
|
||||
packsFlat = room.getImagePacksFlat(ImagePackUsage.sticker);
|
||||
expect(packsFlat, {});
|
||||
|
||||
room.setState(Event(
|
||||
type: 'im.ponies.room_emotes',
|
||||
content: {
|
||||
'images': {
|
||||
'fox': {'url': 'mxc://fox'},
|
||||
},
|
||||
'pack': {
|
||||
'usage': ['emoticon'],
|
||||
}
|
||||
},
|
||||
room: room,
|
||||
stateKey: 'fox',
|
||||
sortOrder: sortOrder++,
|
||||
));
|
||||
packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
|
||||
expect(packsFlat, {
|
||||
'room': {'emote': 'mxc://emote', 'sticker': 'mxc://sticker'},
|
||||
'fox': {'fox': 'mxc://fox'},
|
||||
});
|
||||
});
|
||||
|
||||
test('user pack', () async {
|
||||
client.accountData['im.ponies.user_emotes'] = BasicEvent.fromJson({
|
||||
'type': 'im.ponies.user_emotes',
|
||||
'content': {
|
||||
'images': {
|
||||
'user': {
|
||||
'url': 'mxc://user',
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
final packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
|
||||
expect(packsFlat, {
|
||||
'room': {'emote': 'mxc://emote', 'sticker': 'mxc://sticker'},
|
||||
'fox': {'fox': 'mxc://fox'},
|
||||
'user': {'user': 'mxc://user'},
|
||||
});
|
||||
});
|
||||
|
||||
test('other rooms', () async {
|
||||
room2.setState(Event(
|
||||
type: 'im.ponies.room_emotes',
|
||||
content: {
|
||||
'images': {
|
||||
'other_room_emote': {'url': 'mxc://other_room_emote'},
|
||||
},
|
||||
'pack': {
|
||||
'usage': ['emoticon'],
|
||||
}
|
||||
},
|
||||
room: room2,
|
||||
stateKey: '',
|
||||
sortOrder: sortOrder++,
|
||||
));
|
||||
client.accountData['im.ponies.emote_rooms'] = BasicEvent.fromJson({
|
||||
'type': 'im.ponies.emote_rooms',
|
||||
'content': {
|
||||
'rooms': {
|
||||
'!abcd:fakeServer.notExisting': {'': {}},
|
||||
},
|
||||
},
|
||||
});
|
||||
var packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
|
||||
expect(packsFlat, {
|
||||
'room': {'emote': 'mxc://emote', 'sticker': 'mxc://sticker'},
|
||||
'fox': {'fox': 'mxc://fox'},
|
||||
'user': {'user': 'mxc://user'},
|
||||
'empty-chat-abcdfakeservernotexisting': {
|
||||
'other_room_emote': 'mxc://other_room_emote'
|
||||
},
|
||||
});
|
||||
room2.setState(Event(
|
||||
type: 'im.ponies.room_emotes',
|
||||
content: {
|
||||
'images': {
|
||||
'other_fox': {'url': 'mxc://other_fox'},
|
||||
},
|
||||
'pack': {
|
||||
'usage': ['emoticon'],
|
||||
}
|
||||
},
|
||||
room: room2,
|
||||
stateKey: 'fox',
|
||||
sortOrder: sortOrder++,
|
||||
));
|
||||
client.accountData['im.ponies.emote_rooms'] = BasicEvent.fromJson({
|
||||
'type': 'im.ponies.emote_rooms',
|
||||
'content': {
|
||||
'rooms': {
|
||||
'!abcd:fakeServer.notExisting': {'': {}, 'fox': {}},
|
||||
},
|
||||
},
|
||||
});
|
||||
packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
|
||||
expect(packsFlat, {
|
||||
'room': {'emote': 'mxc://emote', 'sticker': 'mxc://sticker'},
|
||||
'fox': {'fox': 'mxc://fox'},
|
||||
'user': {'user': 'mxc://user'},
|
||||
'empty-chat-abcdfakeservernotexisting': {
|
||||
'other_room_emote': 'mxc://other_room_emote'
|
||||
},
|
||||
'empty-chat-fox-abcdfakeservernotexisting': {
|
||||
'other_fox': 'mxc://other_fox'
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -23,13 +23,13 @@ void main() {
|
|||
group('markdown', () {
|
||||
final emotePacks = {
|
||||
'room': {
|
||||
':fox:': 'mxc://roomfox',
|
||||
':bunny:': 'mxc://roombunny',
|
||||
'fox': 'mxc://roomfox',
|
||||
'bunny': 'mxc://roombunny',
|
||||
},
|
||||
'user': {
|
||||
':fox:': 'mxc://userfox',
|
||||
':bunny:': 'mxc://userbunny',
|
||||
':raccoon:': 'mxc://raccoon',
|
||||
'fox': 'mxc://userfox',
|
||||
'bunny': 'mxc://userbunny',
|
||||
'raccoon': 'mxc://raccoon',
|
||||
},
|
||||
};
|
||||
final mentionMap = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue