allow sending emoji/stickers from emote packs
This commit is contained in:
parent
868023a4a1
commit
3754bc2feb
7 changed files with 701 additions and 74 deletions
25
lib/controllers/all_emoji.dart
Normal file
25
lib/controllers/all_emoji.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:material_emoji_picker/material_emoji_picker.dart";
|
||||
import "package:nexus/controllers/image_packs.dart";
|
||||
import "package:nexus/helpers/extensions/image_pack_emoji.dart";
|
||||
|
||||
class AllEmojiController(final String roomId)
|
||||
extends AsyncNotifier<IList<EmojiCategory>> {
|
||||
@override
|
||||
Future<IList<EmojiCategory>> build() async {
|
||||
final packs = await ref.watch(ImagePacksController.provider(roomId).future);
|
||||
final unicode = await ref.watch(EmojiController.provider.future);
|
||||
|
||||
return packs
|
||||
.map((pack) => pack.emojiCategory)
|
||||
.nonNulls
|
||||
.toIList()
|
||||
.addAll(unicode);
|
||||
}
|
||||
|
||||
static final provider = AsyncNotifierProvider.autoDispose
|
||||
.family<AllEmojiController, IList<EmojiCategory>, String>(
|
||||
AllEmojiController.new,
|
||||
);
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import "package:nexus/models/requests/get_event_context.dart";
|
|||
import "package:nexus/models/requests/get_mentions.dart";
|
||||
import "package:nexus/models/requests/get_related_events.dart";
|
||||
import "package:nexus/models/requests/get_room_state.dart";
|
||||
import "package:nexus/models/requests/get_specific_room_state.dart";
|
||||
import "package:nexus/models/requests/get_space_hierarchy.dart";
|
||||
import "package:nexus/models/requests/join_room.dart";
|
||||
import "package:nexus/models/profile_response.dart";
|
||||
|
|
@ -185,11 +186,12 @@ class ClientController extends AsyncNotifier<int> {
|
|||
...json,
|
||||
"content": {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
...((content["m.relates_to"] as Map<String, dynamic>?) ?? {}),
|
||||
"event_id": request.relatesTo,
|
||||
"rel_type": request.relationType,
|
||||
},
|
||||
if (request.relatesTo != null)
|
||||
"m.relates_to": {
|
||||
...((content["m.relates_to"] as Map<String, dynamic>?) ?? {}),
|
||||
"event_id": request.relatesTo,
|
||||
"rel_type": request.relationType,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -265,6 +267,15 @@ class ClientController extends AsyncNotifier<int> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<IList<Event>> getSpecificRoomState(
|
||||
GetSpecificRoomStateRequest request,
|
||||
) async => .new(
|
||||
((await _sendCommand("get_specific_room_state", request.toJson()))
|
||||
as List? ??
|
||||
[])
|
||||
.map((event) => .fromJson(event)),
|
||||
);
|
||||
|
||||
Future<IList<Event>?> getRelatedEvents(
|
||||
GetRelatedEventsRequest request,
|
||||
) async {
|
||||
|
|
|
|||
112
lib/controllers/image_pack_sources.dart
Normal file
112
lib/controllers/image_pack_sources.dart
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/account_data.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/controllers/space_edges.dart";
|
||||
import "package:nexus/models/content/content.dart";
|
||||
import "package:nexus/models/requests/get_specific_room_state.dart";
|
||||
|
||||
typedef ImagePackSource = ({String roomId, String? stateKey});
|
||||
|
||||
class ImagePackSourcesController(final String roomId)
|
||||
extends AsyncNotifier<IList<ImagePackSource>> {
|
||||
@override
|
||||
Future<IList<ImagePackSource>> build() async {
|
||||
final enabledPacks = ref.watch(
|
||||
AccountDataController.provider.select(
|
||||
(data) =>
|
||||
(data.imagePackRooms ?? data.legacyImagePackRooms)?.rooms.map(
|
||||
(roomId, packs) => .new(roomId, packs.keys.toISet()),
|
||||
) ??
|
||||
const .empty(),
|
||||
),
|
||||
);
|
||||
final spaceEdges = ref.watch(SpaceEdgesController.provider);
|
||||
final directParents = ref.watch(
|
||||
RoomsController.provider.select(
|
||||
(rooms) =>
|
||||
rooms[roomId]?.state["m.space.parent"]?.keys.toISet() ??
|
||||
const .empty(),
|
||||
),
|
||||
);
|
||||
|
||||
Iterable<String> parentsOf(String childId) => spaceEdges.entries
|
||||
.where((entry) => entry.value.any((edge) => edge.childId == childId))
|
||||
.map((entry) => entry.key)
|
||||
.followedBy(childId == roomId ? directParents : const []);
|
||||
|
||||
ISet<String> withAncestors(ISet<String> spaces, String childId) =>
|
||||
parentsOf(childId).fold(
|
||||
spaces,
|
||||
(spaces, parentId) => spaces.contains(parentId)
|
||||
? spaces
|
||||
: withAncestors(spaces.add(parentId), parentId),
|
||||
);
|
||||
|
||||
final rooms = ref.read(RoomsController.provider);
|
||||
final spaces = withAncestors(
|
||||
const .empty(),
|
||||
roomId,
|
||||
).remove(roomId).where(rooms.containsKey).toIList();
|
||||
|
||||
final client = ref.read(ClientController.provider.notifier);
|
||||
final roomsNotifier = ref.read(RoomsController.provider.notifier);
|
||||
|
||||
await Future.wait(
|
||||
spaces
|
||||
.where((spaceId) => !rooms[spaceId]!.hasFetchedState)
|
||||
.map(
|
||||
(spaceId) async => roomsNotifier.addState(
|
||||
spaceId,
|
||||
await client.getRoomState(.new(roomId: spaceId)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const packTypes = <EventType>[.imagePack, .legacyImagePack];
|
||||
// Rooms we aren't in can't be fetched, and fetched rooms have every pack
|
||||
final missingKeys = enabledPacks.entries
|
||||
.where((entry) => !(rooms[entry.key]?.hasFetchedState ?? true))
|
||||
.expand(
|
||||
(entry) => entry.value
|
||||
.where(
|
||||
(stateKey) => packTypes.every(
|
||||
(type) =>
|
||||
rooms[entry.key]?.state[type.type]?[stateKey] == null,
|
||||
),
|
||||
)
|
||||
.expand(
|
||||
(stateKey) => packTypes.map(
|
||||
(type) => RoomStateKey(
|
||||
roomId: entry.key,
|
||||
type: type.type,
|
||||
stateKey: stateKey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toIList();
|
||||
|
||||
if (missingKeys.isNotEmpty) {
|
||||
roomsNotifier.addSpecificState(
|
||||
await client.getSpecificRoomState(.new(keys: missingKeys)),
|
||||
);
|
||||
}
|
||||
|
||||
return .new([
|
||||
(roomId: roomId, stateKey: null),
|
||||
...spaces.map((spaceId) => (roomId: spaceId, stateKey: null)),
|
||||
...enabledPacks.entries.expand(
|
||||
(entry) => entry.value.map(
|
||||
(stateKey) => (roomId: entry.key, stateKey: stateKey),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
static final provider = AsyncNotifierProvider.autoDispose
|
||||
.family<ImagePackSourcesController, IList<ImagePackSource>, String>(
|
||||
ImagePackSourcesController.new,
|
||||
);
|
||||
}
|
||||
72
lib/controllers/image_packs.dart
Normal file
72
lib/controllers/image_packs.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/image_pack_sources.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/models/content/content.dart";
|
||||
import "package:nexus/models/content/image_pack.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/models/image_pack.dart";
|
||||
|
||||
class ImagePacksController(final String roomId)
|
||||
extends AsyncNotifier<IList<ImagePack>> {
|
||||
@override
|
||||
Future<IList<ImagePack>> build() async {
|
||||
final sources = await ref.watch(
|
||||
ImagePackSourcesController.provider(roomId).future,
|
||||
);
|
||||
|
||||
final packs = ref.watch(
|
||||
RoomsController.provider.select(
|
||||
(rooms) => sources.expand((source) {
|
||||
final room = rooms[source.roomId];
|
||||
if (room == null) return const <ImagePack>[];
|
||||
|
||||
final newPacks = room.state[EventType.imagePack.type];
|
||||
final legacyPacks = room.state[EventType.legacyImagePack.type];
|
||||
final roomName = room.metadata?.name ?? source.roomId;
|
||||
|
||||
return switch (source.stateKey) {
|
||||
final stateKey? => ISet([stateKey]),
|
||||
null => ISet(
|
||||
newPacks?.keys ?? const <String>[],
|
||||
).addAll(legacyPacks?.keys ?? const []),
|
||||
}
|
||||
.map(
|
||||
(stateKey) =>
|
||||
switch (newPacks?[stateKey] ?? legacyPacks?[stateKey]) {
|
||||
final rowId? => switch (room.events[rowId]) {
|
||||
Event(
|
||||
redactedBy: null,
|
||||
content: final ImagePackContent content,
|
||||
) =>
|
||||
ImagePack.fromContent(
|
||||
content,
|
||||
id: "${source.roomId}/$stateKey",
|
||||
fallbackName: stateKey.isEmpty
|
||||
? roomName
|
||||
: "$roomName - $stateKey",
|
||||
),
|
||||
_ => null,
|
||||
},
|
||||
null => null,
|
||||
},
|
||||
)
|
||||
.nonNulls;
|
||||
}).toIList(),
|
||||
),
|
||||
);
|
||||
|
||||
return .new(
|
||||
packs
|
||||
.whereNoDuplicates(by: (pack) => pack.id)
|
||||
.where(
|
||||
(pack) => pack.emoticons.isNotEmpty || pack.stickers.isNotEmpty,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static final provider = AsyncNotifierProvider.autoDispose
|
||||
.family<ImagePacksController, IList<ImagePack>, String>(
|
||||
ImagePacksController.new,
|
||||
);
|
||||
}
|
||||
|
|
@ -23,7 +23,13 @@ class RecentEmojiController extends Notifier<IList<RecentEmoji>> {
|
|||
),
|
||||
);
|
||||
|
||||
return .new(recents.where((recent) => emoji.contains(recent.emoji)));
|
||||
return .new(
|
||||
recents.where(
|
||||
(recent) =>
|
||||
// Is unicode or custom emote
|
||||
emoji.contains(recent.emoji) || recent.emoji.startsWith("mxc://"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> add(String emoji) => ref
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import "package:nexus/controllers/attachment.dart";
|
|||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/models/content/content.dart";
|
||||
import "package:nexus/models/content/image_pack.dart";
|
||||
import "package:nexus/models/content/reaction.dart";
|
||||
import "package:nexus/models/content/sticker.dart";
|
||||
import "package:nexus/models/direction.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/models/requests/redact_event.dart";
|
||||
|
|
@ -315,14 +317,24 @@ class RoomChatController(final ChatParams info)
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> sendReaction(String reaction, Event event) async {
|
||||
/// Reacts to [event] with [reaction], which is an mxc URL for custom emoji,
|
||||
/// with their [shortcode].
|
||||
Future<void> sendReaction(
|
||||
String reaction,
|
||||
Event event, {
|
||||
String? shortcode,
|
||||
}) async {
|
||||
final client = ref.read(ClientController.provider.notifier);
|
||||
|
||||
await client.sendEvent(
|
||||
.new(
|
||||
roomId: info.roomId,
|
||||
type: EventType.reaction.type,
|
||||
content: ReactionContent(key: reaction),
|
||||
// Wrapped in colons, like other clients send it
|
||||
content: ReactionContent(
|
||||
key: reaction,
|
||||
shortcode: shortcode == null ? null : ":$shortcode:",
|
||||
),
|
||||
synchronous: true,
|
||||
disableEncryption: true,
|
||||
relatesTo: event.eventId,
|
||||
|
|
@ -331,6 +343,33 @@ class RoomChatController(final ChatParams info)
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> sendSticker(String shortcode, PackImage image) async {
|
||||
final event = await ref
|
||||
.read(ClientController.provider.notifier)
|
||||
.sendEvent(
|
||||
.new(
|
||||
roomId: info.roomId,
|
||||
type: EventType.sticker.type,
|
||||
content: StickerContent(
|
||||
body: image.body ?? shortcode,
|
||||
info: image.info ?? const .new(),
|
||||
url: image.url,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
ref
|
||||
.read(RoomsController.provider.notifier)
|
||||
.update(
|
||||
.new({
|
||||
info.roomId: .new(
|
||||
events: .new({event.rowId: event}),
|
||||
clientSticky: .new({event.rowId}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static final provider = AsyncNotifierProvider.family
|
||||
.autoDispose<RoomChatController, RoomChat?, ChatParams>(
|
||||
RoomChatController.new,
|
||||
|
|
|
|||
|
|
@ -11,29 +11,35 @@ class RoomsController extends Notifier<IMap<String, Room>> {
|
|||
@override
|
||||
IMap<String, Room> build() => .new();
|
||||
|
||||
void addState(
|
||||
String roomId,
|
||||
IList<Event> state, {
|
||||
bool isMembers = false,
|
||||
}) => update(
|
||||
.new({
|
||||
roomId: Room(
|
||||
events: .fromEntries(state.map((event) => .new(event.rowId, event))),
|
||||
hasFetchedState: true,
|
||||
hasFetchedMembers: isMembers,
|
||||
state: .new(
|
||||
state
|
||||
.groupListsBy((event) => event.type)
|
||||
.map(
|
||||
(type, group) => .new(
|
||||
type,
|
||||
.fromEntries(group.map((e) => .new(e.stateKey!, e.rowId))),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
.new(),
|
||||
void addState(String roomId, IList<Event> state, {bool isMembers = false}) =>
|
||||
update(
|
||||
.new({
|
||||
roomId: stateRoom(state)
|
||||
.copyWith(hasFetchedState: true, hasFetchedMembers: isMembers),
|
||||
}),
|
||||
.new(),
|
||||
);
|
||||
|
||||
void addSpecificState(Iterable<Event> state) => update(
|
||||
.new(
|
||||
state
|
||||
.groupListsBy((event) => event.roomId)
|
||||
.map((roomId, roomState) => .new(roomId, stateRoom(roomState))),
|
||||
),
|
||||
);
|
||||
|
||||
static Room stateRoom(Iterable<Event> state) => Room(
|
||||
events: .fromEntries(state.map((event) => .new(event.rowId, event))),
|
||||
state: .new(
|
||||
state
|
||||
.groupListsBy((event) => event.type)
|
||||
.map(
|
||||
(type, group) => .new(
|
||||
type,
|
||||
.fromEntries(group.map((e) => .new(e.stateKey!, e.rowId))),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
static IMap<String, IList<ReadReceipt>> mergeReceipts(
|
||||
|
|
|
|||
Loading…
Reference in a new issue