Skip to content

Commit

Permalink
Store timestamp/alt text with reactions
Browse files Browse the repository at this point in the history
Also fixes a bug where sending a new set of emoji reaction would erase
any custom emoji reaction.

Sending a new set of emoji reaction will still erase any append-style
emoji reaction but the assumption is you wanted that since you sent a
whole new set of emoji. But custom can only be sent as append.
  • Loading branch information
singpolyma committed Nov 14, 2024
1 parent 2751744 commit 7adcfb3
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 47 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ npm/snikket-browser.js:
sed -i 's/snikket\.MessageType/enums.MessageType/g' npm/snikket-browser.d.ts
sed -i 's/snikket\.UserState/enums.UserState/g' npm/snikket-browser.d.ts
sed -i 's/snikket\.ChatMessageEvent/enums.ChatMessageEvent/g' npm/snikket-browser.d.ts
sed -i 's/snikket\.ReactionUpdateKind/enums.ReactionUpdateKind/g' npm/snikket-browser.d.ts
sed -i 's/_Push.Push_Fields_/Push/g' npm/snikket-browser.d.ts
sed -i '1ivar exports = {};' npm/snikket-browser.js
echo "export const snikket = exports.snikket;" >> npm/snikket-browser.js
Expand All @@ -29,6 +30,7 @@ npm/snikket.js:
sed -i 's/snikket\.MessageType/enums.MessageType/g' npm/snikket.d.ts
sed -i 's/snikket\.UserState/enums.UserState/g' npm/snikket.d.ts
sed -i 's/snikket\.ChatMessageEvent/enums.ChatMessageEvent/g' npm/snikket.d.ts
sed -i 's/snikket\.ReactionUpdateKind/enums.ReactionUpdateKind/g' npm/snikket.d.ts
sed -i 's/_Push.Push_Fields_/Push/g' npm/snikket.d.ts
sed -i '1iimport { createRequire } from "module";' npm/snikket.js
sed -i '1iglobal.require = createRequire(import.meta.url);' npm/snikket.js
Expand Down
3 changes: 3 additions & 0 deletions npm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ export import ChatAttachment = snikket.ChatAttachment;
export import ChatMessage = snikket.ChatMessage;
export import Client = snikket.Client;
export import Config = snikket.Config;
export import CustomEmojiReaction = snikket.CustomEmojiReaction;
export import DirectChat = snikket.DirectChat;
export import Hash = snikket.Hash;
export import Identicon = snikket.Identicon;
export import Identity = snikket.Identity;
export import Notification = snikket.Notification;
export import Participant = snikket.Participant;
export import Push = snikket.Push;
export import Reaction = snikket.Reaction;
export import SerializedChat = snikket.SerializedChat;
export import jingle = snikket.jingle;
export const VERSION = snikket.Version.HUMAN;
Expand All @@ -26,6 +28,7 @@ export import ChatMessageEvent = enums.ChatMessageEvent;
export import MessageDirection = enums.MessageDirection;
export import MessageStatus = enums.MessageStatus;
export import MessageType = enums.MessageType;
export import ReactionUpdateKind = enums.ReactionUpdateKind;
export import UiState = enums.UiState;
export import UserState = enums.UserState;

Expand Down
21 changes: 15 additions & 6 deletions snikket/Chat.hx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import snikket.GenericStream;
import snikket.ID;
import snikket.Message;
import snikket.MessageSync;
import snikket.Reaction;
import snikket.jingle.PeerConnection;
import snikket.jingle.Session;
import snikket.queries.DiscoInfoGet;
Expand Down Expand Up @@ -768,10 +769,15 @@ class DirectChat extends Chat {
public function removeReaction(m:ChatMessage, reaction:String) {
// NOTE: doing it this way means no fallback behaviour
final reactions = [];
for (areaction => senders in m.reactions) {
if (areaction != reaction && senders.contains(client.accountId())) reactions.push(areaction);
for (areaction => reacts in m.reactions) {
if (areaction != reaction) {
final react = reacts.find(r -> r.senderId == client.accountId());
if (react != null && !Std.is(react, CustomEmojiReaction)) {
reactions.push(react);
}
}
}
final update = new ReactionUpdate(ID.long(), null, 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(), client.accountId(), Date.format(std.Date.now()), reactions, EmojiReactions);
persistence.storeReaction(client.accountId(), update, (stored) -> {
final stanza = update.asStanza();
for (recipient in getParticipants()) {
Expand Down Expand Up @@ -1194,10 +1200,13 @@ class Channel extends Chat {
public function removeReaction(m:ChatMessage, reaction:String) {
// NOTE: doing it this way means no fallback behaviour
final reactions = [];
for (areaction => senders in m.reactions) {
if (areaction != reaction && senders.contains(getFullJid().asString())) reactions.push(areaction);
for (areaction => reacts in m.reactions) {
if (areaction != reaction) {
final react = reacts.find(r -> r.senderId == getFullJid().asString());
if (react != null && !Std.is(react, CustomEmojiReaction)) reactions.push(react);
}
}
final update = new ReactionUpdate(ID.long(), m.serverId, m.chatId(), null, m.chatId(), Date.format(std.Date.now()), client.accountId(), reactions);
final update = new ReactionUpdate(ID.long(), m.serverId, m.chatId(), null, m.chatId(), getFullJid().asString(), Date.format(std.Date.now()), reactions, EmojiReactions);
persistence.storeReaction(client.accountId(), update, (stored) -> {
final stanza = update.asStanza();
stanza.attr.set("to", chatId);
Expand Down
6 changes: 3 additions & 3 deletions snikket/ChatMessage.hx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class ChatMessage {
Map of reactions to this message
**/
@HaxeCBridge.noemit
public var reactions: Map<String, Array<String>> = [];
public var reactions: Map<String, Array<Reaction>> = [];

/**
Body text of this message or NULL
Expand Down Expand Up @@ -435,8 +435,8 @@ class ChatMessage {
stanza.textTag("reaction", reaction);
addedReactions[reaction] = true;

for (areaction => senders in replyToM.reactions) {
if (!(addedReactions[areaction] ?? false) && senders.contains(senderId())) {
for (areaction => reactions in replyToM.reactions) {
if (!(addedReactions[areaction] ?? false) && reactions.find(r -> r.senderId == senderId()) != null) {
addedReactions[areaction] = true;
stanza.textTag("reaction", areaction);
}
Expand Down
18 changes: 10 additions & 8 deletions snikket/Message.hx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package snikket;

import snikket.Reaction;
using Lambda;
using StringTools;

Expand Down Expand Up @@ -166,9 +167,10 @@ class Message {
isGroupchat ? msg.chatId() : null,
isGroupchat ? null : reactionId,
msg.chatId(),
timestamp,
msg.senderId(),
reactions
timestamp,
reactions.map(text -> new Reaction(msg.senderId(), timestamp, text)),
EmojiReactions
)));
}
}
Expand Down Expand Up @@ -225,10 +227,10 @@ class Message {
isGroupchat ? msg.chatId() : null,
isGroupchat ? null : replyToID,
msg.chatId(),
timestamp,
msg.senderId(),
[text.trim()],
true
timestamp,
[new Reaction(msg.senderId(), timestamp, text.trim())],
AppendReactions
)));
}

Expand All @@ -245,10 +247,10 @@ class Message {
isGroupchat ? msg.chatId() : null,
isGroupchat ? null : replyToID,
msg.chatId(),
timestamp,
msg.senderId(),
[hash.serializeUri()],
true
timestamp,
[new CustomEmojiReaction(msg.senderId(), timestamp, els[0].attr.get("alt") ?? "", hash.serializeUri())],
AppendReactions
)));
}
}
Expand Down
36 changes: 36 additions & 0 deletions snikket/Reaction.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package snikket;

@:nullSafety(Strict)
@:expose
class Reaction {
public final senderId: String;
public final timestamp: String;
public final text: String;
public final key: String;

public function new(senderId: String, timestamp: String, text: String, key: Null<String> = null) {
this.senderId = senderId;
this.timestamp = timestamp;
this.text = text;
this.key = key ?? text;
}

public function render<T>(forText: (String) -> T, forImage: (String, String) -> T) {
return forText(text + "\u{fe0f}");
}
}

@:expose
class CustomEmojiReaction extends Reaction {
public final uri: String;

public function new(senderId: String, timestamp: String, text: String, uri: String) {
super(senderId, timestamp, text, uri);
this.uri = uri;
}

override public function render<T>(forText: (String) -> T, forImage: (String, String) -> T) {
final hash = Hash.fromUri(uri);
return forImage(text, hash?.toUri() ?? uri);
}
}
55 changes: 39 additions & 16 deletions snikket/ReactionUpdate.hx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package snikket;

import snikket.Reaction;
using Lambda;

enum abstract ReactionUpdateKind(Int) {
var EmojiReactions;
var AppendReactions;
var CompleteReactions;
}

@:nullSafety(Strict)
@:expose
class ReactionUpdate {
Expand All @@ -10,61 +17,77 @@ class ReactionUpdate {
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 final timestamp: String;
public final reactions: Array<Reaction>;
public final kind: ReactionUpdateKind;

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) {
public function new(updateId: String, serverId: Null<String>, serverIdBy: Null<String>, localId: Null<String>, chatId: String, senderId: String, timestamp: String, reactions: Array<Reaction>, kind: ReactionUpdateKind) {
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.timestamp = timestamp;
this.reactions = reactions;
this.append = append ?? false;
this.kind = kind;
}

public function getReactions(existingReactions: Null<Array<String>>): Array<String> {
if (append) {
public function getReactions(existingReactions: Null<Array<Reaction>>): Array<Reaction> {
if (kind == AppendReactions) { // TODO: make sure a new non-custom react doesn't override any customs we've added
final set: Map<String, Bool> = [];
final list = [];
for (r in existingReactions ?? []) {
set[r] = true;
if (!set.exists(r.key)) list.push(r);
set[r.key] = true;
}
for (r in reactions) {
set[r] = true;
if (!set.exists(r.key)) list.push(r);
set[r.key] = true;
}
return list;
} else if (kind == EmojiReactions) {
// Complete set of emoji but lacks any customs added before now
final list = reactions.array();
for (r in existingReactions ?? []) {
final custom = Util.downcast(r, CustomEmojiReaction);
if (custom != null) list.push(custom);
}
return { iterator: () -> set.keys() }.array();
} else {
return list;
} else if (kind == CompleteReactions) {
return reactions;
}
throw "Unknown kind of reaction update";
}

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

// Note that using this version means you don't get any fallbacks!
// It also won't update any custom emoji reactions at all
@:allow(snikket)
private function asStanza():Stanza {
if (append) throw "Cannot make a reaction XEP stanza for an append";
if (kind != EmojiReactions) throw "Cannot make a reaction XEP stanza for this kind";

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

stanza.tag("reactions", { xmlns: "urn:xmpp:reactions:0", id: localId ?? serverId });
for (reaction in reactions) {
stanza.textTag("reaction", reaction);
if (!Std.is(reaction, CustomEmojiReaction)) stanza.textTag("reaction", reaction.text);
}
stanza.up();

Expand Down
Loading

0 comments on commit 7adcfb3

Please sign in to comment.