broadcast channel reactions - #8450
Conversation
c8015bc to
8831752
Compare
this reverts 4c75f2d, esp. in small channels, it is weird that there is an additional person reacting, always then minutes after you reacted with the same reaction. a ghost 👻
…ications for reactions is usually not wanted
f913680 to
21f00c0
Compare
|
|
||
| // Alice broadcasts recent reaction changes to Bob and Claire. | ||
| // On the wire, the hidden message has a header like | ||
| // `Broadcast-Reactions: {"messages":[{"id":"123@adc","reactions":[{"emoji":"🏳️🌈","count":1}]}]}` |
There was a problem hiding this comment.
A separate test for serialization/deserialization with such example would be nice to have, this is the first thing i looked for after seeing the wire format structures. This will also make sure we don't accidentally change the format by tweaking serde settings, refactoring the structures etc.
| } | ||
| #[derive(Debug, Serialize, Deserialize)] | ||
| struct WireMessage { | ||
| id: String, |
There was a problem hiding this comment.
This needs a doc comment saying that this is an rfc724_mid of the message, this is unclear without reading receive_broadcast_reactions.
| /// Seconds between sending out accumulated reaction updates for broadcast channels from `reactions_need_broadcast` table | ||
| const REACTION_BROADCAST_PERIOD: i64 = 10 * 60; | ||
|
|
||
| /// Starts broadcasting if last broadcasting so more than `REACTION_BROADCAST_PERIOD` seconds in the past. |
There was a problem hiding this comment.
| /// Starts broadcasting if last broadcasting so more than `REACTION_BROADCAST_PERIOD` seconds in the past. | |
| /// Starts broadcasting if last broadcasting is more than `REACTION_BROADCAST_PERIOD` seconds in the past. |
| .get_config_i64(Config::LastReactionsBroadcast) | ||
| .await?; | ||
| let next_broadcast_time = last_broadcast_time.saturating_add(REACTION_BROADCAST_PERIOD); | ||
| if next_broadcast_time <= time() { |
There was a problem hiding this comment.
Standard comment for this kind of code: if last_broadcast_time is in the future, this should also broadcast, so we are not stuck not broadcasting for the next year if the user has accidentally set the year to +1 then rewinded the clock back.
| let next_broadcast_time = last_broadcast_time.saturating_add(REACTION_BROADCAST_PERIOD); | ||
| if next_broadcast_time <= time() { | ||
| context | ||
| .set_config_internal(Config::LastReactionsBroadcast, Some(&time().to_string())) |
There was a problem hiding this comment.
time() is called twice, so this can be moved to the beginning of the function with let now = time().
| pub(crate) async fn save_broadcast_reactions( | ||
| context: &Context, | ||
| msg_id: MsgId, | ||
| frequencies: &Vec<ReactionFrequency>, |
There was a problem hiding this comment.
I would make a separate type instead of ReactionFrequency here. ReactionFrequency is a public type meant for API, and it contains e.g. is_from_self which is not stored anywhere.
Something like this, and not pub (pub(crate) or better local to broadcast_reactions):
/// Broadcast reactions as stored in the database.
#[derive(Debug)]
pub(crate) struct BroadcastReactions {
/// The reaction emoji.
pub reaction: Reaction,
/// Number of contacts that reacted with this emoji.
pub count: usize,
}
Then you don't need comments like is_from_self: false, // set in refine_frequencies() when save_broadcast_reactions is called immediately after.
|
|
||
| /// Merge `by_contact` status to broadcasted reaction frequencies. | ||
| pub(crate) fn refine_frequencies( | ||
| mut broadcasted_reactions: Vec<ReactionFrequency>, |
There was a problem hiding this comment.
This could accept a vector of BroadcastReactions and return a vector of ReactionFrequency.
| normalize_text(&chat_name), | ||
| &grpid, | ||
| timestamp, | ||
| MuteDuration::Forever, |
There was a problem hiding this comment.
Note (for other reviewers as well): this does not seem to be explicitly tested, but there is a "muted" emoji in snapshot tests.
| /// For Messages: Render message as a RFC 9078 reaction. | ||
| Reaction = b'x', | ||
|
|
||
| /// For Messages: Additional reactions that go to the BroadcastReactions: header |
There was a problem hiding this comment.
| /// For Messages: Additional reactions that go to the BroadcastReactions: header | |
| /// For Messages: Additional reactions that go to the Broadcast-Reactions: header |
|
|
||
| /// Broadcasted reactions for this or other chat messages. | ||
| /// See broadcast_reactions.rs for the wire format. | ||
| BroadcastReactions, |
There was a problem hiding this comment.
Maybe should be Chat-Broadcast-Reactions so we have everything in the Chat- namespace.
this PR adds support for reactions in broadcast channels.
the PR is quite large, but a good share are tests and otherwise many things are straight forward.
review should be done by-file, not by-commit. to make review easier, here is a high-level overview:
first a change in the existing internal
Reactionsobject was required. before this PR,Reactionshad a "contact to reaction map" only, and the "frequencies map", that are actually mainly needed for UI, were calculated as needed. with this PR, the "frequencies map" is the field that always exist, the "contact map" is only available on top of that.moreover, this PR shifts that part to the core, it was unfortunately in the bindings before.
with that preparation things done as follows:
reactions from broadcast channel subscriber (
Chattype::InBroadcast) to broadcast channel owner (Chattype::OutBroadcast) are sent as usual, only change for that step was to allow sending them at allthe owner receives reactions and saves them to the existing
reactionstable as usual. additionally, the changed message is remembered inreactions_need_broadcasttablein the IMAP loop, when ~10 minutes have passed, and
reactions_need_broadcastcontains entries, a single, hidden message with accumulated reactions is sent. this message may contain reactions to different messages. for each message, all known reactions are sent as reaction+count.subscriber receive that message and save the accumulated reactions in
reactions_broadcastedget_message_reactionsis adapted so thatfrequenciesare set independently of who-reacted-what (the old and only field). who-reacted-what is calledby_contactnow. it is always set for compatibility reasons, however, it is not exhaustive for subscribers.in general, UI should work with frequencies, the API itself, however, has not changed.
other tweaks:
outgoing channels are muted on creation, and UI shall allow to unmute/mute them as all other chats. reason is that reactions are notified, but in many cases not of large interest. this is also what telegram is doing
to have an intermediate feedback when reacting, the local state should include ones own reaction, even if it is not yet broadcasted. for that, we modify
reactions_broadcastedusingmodify_frequencies()as needed when sending an reaction. there are still some situations where the update may not include ones own reaction, in this case it is added lately byrefine_frequencies(), so thatget_message_reactions()always contain SELF.(in a first implementation, we always increased SELF reaction in refine_frequencies(), however, that was worse and led to SELF counted twice once the owner sent broadcast)
wire format
wire format is a JSON in the
Broadcast-Reactions:header. additionally,Content-Disposition: reactionis set to not show the hidden message on existing devices.using a header also allows us to broadcast reactions with resent channel messages (on joining) later.
for
id, the wire format needs to userfc724_midasmsg_idare local only.known issues
if the channel owner uses multiple devices, broadcasted reaction updates are sent from each device. the updates are not that big, so that is probably not a big deal. if it turns out that this is an issue, we can think about fixes in another PR. might be done by restarting our 10-minute-wait once we see an update from another device
we cannot set contact_id for DC_EVENT_REACTIONS_CHANGED - but i doubt it was ever used
for another pr
addBroadcast-Reactions:header also for resent channel messages, so that new subscriber do not only get the latest messages, but also their reactions. for that, theBroadcast-Reactions:header can go to the corresponding message, no need to send extra messages. we would need to change the sending part to send all reactions for a given message. on receiving part, we need to make sure,receive_broadcast_reactions()is called when the message actually exist.EDIT: subsequent PR for resending broadcast reactions at resend broadcast reactions together with message #8496
add api to allow only a subset of reactions, fiter incoming reactions before broadcasting
misc.
ui pr: deltachat/deltachat-ios#3225 and deltachat/deltachat-android#4560 , which both were tested successfully with this core PR already. desktop is meant to be done once this is merged