Skip to content

Commit

Permalink
Support custom emoji reactions incoming
Browse files Browse the repository at this point in the history
  • Loading branch information
singpolyma committed Oct 24, 2024
1 parent bc8a169 commit c267f59
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 14 deletions.
4 changes: 2 additions & 2 deletions snikket/Chat.hx
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ class DirectChat extends Chat {
for (areaction => senders in m.reactions) {
if (areaction != reaction && senders.contains(client.accountId())) reactions.push(areaction);
}
final update = new ReactionUpdate(ID.long(), null, m.localId, m.chatId(), Date.format(std.Date.now()), client.accountId(), reactions);
final update = new ReactionUpdate(ID.long(), null, null, m.localId, m.chatId(), Date.format(std.Date.now()), client.accountId(), reactions);
persistence.storeReaction(client.accountId(), update, (stored) -> {
final stanza = update.asStanza();
for (recipient in getParticipants()) {
Expand Down Expand Up @@ -1136,7 +1136,7 @@ class Channel extends Chat {
for (areaction => senders in m.reactions) {
if (areaction != reaction && senders.contains(getFullJid().asString())) reactions.push(areaction);
}
final update = new ReactionUpdate(ID.long(), m.serverId, null, m.chatId(), Date.format(std.Date.now()), client.accountId(), reactions);
final update = new ReactionUpdate(ID.long(), m.serverId, m.chatId(), null, m.chatId(), Date.format(std.Date.now()), client.accountId(), reactions);
persistence.storeReaction(client.accountId(), update, (stored) -> {
final stanza = update.asStanza();
stanza.attr.set("to", chatId);
Expand Down
3 changes: 3 additions & 0 deletions snikket/Client.hx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ class Client extends EventEmitter {
}
}
case ReactionUpdateStanza(update):
for (hash in update.inlineHashReferences()) {
fetchMediaByHash([hash], [from]);
}
persistence.storeReaction(accountId(), update, (stored) -> if (stored != null) notifyMessageHandlers(stored));
default:
// ignore
Expand Down
7 changes: 6 additions & 1 deletion snikket/Hash.hx
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,15 @@ class Hash {
if (Config.relativeHashUri) {
return "/.well-known/ni/" + algorithm.urlEncode() + "/" + toBase64Url();
} else {
return "ni:///" + algorithm.urlEncode() + ";" + toBase64Url();
return serializeUri();
}
}

@:allow(snikket)
private function serializeUri() {
return "ni:///" + algorithm.urlEncode() + ";" + toBase64Url();
}

public function toHex() {
return Bytes.ofData(hash).toHex();
}
Expand Down
41 changes: 41 additions & 0 deletions snikket/Message.hx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package snikket;

using Lambda;
using StringTools;

enum abstract MessageDirection(Int) {
var MessageReceived;
Expand Down Expand Up @@ -162,6 +163,7 @@ class Message {
return new Message(msg.chatId(), msg.senderId(), msg.threadId, ReactionUpdateStanza(new ReactionUpdate(
stanza.attr.get("id") ?? ID.long(),
isGroupchat ? reactionId : null,
isGroupchat ? msg.chatId() : null,
isGroupchat ? null : reactionId,
msg.chatId(),
timestamp,
Expand Down Expand Up @@ -214,6 +216,45 @@ class Message {
if (reply != null) {
final replyToJid = reply.attr.get("to");
final replyToID = reply.attr.get("id");

final text = msg.text;
if (text != null && EmojiUtil.isOnlyEmoji(text.trim())) {
return new Message(msg.chatId(), msg.senderId(), msg.threadId, ReactionUpdateStanza(new ReactionUpdate(
stanza.attr.get("id") ?? ID.long(),
isGroupchat ? replyToID : null,
isGroupchat ? msg.chatId() : null,
isGroupchat ? null : replyToID,
msg.chatId(),
timestamp,
msg.senderId(),
[text.trim()],
true
)));
}

if (html != null) {
final body = html.getChild("body", "http://www.w3.org/1999/xhtml");
if (body != null) {
final els = body.allTags();
if (els.length == 1 && els[0].name == "img") {
final hash = Hash.fromUri(els[0].attr.get("src") ?? "");
if (hash != null) {
return new Message(msg.chatId(), msg.senderId(), msg.threadId, ReactionUpdateStanza(new ReactionUpdate(
stanza.attr.get("id") ?? ID.long(),
isGroupchat ? replyToID : null,
isGroupchat ? msg.chatId() : null,
isGroupchat ? null : replyToID,
msg.chatId(),
timestamp,
msg.senderId(),
[hash.serializeUri()],
true
)));
}
}
}
}

if (replyToID != null) {
// Reply stub
final replyToMessage = new ChatMessage();
Expand Down
40 changes: 38 additions & 2 deletions snikket/ReactionUpdate.hx
Original file line number Diff line number Diff line change
@@ -1,28 +1,64 @@
package snikket;

using Lambda;

@:nullSafety(Strict)
@:expose
class ReactionUpdate {
public final updateId: String;
public final serverId: Null<String>;
public final serverIdBy: Null<String>;
public final localId: Null<String>;
public final chatId: String;
public final timestamp: String;
public final senderId: String;
public final reactions: Array<String>;
public final append: Bool;

public function new(updateId: String, serverId: Null<String>, localId: Null<String>, chatId: String, timestamp: String, senderId: String, reactions: Array<String>) {
public function new(updateId: String, serverId: Null<String>, serverIdBy: Null<String>, localId: Null<String>, chatId: String, timestamp: String, senderId: String, reactions: Array<String>, ?append: Bool = false) {
if (serverId == null && localId == null) throw "ReactionUpdate serverId and localId cannot both be null";
if (serverId != null && serverIdBy == null) throw "serverId requires serverIdBy";
this.updateId = updateId;
this.serverId = serverId;
this.serverIdBy = serverIdBy;
this.localId = localId;
this.chatId = chatId;
this.timestamp = timestamp;
this.senderId = senderId;
this.reactions = reactions;
this.append = append ?? false;
}

public function getReactions(existingReactions: Null<Array<String>>): Array<String> {
if (append) {
final set: Map<String, Bool> = [];
for (r in existingReactions ?? []) {
set[r] = true;
}
for (r in reactions) {
set[r] = true;
}
return { iterator: () -> set.keys() }.array();
} else {
return reactions;
}
}

@:allow(snikket)
private function inlineHashReferences() {
final hashes = [];
for (r in reactions) {
final hash = Hash.fromUri(r);
if (hash != null) hashes.push(hash);
}
return hashes;
}

// Note that using this version means you don't get any fallbacks!
public function asStanza():Stanza {
@:allow(snikket)
private function asStanza():Stanza {
if (append) throw "Cannot make a reaction XEP stanza for an append";

var attrs: haxe.DynamicAccess<String> = { type: serverId == null ? "chat" : "groupchat", id: updateId };
var stanza = new Stanza("message", attrs);

Expand Down
26 changes: 17 additions & 9 deletions snikket/persistence/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,21 +307,20 @@ const browser = (dbname, tokenize, stemmer) => {
const reactionStore = tx.objectStore("reactions");
let result;
if (update.serverId) {
result = await promisifyRequest(store.openCursor(IDBKeyRange.bound([account, update.serverId], [account, update.serverId, []])));
result = await promisifyRequest(store.openCursor(IDBKeyRange.bound([account, update.serverId, update.serverIdBy], [account, update.serverId, update.serverIdBy, []])));
} else {
result = await promisifyRequest(store.index("localId").openCursor(IDBKeyRange.only([account, update.localId, update.chatId])));
}
await promisifyRequest(reactionStore.put({...update, messageId: update.serverId || update.localId, timestamp: new Date(update.timestamp), account: account}));
if (!result || !result.value) {
return null;
}
const message = result.value;
const lastFromSender = promisifyRequest(reactionStore.index("senders").openCursor(IDBKeyRange.bound(
[account, update.chatId, update.serverId || update.localId, update.senderId],
[account, update.chatId, update.serverId || update.localId, update.senderId, []]
), "prev"));
const reactions = update.getReactions(lastFromSender?.value?.reactions);
await promisifyRequest(reactionStore.put({...update, reactions: reactions, append: (update.append ? update.reactions : null), messageId: update.serverId || update.localId, timestamp: new Date(update.timestamp), account: account}));
if (!result || !result.value) return null;
if (lastFromSender?.value && lastFromSender.value.timestamp > new Date(update.timestamp)) return;
setReactions(message.reactions, update.senderId, update.reactions);
const message = result.value;
setReactions(message.reactions, update.senderId, reactions);
store.put(message);
return await hydrateMessage(message);
})().then(callback);
Expand All @@ -339,8 +338,17 @@ const browser = (dbname, tokenize, stemmer) => {
message.replyToMessage = replyToMessage;
const tx = db.transaction(["messages", "reactions"], "readwrite");
const store = tx.objectStore("messages");
return promisifyRequest(store.index("localId").openCursor(IDBKeyRange.only([account, message.localId || [], message.chatId()]))).then((result) => {
if (result?.value && !message.isIncoming() && result?.value.direction === enums.MessageDirection.MessageSent && message.versions.length < 1) {
return Promise.all([
promisifyRequest(store.index("localId").openCursor(IDBKeyRange.only([account, message.localId || [], message.chatId()]))),
promisifyRequest(tx.objectStore("reactions").openCursor(IDBKeyRange.only([account, message.chatId(), message.senderId(), message.localId])))
]).then(([result, reactionResult]) => {
if (reactionResult?.value?.append && message.html().trim() == "") {
this.getMessage(account, message.chatId(), reactionResult.value.serverId, reactionResult.value.localId, (reactToMessage) => {
const reactions = Array.from(reactToMessage.reactions.keys()).filter((r) => !reactionResult.value.append.includes(r));
this.storeReaction(account, new snikket.ReactionUpdate(message.localId, reactionResult.value.serverId, reactionResult.value.serverIdBy, reactionResult.value.localId, message.chatId(), message.timestamp, message.senderId(), reactions), callback);
});
return true;
} else if (result?.value && !message.isIncoming() && result?.value.direction === enums.MessageDirection.MessageSent && message.versions.length < 1) {
// Duplicate, we trust our own sent ids
return promisifyRequest(result.delete());
} else if (result?.value && (result.value.sender == message.senderId() || result.value.type == enums.MessageType.MessageCall) && (message.versions.length > 0 || (result.value.versions || []).length > 0)) {
Expand Down

0 comments on commit c267f59

Please sign in to comment.