mirror of
https://github.com/chatmail/core.git
synced 2026-05-05 22:36:30 +03:00
Implement reactions
Co-Authored-By: bjoern <r10s@b44t.com> Co-Authored-By: Simon Laux <mobile.info@simonlaux.de>
This commit is contained in:
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
### API-Changes
|
### API-Changes
|
||||||
- jsonrpc: typescript client: export constants under `C` enum, similar to how its exported from `deltachat-node` #3681
|
- jsonrpc: typescript client: export constants under `C` enum, similar to how its exported from `deltachat-node` #3681
|
||||||
|
- added reactions support #3644
|
||||||
|
- jsonrpc: reactions: added reactions to `Message` type and the `sendReaction()` method #3686
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- simplify `UPSERT` queries #3676
|
- simplify `UPSERT` queries #3676
|
||||||
|
|||||||
@@ -11,16 +11,17 @@ extern "C" {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
typedef struct _dc_context dc_context_t;
|
typedef struct _dc_context dc_context_t;
|
||||||
typedef struct _dc_accounts dc_accounts_t;
|
typedef struct _dc_accounts dc_accounts_t;
|
||||||
typedef struct _dc_array dc_array_t;
|
typedef struct _dc_array dc_array_t;
|
||||||
typedef struct _dc_chatlist dc_chatlist_t;
|
typedef struct _dc_chatlist dc_chatlist_t;
|
||||||
typedef struct _dc_chat dc_chat_t;
|
typedef struct _dc_chat dc_chat_t;
|
||||||
typedef struct _dc_msg dc_msg_t;
|
typedef struct _dc_msg dc_msg_t;
|
||||||
typedef struct _dc_contact dc_contact_t;
|
typedef struct _dc_reactions dc_reactions_t;
|
||||||
typedef struct _dc_lot dc_lot_t;
|
typedef struct _dc_contact dc_contact_t;
|
||||||
typedef struct _dc_provider dc_provider_t;
|
typedef struct _dc_lot dc_lot_t;
|
||||||
typedef struct _dc_event dc_event_t;
|
typedef struct _dc_provider dc_provider_t;
|
||||||
|
typedef struct _dc_event dc_event_t;
|
||||||
typedef struct _dc_event_emitter dc_event_emitter_t;
|
typedef struct _dc_event_emitter dc_event_emitter_t;
|
||||||
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
|
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
|
||||||
|
|
||||||
@@ -991,6 +992,34 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
|
|||||||
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a reaction to message.
|
||||||
|
*
|
||||||
|
* Reaction is a string of emojis separated by spaces. Reaction to a
|
||||||
|
* single message can be sent multiple times. The last reaction
|
||||||
|
* received overrides all previously received reactions. It is
|
||||||
|
* possible to remove all reactions by sending an empty string.
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object.
|
||||||
|
* @param msg_id ID of the message you react to.
|
||||||
|
* @param reaction A string consisting of emojis separated by spaces.
|
||||||
|
* @return The ID of the message sent out or 0 for errors.
|
||||||
|
*/
|
||||||
|
uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reaction);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a structure with reactions to the message.
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object.
|
||||||
|
* @param msg_id The message ID to get reactions for.
|
||||||
|
* @return A structure with all reactions to the message.
|
||||||
|
*/
|
||||||
|
dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A webxdc instance sends a status update to its other members.
|
* A webxdc instance sends a status update to its other members.
|
||||||
*
|
*
|
||||||
@@ -4882,7 +4911,49 @@ uint32_t dc_lot_get_id (const dc_lot_t* lot);
|
|||||||
* @param lot The lot object.
|
* @param lot The lot object.
|
||||||
* @return The timestamp as defined by the creator of the object. 0 if there is not timestamp or on errors.
|
* @return The timestamp as defined by the creator of the object. 0 if there is not timestamp or on errors.
|
||||||
*/
|
*/
|
||||||
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class dc_reactions_t
|
||||||
|
*
|
||||||
|
* An object representing all reactions for a single message.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns array of contacts which reacted to the given message.
|
||||||
|
*
|
||||||
|
* @memberof dc_reactions_t
|
||||||
|
* @param reactions The object containing message reactions.
|
||||||
|
* @return array of contact IDs. Use dc_array_get_cnt() to get array length and
|
||||||
|
* dc_array_get_id() to get the IDs. Should be freed using `dc_array_unref()` after usage.
|
||||||
|
*/
|
||||||
|
dc_array_t* dc_reactions_get_contacts(dc_reactions_t* reactions);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string containing space-separated reactions of a single contact.
|
||||||
|
*
|
||||||
|
* @memberof dc_reactions_t
|
||||||
|
* @param reactions The object containing message reactions.
|
||||||
|
* @param contact_id ID of the contact.
|
||||||
|
* @return Space-separated list of emoji sequences, which could be empty.
|
||||||
|
* Returned string should not be modified and should be freed
|
||||||
|
* with dc_str_unref() after usage.
|
||||||
|
*/
|
||||||
|
char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32_t contact_id);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frees an object containing message reactions.
|
||||||
|
*
|
||||||
|
* Reactions objects are created by dc_get_msg_reactions().
|
||||||
|
*
|
||||||
|
* @memberof dc_reactions_t
|
||||||
|
* @param reactions The object to free.
|
||||||
|
* If NULL is given, nothing is done.
|
||||||
|
*/
|
||||||
|
void dc_reactions_unref (dc_reactions_t* reactions);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -5533,6 +5604,15 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
#define DC_EVENT_MSGS_CHANGED 2000
|
#define DC_EVENT_MSGS_CHANGED 2000
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message reactions changed.
|
||||||
|
*
|
||||||
|
* @param data1 (int) chat_id ID of the chat affected by the changes.
|
||||||
|
* @param data2 (int) msg_id ID of the message for which reactions were changed.
|
||||||
|
*/
|
||||||
|
#define DC_EVENT_REACTIONS_CHANGED 2001
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* There is a fresh message. Typically, the user will show an notification
|
* There is a fresh message. Typically, the user will show an notification
|
||||||
* when receiving this message.
|
* when receiving this message.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::chat::ChatItem;
|
use crate::chat::ChatItem;
|
||||||
use crate::constants::DC_MSG_ID_DAYMARKER;
|
use crate::constants::DC_MSG_ID_DAYMARKER;
|
||||||
|
use crate::contact::ContactId;
|
||||||
use crate::location::Location;
|
use crate::location::Location;
|
||||||
use crate::message::MsgId;
|
use crate::message::MsgId;
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ use crate::message::MsgId;
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum dc_array_t {
|
pub enum dc_array_t {
|
||||||
MsgIds(Vec<MsgId>),
|
MsgIds(Vec<MsgId>),
|
||||||
|
ContactIds(Vec<ContactId>),
|
||||||
Chat(Vec<ChatItem>),
|
Chat(Vec<ChatItem>),
|
||||||
Locations(Vec<Location>),
|
Locations(Vec<Location>),
|
||||||
Uint(Vec<u32>),
|
Uint(Vec<u32>),
|
||||||
@@ -16,6 +18,7 @@ impl dc_array_t {
|
|||||||
pub(crate) fn get_id(&self, index: usize) -> u32 {
|
pub(crate) fn get_id(&self, index: usize) -> u32 {
|
||||||
match self {
|
match self {
|
||||||
Self::MsgIds(array) => array[index].to_u32(),
|
Self::MsgIds(array) => array[index].to_u32(),
|
||||||
|
Self::ContactIds(array) => array[index].to_u32(),
|
||||||
Self::Chat(array) => match array[index] {
|
Self::Chat(array) => match array[index] {
|
||||||
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||||
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||||
@@ -28,6 +31,7 @@ impl dc_array_t {
|
|||||||
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
|
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
|
||||||
match self {
|
match self {
|
||||||
Self::MsgIds(_) => None,
|
Self::MsgIds(_) => None,
|
||||||
|
Self::ContactIds(_) => None,
|
||||||
Self::Chat(array) => array.get(index).and_then(|item| match item {
|
Self::Chat(array) => array.get(index).and_then(|item| match item {
|
||||||
ChatItem::Message { .. } => None,
|
ChatItem::Message { .. } => None,
|
||||||
ChatItem::DayMarker { timestamp } => Some(*timestamp),
|
ChatItem::DayMarker { timestamp } => Some(*timestamp),
|
||||||
@@ -40,6 +44,7 @@ impl dc_array_t {
|
|||||||
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
|
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
Self::MsgIds(_) => None,
|
Self::MsgIds(_) => None,
|
||||||
|
Self::ContactIds(_) => None,
|
||||||
Self::Chat(_) => None,
|
Self::Chat(_) => None,
|
||||||
Self::Locations(array) => array
|
Self::Locations(array) => array
|
||||||
.get(index)
|
.get(index)
|
||||||
@@ -60,6 +65,7 @@ impl dc_array_t {
|
|||||||
pub(crate) fn len(&self) -> usize {
|
pub(crate) fn len(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::MsgIds(array) => array.len(),
|
Self::MsgIds(array) => array.len(),
|
||||||
|
Self::ContactIds(array) => array.len(),
|
||||||
Self::Chat(array) => array.len(),
|
Self::Chat(array) => array.len(),
|
||||||
Self::Locations(array) => array.len(),
|
Self::Locations(array) => array.len(),
|
||||||
Self::Uint(array) => array.len(),
|
Self::Uint(array) => array.len(),
|
||||||
@@ -83,6 +89,12 @@ impl From<Vec<MsgId>> for dc_array_t {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Vec<ContactId>> for dc_array_t {
|
||||||
|
fn from(array: Vec<ContactId>) -> Self {
|
||||||
|
dc_array_t::ContactIds(array)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Vec<ChatItem>> for dc_array_t {
|
impl From<Vec<ChatItem>> for dc_array_t {
|
||||||
fn from(array: Vec<ChatItem>) -> Self {
|
fn from(array: Vec<ChatItem>) -> Self {
|
||||||
dc_array_t::Chat(array)
|
dc_array_t::Chat(array)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ use deltachat::context::Context;
|
|||||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||||
use deltachat::key::DcKey;
|
use deltachat::key::DcKey;
|
||||||
use deltachat::message::MsgId;
|
use deltachat::message::MsgId;
|
||||||
|
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
||||||
use deltachat::stock_str::StockMessage;
|
use deltachat::stock_str::StockMessage;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
use deltachat::webxdc::StatusUpdateSerial;
|
use deltachat::webxdc::StatusUpdateSerial;
|
||||||
@@ -66,6 +67,8 @@ use deltachat::chatlist::Chatlist;
|
|||||||
/// Struct representing the deltachat context.
|
/// Struct representing the deltachat context.
|
||||||
pub type dc_context_t = Context;
|
pub type dc_context_t = Context;
|
||||||
|
|
||||||
|
pub type dc_reactions_t = Reactions;
|
||||||
|
|
||||||
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
|
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
|
||||||
|
|
||||||
fn block_on<T>(fut: T) -> T::Output
|
fn block_on<T>(fut: T) -> T::Output
|
||||||
@@ -498,6 +501,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
|||||||
EventType::Error(_) => 400,
|
EventType::Error(_) => 400,
|
||||||
EventType::ErrorSelfNotInGroup(_) => 410,
|
EventType::ErrorSelfNotInGroup(_) => 410,
|
||||||
EventType::MsgsChanged { .. } => 2000,
|
EventType::MsgsChanged { .. } => 2000,
|
||||||
|
EventType::ReactionsChanged { .. } => 2001,
|
||||||
EventType::IncomingMsg { .. } => 2005,
|
EventType::IncomingMsg { .. } => 2005,
|
||||||
EventType::MsgsNoticed { .. } => 2008,
|
EventType::MsgsNoticed { .. } => 2008,
|
||||||
EventType::MsgDelivered { .. } => 2010,
|
EventType::MsgDelivered { .. } => 2010,
|
||||||
@@ -542,6 +546,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
|||||||
| EventType::SelfavatarChanged
|
| EventType::SelfavatarChanged
|
||||||
| EventType::ErrorSelfNotInGroup(_) => 0,
|
| EventType::ErrorSelfNotInGroup(_) => 0,
|
||||||
EventType::MsgsChanged { chat_id, .. }
|
EventType::MsgsChanged { chat_id, .. }
|
||||||
|
| EventType::ReactionsChanged { chat_id, .. }
|
||||||
| EventType::IncomingMsg { chat_id, .. }
|
| EventType::IncomingMsg { chat_id, .. }
|
||||||
| EventType::MsgsNoticed(chat_id)
|
| EventType::MsgsNoticed(chat_id)
|
||||||
| EventType::MsgDelivered { chat_id, .. }
|
| EventType::MsgDelivered { chat_id, .. }
|
||||||
@@ -598,6 +603,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
|||||||
| EventType::SelfavatarChanged => 0,
|
| EventType::SelfavatarChanged => 0,
|
||||||
EventType::ChatModified(_) => 0,
|
EventType::ChatModified(_) => 0,
|
||||||
EventType::MsgsChanged { msg_id, .. }
|
EventType::MsgsChanged { msg_id, .. }
|
||||||
|
| EventType::ReactionsChanged { msg_id, .. }
|
||||||
| EventType::IncomingMsg { msg_id, .. }
|
| EventType::IncomingMsg { msg_id, .. }
|
||||||
| EventType::MsgDelivered { msg_id, .. }
|
| EventType::MsgDelivered { msg_id, .. }
|
||||||
| EventType::MsgFailed { msg_id, .. }
|
| EventType::MsgFailed { msg_id, .. }
|
||||||
@@ -637,6 +643,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
|||||||
data2.into_raw()
|
data2.into_raw()
|
||||||
}
|
}
|
||||||
EventType::MsgsChanged { .. }
|
EventType::MsgsChanged { .. }
|
||||||
|
| EventType::ReactionsChanged { .. }
|
||||||
| EventType::IncomingMsg { .. }
|
| EventType::IncomingMsg { .. }
|
||||||
| EventType::MsgsNoticed(_)
|
| EventType::MsgsNoticed(_)
|
||||||
| EventType::MsgDelivered { .. }
|
| EventType::MsgDelivered { .. }
|
||||||
@@ -948,6 +955,48 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_send_reaction(
|
||||||
|
context: *mut dc_context_t,
|
||||||
|
msg_id: u32,
|
||||||
|
reaction: *const libc::c_char,
|
||||||
|
) -> u32 {
|
||||||
|
if context.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_send_reaction()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
|
||||||
|
block_on(async move {
|
||||||
|
send_reaction(ctx, MsgId::new(msg_id), &to_string_lossy(reaction))
|
||||||
|
.await
|
||||||
|
.map(|msg_id| msg_id.to_u32())
|
||||||
|
.unwrap_or_log_default(ctx, "Failed to send reaction")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_get_msg_reactions(
|
||||||
|
context: *mut dc_context_t,
|
||||||
|
msg_id: u32,
|
||||||
|
) -> *mut dc_reactions_t {
|
||||||
|
if context.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_get_msg_reactions()");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
|
||||||
|
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
|
||||||
|
.log_err(ctx, "failed dc_get_msg_reactions() call")
|
||||||
|
{
|
||||||
|
reactions
|
||||||
|
} else {
|
||||||
|
return ptr::null_mut();
|
||||||
|
};
|
||||||
|
|
||||||
|
Box::into_raw(Box::new(reactions))
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
@@ -3988,6 +4037,45 @@ pub unsafe extern "C" fn dc_lot_get_timestamp(lot: *mut dc_lot_t) -> i64 {
|
|||||||
lot.get_timestamp()
|
lot.get_timestamp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_reactions_get_contacts(
|
||||||
|
reactions: *mut dc_reactions_t,
|
||||||
|
) -> *mut dc_array::dc_array_t {
|
||||||
|
if reactions.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_reactions_get_contacts()");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let reactions = &*reactions;
|
||||||
|
let array: dc_array_t = reactions.contacts().into();
|
||||||
|
|
||||||
|
Box::into_raw(Box::new(array))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_reactions_get_by_contact_id(
|
||||||
|
reactions: *mut dc_reactions_t,
|
||||||
|
contact_id: u32,
|
||||||
|
) -> *mut libc::c_char {
|
||||||
|
if reactions.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_reactions_get_by_contact_id()");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let reactions = &*reactions;
|
||||||
|
reactions.get(ContactId::new(contact_id)).as_str().strdup()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_reactions_unref(reactions: *mut dc_reactions_t) {
|
||||||
|
if reactions.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_reactions_unref()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(Box::from_raw(reactions));
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
||||||
libc::free(s as *mut _)
|
libc::free(s as *mut _)
|
||||||
|
|||||||
@@ -102,6 +102,14 @@ pub enum JSONRPCEventType {
|
|||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Reactions for the message changed.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
ReactionsChanged {
|
||||||
|
chat_id: u32,
|
||||||
|
msg_id: u32,
|
||||||
|
contact_id: u32,
|
||||||
|
},
|
||||||
|
|
||||||
/// There is a fresh message. Typically, the user will show an notification
|
/// There is a fresh message. Typically, the user will show an notification
|
||||||
/// when receiving this message.
|
/// when receiving this message.
|
||||||
///
|
///
|
||||||
@@ -284,6 +292,15 @@ impl From<EventType> for JSONRPCEventType {
|
|||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
|
EventType::ReactionsChanged {
|
||||||
|
chat_id,
|
||||||
|
msg_id,
|
||||||
|
contact_id,
|
||||||
|
} => ReactionsChanged {
|
||||||
|
chat_id: chat_id.to_u32(),
|
||||||
|
msg_id: msg_id.to_u32(),
|
||||||
|
contact_id: contact_id.to_u32(),
|
||||||
|
},
|
||||||
EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use deltachat::{
|
|||||||
provider::get_provider_info,
|
provider::get_provider_info,
|
||||||
qr,
|
qr,
|
||||||
qr_code_generator::get_securejoin_qr_svg,
|
qr_code_generator::get_securejoin_qr_svg,
|
||||||
|
reaction::send_reaction,
|
||||||
securejoin,
|
securejoin,
|
||||||
stock_str::StockMessage,
|
stock_str::StockMessage,
|
||||||
webxdc::StatusUpdateSerial,
|
webxdc::StatusUpdateSerial,
|
||||||
@@ -1466,6 +1467,23 @@ impl CommandApi {
|
|||||||
Ok(message_id.to_u32())
|
Ok(message_id.to_u32())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a reaction to message.
|
||||||
|
///
|
||||||
|
/// Reaction is a string of emojis separated by spaces. Reaction to a
|
||||||
|
/// single message can be sent multiple times. The last reaction
|
||||||
|
/// received overrides all previously received reactions. It is
|
||||||
|
/// possible to remove all reactions by sending an empty string.
|
||||||
|
async fn send_reaction(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
message_id: u32,
|
||||||
|
reaction: Vec<String>,
|
||||||
|
) -> Result<u32> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let message_id = send_reaction(&ctx, MsgId::new(message_id), &reaction.join(" ")).await?;
|
||||||
|
Ok(message_id.to_u32())
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
// functions for the composer
|
// functions for the composer
|
||||||
// the composer is the message input field
|
// the composer is the message input field
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use deltachat::download;
|
|||||||
use deltachat::message::Message;
|
use deltachat::message::Message;
|
||||||
use deltachat::message::MsgId;
|
use deltachat::message::MsgId;
|
||||||
use deltachat::message::Viewtype;
|
use deltachat::message::Viewtype;
|
||||||
|
use deltachat::reaction::get_msg_reactions;
|
||||||
use num_traits::cast::ToPrimitive;
|
use num_traits::cast::ToPrimitive;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -15,6 +16,7 @@ use typescript_type_def::TypeDef;
|
|||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
use super::contact::ContactObject;
|
use super::contact::ContactObject;
|
||||||
|
use super::reactions::JSONRPCReactions;
|
||||||
use super::webxdc::WebxdcMessageInfo;
|
use super::webxdc::WebxdcMessageInfo;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef)]
|
||||||
@@ -64,6 +66,8 @@ pub struct MessageObject {
|
|||||||
webxdc_info: Option<WebxdcMessageInfo>,
|
webxdc_info: Option<WebxdcMessageInfo>,
|
||||||
|
|
||||||
download_state: DownloadState,
|
download_state: DownloadState,
|
||||||
|
|
||||||
|
reactions: Option<JSONRPCReactions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef)]
|
||||||
@@ -139,6 +143,13 @@ impl MessageObject {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let reactions = get_msg_reactions(context, msg_id).await?;
|
||||||
|
let reactions = if reactions.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(reactions.into())
|
||||||
|
};
|
||||||
|
|
||||||
Ok(MessageObject {
|
Ok(MessageObject {
|
||||||
id: msg_id.to_u32(),
|
id: msg_id.to_u32(),
|
||||||
chat_id: message.get_chat_id().to_u32(),
|
chat_id: message.get_chat_id().to_u32(),
|
||||||
@@ -193,6 +204,8 @@ impl MessageObject {
|
|||||||
webxdc_info,
|
webxdc_info,
|
||||||
|
|
||||||
download_state,
|
download_state,
|
||||||
|
|
||||||
|
reactions,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub mod contact;
|
|||||||
pub mod location;
|
pub mod location;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod provider_info;
|
pub mod provider_info;
|
||||||
|
pub mod reactions;
|
||||||
pub mod webxdc;
|
pub mod webxdc;
|
||||||
|
|
||||||
pub fn color_int_to_hex_string(color: u32) -> String {
|
pub fn color_int_to_hex_string(color: u32) -> String {
|
||||||
|
|||||||
47
deltachat-jsonrpc/src/api/types/reactions.rs
Normal file
47
deltachat-jsonrpc/src/api/types/reactions.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use deltachat::reaction::Reactions;
|
||||||
|
use serde::Serialize;
|
||||||
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
/// Structure representing all reactions to a particular message.
|
||||||
|
#[derive(Serialize, TypeDef)]
|
||||||
|
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
||||||
|
pub struct JSONRPCReactions {
|
||||||
|
/// Map from a contact to it's reaction to message.
|
||||||
|
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
||||||
|
/// Unique reactions and their count
|
||||||
|
reactions: BTreeMap<String, u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Reactions> for JSONRPCReactions {
|
||||||
|
fn from(reactions: Reactions) -> Self {
|
||||||
|
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
||||||
|
let mut unique_reactions: BTreeMap<String, u32> = BTreeMap::new();
|
||||||
|
|
||||||
|
for contact_id in reactions.contacts() {
|
||||||
|
let reaction = reactions.get(contact_id);
|
||||||
|
if reaction.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let emojis: Vec<String> = reaction
|
||||||
|
.emojis()
|
||||||
|
.into_iter()
|
||||||
|
.map(|emoji| emoji.to_owned())
|
||||||
|
.collect();
|
||||||
|
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
|
||||||
|
for emoji in emojis {
|
||||||
|
if let Some(x) = unique_reactions.get_mut(&emoji) {
|
||||||
|
*x += 1;
|
||||||
|
} else {
|
||||||
|
unique_reactions.insert(emoji, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONRPCReactions {
|
||||||
|
reactions_by_contact,
|
||||||
|
reactions: unique_reactions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -878,6 +878,18 @@ export class RawClient {
|
|||||||
return (this._transport.request('send_sticker', [accountId, chatId, stickerPath] as RPC.Params)) as Promise<T.U32>;
|
return (this._transport.request('send_sticker', [accountId, chatId, stickerPath] as RPC.Params)) as Promise<T.U32>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a reaction to message.
|
||||||
|
*
|
||||||
|
* Reaction is a string of emojis separated by spaces. Reaction to a
|
||||||
|
* single message can be sent multiple times. The last reaction
|
||||||
|
* received overrides all previously received reactions. It is
|
||||||
|
* possible to remove all reactions by sending an empty string.
|
||||||
|
*/
|
||||||
|
public sendReaction(accountId: T.U32, messageId: T.U32, reaction: (string)[]): Promise<T.U32> {
|
||||||
|
return (this._transport.request('send_reaction', [accountId, messageId, reaction] as RPC.Params)) as Promise<T.U32>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
|
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||||
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ export type Event=(({
|
|||||||
* `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
* `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
||||||
*/
|
*/
|
||||||
"type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({
|
"type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({
|
||||||
|
/**
|
||||||
|
* Reactions for the message changed.
|
||||||
|
*/
|
||||||
|
"type":"ReactionsChanged";}&{"chatId":U32;"msgId":U32;"contactId":U32;})|({
|
||||||
/**
|
/**
|
||||||
* There is a fresh message. Typically, the user will show an notification
|
* There is a fresh message. Typically, the user will show an notification
|
||||||
* when receiving this message.
|
* when receiving this message.
|
||||||
|
|||||||
@@ -147,7 +147,24 @@ export type WebxdcMessageInfo={
|
|||||||
*/
|
*/
|
||||||
"internetAccess":boolean;};
|
"internetAccess":boolean;};
|
||||||
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
|
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
|
||||||
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;};
|
|
||||||
|
/**
|
||||||
|
* Structure representing all reactions to a particular message.
|
||||||
|
*/
|
||||||
|
export type Reactions=
|
||||||
|
/**
|
||||||
|
* Structure representing all reactions to a particular message.
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Map from a contact to it's reaction to message.
|
||||||
|
*/
|
||||||
|
"reactionsByContact":Record<U32,(string)[]>;
|
||||||
|
/**
|
||||||
|
* Unique reactions and their count
|
||||||
|
*/
|
||||||
|
"reactions":Record<string,U32>;};
|
||||||
|
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;"reactions":(Reactions|null);};
|
||||||
export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null);
|
export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null);
|
||||||
/**
|
/**
|
||||||
* also known as summary_text1
|
* also known as summary_text1
|
||||||
@@ -160,4 +177,4 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag
|
|||||||
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
|
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
|
||||||
export type F64=number;
|
export type F64=number;
|
||||||
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
|
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
|
||||||
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,Record<string,(string)[]>,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
|
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,Record<string,(string)[]>,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use deltachat::log::LogExt;
|
|||||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||||
use deltachat::peerstate::*;
|
use deltachat::peerstate::*;
|
||||||
use deltachat::qr::*;
|
use deltachat::qr::*;
|
||||||
|
use deltachat::reaction::send_reaction;
|
||||||
use deltachat::receive_imf::*;
|
use deltachat::receive_imf::*;
|
||||||
use deltachat::sql;
|
use deltachat::sql;
|
||||||
use deltachat::tools::*;
|
use deltachat::tools::*;
|
||||||
@@ -407,6 +408,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
resend <msg-id>\n\
|
resend <msg-id>\n\
|
||||||
markseen <msg-id>\n\
|
markseen <msg-id>\n\
|
||||||
delmsg <msg-id>\n\
|
delmsg <msg-id>\n\
|
||||||
|
react <msg-id> [<reaction>]\n\
|
||||||
===========================Contact commands==\n\
|
===========================Contact commands==\n\
|
||||||
listcontacts [<query>]\n\
|
listcontacts [<query>]\n\
|
||||||
listverified [<query>]\n\
|
listverified [<query>]\n\
|
||||||
@@ -1121,6 +1123,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
ids[0] = MsgId::new(arg1.parse()?);
|
ids[0] = MsgId::new(arg1.parse()?);
|
||||||
message::delete_msgs(&context, &ids).await?;
|
message::delete_msgs(&context, &ids).await?;
|
||||||
}
|
}
|
||||||
|
"react" => {
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||||
|
let msg_id = MsgId::new(arg1.parse()?);
|
||||||
|
let reaction = arg2;
|
||||||
|
send_reaction(&context, msg_id, reaction).await?;
|
||||||
|
}
|
||||||
"listcontacts" | "contacts" | "listverified" => {
|
"listcontacts" | "contacts" | "listverified" => {
|
||||||
let contacts = Contact::get_all(
|
let contacts = Contact::get_all(
|
||||||
&context,
|
&context,
|
||||||
|
|||||||
@@ -72,6 +72,19 @@ fn receive_event(event: EventType) {
|
|||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
EventType::ReactionsChanged {
|
||||||
|
chat_id,
|
||||||
|
msg_id,
|
||||||
|
contact_id,
|
||||||
|
} => {
|
||||||
|
info!(
|
||||||
|
"{}",
|
||||||
|
yellow.paint(format!(
|
||||||
|
"Received REACTIONS_CHANGED(chat_id={}, msg_id={}, contact_id={})",
|
||||||
|
chat_id, msg_id, contact_id
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
EventType::ContactsChanged(_) => {
|
EventType::ContactsChanged(_) => {
|
||||||
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
|
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
|
||||||
}
|
}
|
||||||
@@ -208,7 +221,7 @@ const CHAT_COMMANDS: [&str; 36] = [
|
|||||||
"accept",
|
"accept",
|
||||||
"blockchat",
|
"blockchat",
|
||||||
];
|
];
|
||||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
const MESSAGE_COMMANDS: [&str; 9] = [
|
||||||
"listmsgs",
|
"listmsgs",
|
||||||
"msginfo",
|
"msginfo",
|
||||||
"listfresh",
|
"listfresh",
|
||||||
@@ -217,6 +230,7 @@ const MESSAGE_COMMANDS: [&str; 8] = [
|
|||||||
"markseen",
|
"markseen",
|
||||||
"delmsg",
|
"delmsg",
|
||||||
"download",
|
"download",
|
||||||
|
"react",
|
||||||
];
|
];
|
||||||
const CONTACT_COMMANDS: [&str; 9] = [
|
const CONTACT_COMMANDS: [&str; 9] = [
|
||||||
"listcontacts",
|
"listcontacts",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ module.exports = {
|
|||||||
DC_EVENT_MSG_FAILED: 2012,
|
DC_EVENT_MSG_FAILED: 2012,
|
||||||
DC_EVENT_MSG_READ: 2015,
|
DC_EVENT_MSG_READ: 2015,
|
||||||
DC_EVENT_NEW_BLOB_FILE: 150,
|
DC_EVENT_NEW_BLOB_FILE: 150,
|
||||||
|
DC_EVENT_REACTIONS_CHANGED: 2001,
|
||||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS: 2060,
|
DC_EVENT_SECUREJOIN_INVITER_PROGRESS: 2060,
|
||||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS: 2061,
|
DC_EVENT_SECUREJOIN_JOINER_PROGRESS: 2061,
|
||||||
DC_EVENT_SELFAVATAR_CHANGED: 2110,
|
DC_EVENT_SELFAVATAR_CHANGED: 2110,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ module.exports = {
|
|||||||
400: 'DC_EVENT_ERROR',
|
400: 'DC_EVENT_ERROR',
|
||||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||||
|
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||||
2005: 'DC_EVENT_INCOMING_MSG',
|
2005: 'DC_EVENT_INCOMING_MSG',
|
||||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export enum C {
|
|||||||
DC_EVENT_MSG_FAILED = 2012,
|
DC_EVENT_MSG_FAILED = 2012,
|
||||||
DC_EVENT_MSG_READ = 2015,
|
DC_EVENT_MSG_READ = 2015,
|
||||||
DC_EVENT_NEW_BLOB_FILE = 150,
|
DC_EVENT_NEW_BLOB_FILE = 150,
|
||||||
|
DC_EVENT_REACTIONS_CHANGED = 2001,
|
||||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
|
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
|
||||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
|
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
|
||||||
DC_EVENT_SELFAVATAR_CHANGED = 2110,
|
DC_EVENT_SELFAVATAR_CHANGED = 2110,
|
||||||
@@ -282,6 +283,7 @@ export const EventId2EventName: { [key: number]: string } = {
|
|||||||
400: 'DC_EVENT_ERROR',
|
400: 'DC_EVENT_ERROR',
|
||||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||||
|
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||||
2005: 'DC_EVENT_INCOMING_MSG',
|
2005: 'DC_EVENT_INCOMING_MSG',
|
||||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||||
|
|||||||
10
spec.md
10
spec.md
@@ -450,6 +450,16 @@ This allows the receiver to show the time without knowing the file format.
|
|||||||
Chat-Duration: 10000
|
Chat-Duration: 10000
|
||||||
|
|
||||||
|
|
||||||
|
# Reactions
|
||||||
|
|
||||||
|
Messengers MAY implement [RFC 9078](https://tools.ietf.org/html/rfc9078) reactions.
|
||||||
|
Received reaction should be interpreted as overwriting all previous reactions
|
||||||
|
received from the same contact.
|
||||||
|
This semantics is compatible to [XEP-0444](https://xmpp.org/extensions/xep-0444.html).
|
||||||
|
As an extension to RFC 9078, it is allowed to send empty reaction message,
|
||||||
|
in which case all previously sent reactions are retracted.
|
||||||
|
|
||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
|
|
||||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ const SEEN_RECENTLY_SECONDS: i64 = 600;
|
|||||||
///
|
///
|
||||||
/// Some contact IDs are reserved to identify special contacts. This
|
/// Some contact IDs are reserved to identify special contacts. This
|
||||||
/// type can represent both the special as well as normal contacts.
|
/// type can represent both the special as well as normal contacts.
|
||||||
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(
|
||||||
|
Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
pub struct ContactId(u32);
|
pub struct ContactId(u32);
|
||||||
|
|
||||||
impl ContactId {
|
impl ContactId {
|
||||||
|
|||||||
@@ -173,6 +173,13 @@ pub enum EventType {
|
|||||||
msg_id: MsgId,
|
msg_id: MsgId,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Reactions for the message changed.
|
||||||
|
ReactionsChanged {
|
||||||
|
chat_id: ChatId,
|
||||||
|
msg_id: MsgId,
|
||||||
|
contact_id: ContactId,
|
||||||
|
},
|
||||||
|
|
||||||
/// There is a fresh message. Typically, the user will show an notification
|
/// There is a fresh message. Typically, the user will show an notification
|
||||||
/// when receiving this message.
|
/// when receiving this message.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ pub mod receive_imf;
|
|||||||
pub mod tools;
|
pub mod tools;
|
||||||
|
|
||||||
pub mod accounts;
|
pub mod accounts;
|
||||||
|
pub mod reaction;
|
||||||
|
|
||||||
/// if set imap/incoming and smtp/outgoing MIME messages will be printed
|
/// if set imap/incoming and smtp/outgoing MIME messages will be printed
|
||||||
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
|
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use crate::imap::markseen_on_imap_table;
|
|||||||
use crate::mimeparser::{parse_message_id, DeliveryReport, SystemMessage};
|
use crate::mimeparser::{parse_message_id, DeliveryReport, SystemMessage};
|
||||||
use crate::param::{Param, Params};
|
use crate::param::{Param, Params};
|
||||||
use crate::pgp::split_armored_data;
|
use crate::pgp::split_armored_data;
|
||||||
|
use crate::reaction::get_msg_reactions;
|
||||||
use crate::scheduler::InterruptInfo;
|
use crate::scheduler::InterruptInfo;
|
||||||
use crate::sql;
|
use crate::sql;
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
@@ -751,6 +752,11 @@ impl Message {
|
|||||||
self.param.set_int(Param::Duration, duration);
|
self.param.set_int(Param::Duration, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Marks the message as reaction.
|
||||||
|
pub(crate) fn set_reaction(&mut self) {
|
||||||
|
self.param.set_int(Param::Reaction, 1);
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn latefiling_mediasize(
|
pub async fn latefiling_mediasize(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
@@ -1082,6 +1088,11 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
|
|||||||
|
|
||||||
ret += "\n";
|
ret += "\n";
|
||||||
|
|
||||||
|
let reactions = get_msg_reactions(context, msg_id).await?;
|
||||||
|
if !reactions.is_empty() {
|
||||||
|
ret += &format!("Reactions: {}\n", reactions);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(error) = msg.error.as_ref() {
|
if let Some(error) = msg.error.as_ref() {
|
||||||
ret += &format!("Error: {}", error);
|
ret += &format!("Error: {}", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,10 @@ impl<'a> MimeFactory<'a> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await? {
|
if !msg.is_system_message()
|
||||||
|
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
|
||||||
|
&& context.get_config_bool(Config::MdnsEnabled).await?
|
||||||
|
{
|
||||||
req_mdn = true;
|
req_mdn = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1122,6 +1125,11 @@ impl<'a> MimeFactory<'a> {
|
|||||||
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
|
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
|
||||||
))
|
))
|
||||||
.body(message_text);
|
.body(message_text);
|
||||||
|
|
||||||
|
if self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0 {
|
||||||
|
main_part = main_part.header(("Content-Disposition", "reaction"));
|
||||||
|
}
|
||||||
|
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
|
|
||||||
// add HTML-part, this is needed only if a HTML-message from a non-delta-client is forwarded;
|
// add HTML-part, this is needed only if a HTML-message from a non-delta-client is forwarded;
|
||||||
|
|||||||
@@ -551,7 +551,10 @@ impl MimeMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if prepend_subject && !subject.is_empty() {
|
if prepend_subject && !subject.is_empty() {
|
||||||
let part_with_text = self.parts.iter_mut().find(|part| !part.msg.is_empty());
|
let part_with_text = self
|
||||||
|
.parts
|
||||||
|
.iter_mut()
|
||||||
|
.find(|part| !part.msg.is_empty() && !part.is_reaction);
|
||||||
if let Some(mut part) = part_with_text {
|
if let Some(mut part) = part_with_text {
|
||||||
part.msg = format!("{} – {}", subject, part.msg);
|
part.msg = format!("{} – {}", subject, part.msg);
|
||||||
}
|
}
|
||||||
@@ -913,6 +916,7 @@ impl MimeMessage {
|
|||||||
Ok(any_part_added)
|
Ok(any_part_added)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if any part was added, false otherwise.
|
||||||
async fn add_single_part_if_known(
|
async fn add_single_part_if_known(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
@@ -946,6 +950,30 @@ impl MimeMessage {
|
|||||||
warn!(context, "Missing attachment");
|
warn!(context, "Missing attachment");
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
mime::TEXT
|
||||||
|
if mail.get_content_disposition().disposition
|
||||||
|
== DispositionType::Extension("reaction".to_string()) =>
|
||||||
|
{
|
||||||
|
// Reaction.
|
||||||
|
let decoded_data = match mail.get_body() {
|
||||||
|
Ok(decoded_data) => decoded_data,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(context, "Invalid body parsed {:?}", err);
|
||||||
|
// Note that it's not always an error - might be no data
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let part = Part {
|
||||||
|
typ: Viewtype::Text,
|
||||||
|
mimetype: Some(mime_type),
|
||||||
|
msg: decoded_data,
|
||||||
|
is_reaction: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
self.do_add_single_part(part);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
mime::TEXT | mime::HTML => {
|
mime::TEXT | mime::HTML => {
|
||||||
let decoded_data = match mail.get_body() {
|
let decoded_data = match mail.get_body() {
|
||||||
Ok(decoded_data) => decoded_data,
|
Ok(decoded_data) => decoded_data,
|
||||||
@@ -1644,6 +1672,9 @@ pub struct Part {
|
|||||||
/// note that multipart/related may contain further multipart nestings
|
/// note that multipart/related may contain further multipart nestings
|
||||||
/// and all of them needs to be marked with `is_related`.
|
/// and all of them needs to be marked with `is_related`.
|
||||||
pub(crate) is_related: bool,
|
pub(crate) is_related: bool,
|
||||||
|
|
||||||
|
/// Part is an RFC 9078 reaction.
|
||||||
|
pub(crate) is_reaction: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// return mimetype and viewtype for a parsed mail
|
/// return mimetype and viewtype for a parsed mail
|
||||||
@@ -3329,4 +3360,39 @@ Message.
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tests parsing of MIME message containing RFC 9078 reaction.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_parse_reaction() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
|
||||||
|
let mime_message = MimeMessage::from_bytes(
|
||||||
|
&alice,
|
||||||
|
"To: alice@example.org\n\
|
||||||
|
From: bob@example.net\n\
|
||||||
|
Date: Today, 29 February 2021 00:00:10 -800\n\
|
||||||
|
Message-ID: 56789@example.net\n\
|
||||||
|
In-Reply-To: 12345@example.org\n\
|
||||||
|
Subject: Meeting\n\
|
||||||
|
Mime-Version: 1.0 (1.0)\n\
|
||||||
|
Content-Type: text/plain; charset=utf-8\n\
|
||||||
|
Content-Disposition: reaction\n\
|
||||||
|
\n\
|
||||||
|
\u{1F44D}"
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(mime_message.parts.len(), 1);
|
||||||
|
assert_eq!(mime_message.parts[0].is_reaction, true);
|
||||||
|
assert_eq!(
|
||||||
|
mime_message
|
||||||
|
.get_header(HeaderDef::InReplyTo)
|
||||||
|
.and_then(|msgid| parse_message_id(msgid).ok())
|
||||||
|
.unwrap(),
|
||||||
|
"12345@example.org"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ pub enum Param {
|
|||||||
/// For Messages
|
/// For Messages
|
||||||
WantsMdn = b'r',
|
WantsMdn = b'r',
|
||||||
|
|
||||||
|
/// For Messages: the message is a reaction.
|
||||||
|
Reaction = b'x',
|
||||||
|
|
||||||
/// For Messages: a message with Auto-Submitted header ("bot").
|
/// For Messages: a message with Auto-Submitted header ("bot").
|
||||||
Bot = b'b',
|
Bot = b'b',
|
||||||
|
|
||||||
|
|||||||
481
src/reaction.rs
Normal file
481
src/reaction.rs
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
//! # Reactions.
|
||||||
|
//!
|
||||||
|
//! Reactions are short messages consisting of emojis sent in reply to
|
||||||
|
//! messages. Unlike normal messages which are added to the end of the chat,
|
||||||
|
//! reactions are supposed to be displayed near the original messages.
|
||||||
|
//!
|
||||||
|
//! RFC 9078 specifies how reactions are transmitted in MIME messages.
|
||||||
|
//!
|
||||||
|
//! Reaction update semantics is not well-defined in RFC 9078, so
|
||||||
|
//! Delta Chat uses the same semantics as in
|
||||||
|
//! [XEP-0444](https://xmpp.org/extensions/xep-0444.html) section
|
||||||
|
//! "3.2 Updating reactions to a message". Received reactions override
|
||||||
|
//! all previously received reactions from the same user and it is
|
||||||
|
//! possible to remove all reactions by sending an empty string as a reaction,
|
||||||
|
//! even though RFC 9078 requires at least one emoji to be sent.
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::chat::{send_msg, ChatId};
|
||||||
|
use crate::contact::ContactId;
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::events::EventType;
|
||||||
|
use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype};
|
||||||
|
|
||||||
|
/// A single reaction consisting of multiple emoji sequences.
|
||||||
|
///
|
||||||
|
/// It is guaranteed to have all emojis sorted and deduplicated inside.
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Reaction {
|
||||||
|
/// Canonical represntation of reaction as a string of space-separated emojis.
|
||||||
|
reaction: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We implement From<&str> instead of std::str::FromStr, because
|
||||||
|
// FromStr requires error type and reaction parsing never returns an
|
||||||
|
// error.
|
||||||
|
impl From<&str> for Reaction {
|
||||||
|
/// Parses a string containing a reaction.
|
||||||
|
///
|
||||||
|
/// Reaction string is separated by spaces or tabs (`WSP` in ABNF),
|
||||||
|
/// but this function accepts any ASCII whitespace, so even a CRLF at
|
||||||
|
/// the end of string is acceptable.
|
||||||
|
///
|
||||||
|
/// Any short enough string is accepted as a reaction to avoid the
|
||||||
|
/// complexity of validating emoji sequences as required by RFC
|
||||||
|
/// 9078. On the sender side UI is responsible to provide only
|
||||||
|
/// valid emoji sequences via reaction picker. On the receiver
|
||||||
|
/// side, abuse of the possibility to use arbitrary strings as
|
||||||
|
/// reactions is not different from other kinds of spam attacks
|
||||||
|
/// such as sending large numbers of large messages, and should be
|
||||||
|
/// dealt with the same way, e.g. by blocking the user.
|
||||||
|
fn from(reaction: &str) -> Self {
|
||||||
|
let mut emojis: Vec<&str> = reaction
|
||||||
|
.split_ascii_whitespace()
|
||||||
|
.filter(|&emoji| emoji.len() < 30)
|
||||||
|
.collect();
|
||||||
|
emojis.sort();
|
||||||
|
emojis.dedup();
|
||||||
|
let reaction = emojis.join(" ");
|
||||||
|
Self { reaction }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reaction {
|
||||||
|
/// Returns true if reaction contains no emojis.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.reaction.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a vector of emojis composing a reaction.
|
||||||
|
pub fn emojis(&self) -> Vec<&str> {
|
||||||
|
self.reaction.split(' ').collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns space-separated string of emojis
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.reaction
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends emojis from another reaction to this reaction.
|
||||||
|
pub fn add(&self, other: Self) -> Self {
|
||||||
|
let mut emojis: Vec<&str> = self.emojis();
|
||||||
|
emojis.append(&mut other.emojis());
|
||||||
|
emojis.sort();
|
||||||
|
emojis.dedup();
|
||||||
|
let reaction = emojis.join(" ");
|
||||||
|
Self { reaction }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structure representing all reactions to a particular message.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Reactions {
|
||||||
|
/// Map from a contact to its reaction to message.
|
||||||
|
reactions: BTreeMap<ContactId, Reaction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reactions {
|
||||||
|
/// Returns vector of contacts that reacted to the message.
|
||||||
|
pub fn contacts(&self) -> Vec<ContactId> {
|
||||||
|
self.reactions.keys().copied().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns reaction of a given contact to message.
|
||||||
|
///
|
||||||
|
/// If contact did not react to message or removed the reaction,
|
||||||
|
/// this method returns an empty reaction.
|
||||||
|
pub fn get(&self, contact_id: ContactId) -> Reaction {
|
||||||
|
self.reactions.get(&contact_id).cloned().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the message has no reactions.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.reactions.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Reactions {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut emoji_frequencies: BTreeMap<String, usize> = BTreeMap::new();
|
||||||
|
for reaction in self.reactions.values() {
|
||||||
|
for emoji in reaction.emojis() {
|
||||||
|
emoji_frequencies
|
||||||
|
.entry(emoji.to_string())
|
||||||
|
.and_modify(|x| *x += 1)
|
||||||
|
.or_insert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut first = true;
|
||||||
|
for (emoji, frequency) in emoji_frequencies {
|
||||||
|
if !first {
|
||||||
|
write!(f, " ")?;
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
write!(f, "{}{}", emoji, frequency)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_msg_id_reaction(
|
||||||
|
context: &Context,
|
||||||
|
msg_id: MsgId,
|
||||||
|
chat_id: ChatId,
|
||||||
|
contact_id: ContactId,
|
||||||
|
reaction: Reaction,
|
||||||
|
) -> Result<()> {
|
||||||
|
if reaction.is_empty() {
|
||||||
|
// Simply remove the record instead of setting it to empty string.
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"DELETE FROM reactions
|
||||||
|
WHERE msg_id = ?1
|
||||||
|
AND contact_id = ?2",
|
||||||
|
paramsv![msg_id, contact_id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO reactions (msg_id, contact_id, reaction)
|
||||||
|
VALUES (?1, ?2, ?3)
|
||||||
|
ON CONFLICT(msg_id, contact_id)
|
||||||
|
DO UPDATE SET reaction=excluded.reaction",
|
||||||
|
paramsv![msg_id, contact_id, reaction.as_str()],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.emit_event(EventType::ReactionsChanged {
|
||||||
|
chat_id,
|
||||||
|
msg_id,
|
||||||
|
contact_id,
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a reaction to message `msg_id`, overriding previously sent reactions.
|
||||||
|
///
|
||||||
|
/// `reaction` is a string consisting of space-separated emoji. Use
|
||||||
|
/// empty string to retract a reaction.
|
||||||
|
pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
|
||||||
|
let msg = Message::load_from_db(context, msg_id).await?;
|
||||||
|
let chat_id = msg.chat_id;
|
||||||
|
|
||||||
|
let reaction: Reaction = reaction.into();
|
||||||
|
let mut reaction_msg = Message::new(Viewtype::Text);
|
||||||
|
reaction_msg.text = Some(reaction.as_str().to_string());
|
||||||
|
reaction_msg.set_reaction();
|
||||||
|
reaction_msg.in_reply_to = Some(msg.rfc724_mid);
|
||||||
|
reaction_msg.hidden = true;
|
||||||
|
|
||||||
|
// Send messsage first.
|
||||||
|
let reaction_msg_id = send_msg(context, chat_id, &mut reaction_msg).await?;
|
||||||
|
|
||||||
|
// Only set reaction if we successfully sent the message.
|
||||||
|
set_msg_id_reaction(context, msg_id, msg.chat_id, ContactId::SELF, reaction).await?;
|
||||||
|
Ok(reaction_msg_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds given reaction to message `msg_id` and sends an update.
|
||||||
|
///
|
||||||
|
/// This can be used to implement advanced clients that allow reacting
|
||||||
|
/// with multiple emojis. For a simple messenger UI, you probably want
|
||||||
|
/// to use [`send_reaction()`] instead so reacting with a new emoji
|
||||||
|
/// removes previous emoji at the same time.
|
||||||
|
pub async fn add_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
|
||||||
|
let self_reaction = get_self_reaction(context, msg_id).await?;
|
||||||
|
let reaction = self_reaction.add(Reaction::from(reaction));
|
||||||
|
send_reaction(context, msg_id, reaction.as_str()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates reaction of `contact_id` on the message with `in_reply_to`
|
||||||
|
/// Message-ID. If no such message is found in the database, reaction
|
||||||
|
/// is ignored.
|
||||||
|
///
|
||||||
|
/// `reaction` is a space-separated string of emojis. It can be empty
|
||||||
|
/// if contact wants to remove all reactions.
|
||||||
|
pub(crate) async fn set_msg_reaction(
|
||||||
|
context: &Context,
|
||||||
|
in_reply_to: &str,
|
||||||
|
chat_id: ChatId,
|
||||||
|
contact_id: ContactId,
|
||||||
|
reaction: Reaction,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||||
|
set_msg_id_reaction(context, msg_id, chat_id, contact_id, reaction).await
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
context,
|
||||||
|
"Can't assign reaction to unknown message with Message-ID {}", in_reply_to
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get our own reaction for a given message.
|
||||||
|
async fn get_self_reaction(context: &Context, msg_id: MsgId) -> Result<Reaction> {
|
||||||
|
let reaction_str: Option<String> = context
|
||||||
|
.sql
|
||||||
|
.query_get_value(
|
||||||
|
"SELECT reaction
|
||||||
|
FROM reactions
|
||||||
|
WHERE msg_id=? AND contact_id=?",
|
||||||
|
paramsv![msg_id, ContactId::SELF],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(reaction_str
|
||||||
|
.as_deref()
|
||||||
|
.map(Reaction::from)
|
||||||
|
.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a structure containing all reactions to the message.
|
||||||
|
pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<Reactions> {
|
||||||
|
let reactions = context
|
||||||
|
.sql
|
||||||
|
.query_map(
|
||||||
|
"SELECT contact_id, reaction FROM reactions WHERE msg_id=?",
|
||||||
|
paramsv![msg_id],
|
||||||
|
|row| {
|
||||||
|
let contact_id: ContactId = row.get(0)?;
|
||||||
|
let reaction: String = row.get(1)?;
|
||||||
|
Ok((contact_id, reaction))
|
||||||
|
},
|
||||||
|
|rows| {
|
||||||
|
let mut reactions = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
let (contact_id, reaction) = row?;
|
||||||
|
reactions.push((contact_id, Reaction::from(reaction.as_str())));
|
||||||
|
}
|
||||||
|
Ok(reactions)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
Ok(Reactions { reactions })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::chat::get_chat_msgs;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::constants::DC_CHAT_ID_TRASH;
|
||||||
|
use crate::contact::{Contact, Origin};
|
||||||
|
use crate::message::MessageState;
|
||||||
|
use crate::receive_imf::receive_imf;
|
||||||
|
use crate::test_utils::TestContext;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_reaction() {
|
||||||
|
// Check that basic set of emojis from RFC 9078 is supported.
|
||||||
|
assert_eq!(Reaction::from("👍").emojis(), vec!["👍"]);
|
||||||
|
assert_eq!(Reaction::from("👎").emojis(), vec!["👎"]);
|
||||||
|
assert_eq!(Reaction::from("😀").emojis(), vec!["😀"]);
|
||||||
|
assert_eq!(Reaction::from("☹").emojis(), vec!["☹"]);
|
||||||
|
assert_eq!(Reaction::from("😢").emojis(), vec!["😢"]);
|
||||||
|
|
||||||
|
// Empty string can be used to remove all reactions.
|
||||||
|
assert!(Reaction::from("").is_empty());
|
||||||
|
|
||||||
|
// Short strings can be used as emojis, could be used to add
|
||||||
|
// support for custom emojis via emoji shortcodes.
|
||||||
|
assert_eq!(Reaction::from(":deltacat:").emojis(), vec![":deltacat:"]);
|
||||||
|
|
||||||
|
// Check that long strings are not valid emojis.
|
||||||
|
assert!(
|
||||||
|
Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Multiple reactions separated by spaces or tabs are supported.
|
||||||
|
assert_eq!(Reaction::from("👍 ❤").emojis(), vec!["❤", "👍"]);
|
||||||
|
assert_eq!(Reaction::from("👍\t❤").emojis(), vec!["❤", "👍"]);
|
||||||
|
|
||||||
|
// Invalid emojis are removed, but valid emojis are retained.
|
||||||
|
assert_eq!(
|
||||||
|
Reaction::from("👍\t:foo: ❤").emojis(),
|
||||||
|
vec![":foo:", "❤", "👍"]
|
||||||
|
);
|
||||||
|
assert_eq!(Reaction::from("👍\t:foo: ❤").as_str(), ":foo: ❤ 👍");
|
||||||
|
|
||||||
|
// Duplicates are removed.
|
||||||
|
assert_eq!(Reaction::from("👍 👍").emojis(), vec!["👍"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_reaction() {
|
||||||
|
let reaction1 = Reaction::from("👍 😀");
|
||||||
|
let reaction2 = Reaction::from("❤");
|
||||||
|
let reaction_sum = reaction1.add(reaction2);
|
||||||
|
|
||||||
|
assert_eq!(reaction_sum.emojis(), vec!["❤", "👍", "😀"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_receive_reaction() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
alice.set_config(Config::ShowEmails, Some("2")).await?;
|
||||||
|
|
||||||
|
// Alice receives BCC-self copy of a message sent to Bob.
|
||||||
|
receive_imf(
|
||||||
|
&alice,
|
||||||
|
"To: bob@example.net\n\
|
||||||
|
From: alice@example.org\n\
|
||||||
|
Date: Today, 29 February 2021 00:00:00 -800\n\
|
||||||
|
Message-ID: 12345@example.org\n\
|
||||||
|
Subject: Meeting\n\
|
||||||
|
\n\
|
||||||
|
Can we chat at 1pm pacific, today?"
|
||||||
|
.as_bytes(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let msg = alice.get_last_msg().await;
|
||||||
|
assert_eq!(msg.state, MessageState::OutDelivered);
|
||||||
|
let reactions = get_msg_reactions(&alice, msg.id).await?;
|
||||||
|
let contacts = reactions.contacts();
|
||||||
|
assert_eq!(contacts.len(), 0);
|
||||||
|
|
||||||
|
let bob_id = Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated)
|
||||||
|
.await?
|
||||||
|
.0;
|
||||||
|
let bob_reaction = reactions.get(bob_id);
|
||||||
|
assert!(bob_reaction.is_empty()); // Bob has not reacted to message yet.
|
||||||
|
|
||||||
|
// Alice receives reaction to her message from Bob.
|
||||||
|
receive_imf(
|
||||||
|
&alice,
|
||||||
|
"To: alice@example.org\n\
|
||||||
|
From: bob@example.net\n\
|
||||||
|
Date: Today, 29 February 2021 00:00:10 -800\n\
|
||||||
|
Message-ID: 56789@example.net\n\
|
||||||
|
In-Reply-To: 12345@example.org\n\
|
||||||
|
Subject: Meeting\n\
|
||||||
|
Mime-Version: 1.0 (1.0)\n\
|
||||||
|
Content-Type: text/plain; charset=utf-8\n\
|
||||||
|
Content-Disposition: reaction\n\
|
||||||
|
\n\
|
||||||
|
\u{1F44D}"
|
||||||
|
.as_bytes(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let reactions = get_msg_reactions(&alice, msg.id).await?;
|
||||||
|
assert_eq!(reactions.to_string(), "👍1");
|
||||||
|
|
||||||
|
let contacts = reactions.contacts();
|
||||||
|
assert_eq!(contacts.len(), 1);
|
||||||
|
|
||||||
|
assert_eq!(contacts.get(0), Some(&bob_id));
|
||||||
|
let bob_reaction = reactions.get(bob_id);
|
||||||
|
assert_eq!(bob_reaction.is_empty(), false);
|
||||||
|
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
|
||||||
|
assert_eq!(bob_reaction.as_str(), "👍");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn expect_reactions_changed_event(
|
||||||
|
t: &TestContext,
|
||||||
|
expected_chat_id: ChatId,
|
||||||
|
expected_msg_id: MsgId,
|
||||||
|
expected_contact_id: ContactId,
|
||||||
|
) -> Result<()> {
|
||||||
|
let event = t
|
||||||
|
.evtracker
|
||||||
|
.get_matching(|evt| matches!(evt, EventType::ReactionsChanged { .. }))
|
||||||
|
.await;
|
||||||
|
match event {
|
||||||
|
EventType::ReactionsChanged {
|
||||||
|
chat_id,
|
||||||
|
msg_id,
|
||||||
|
contact_id,
|
||||||
|
} => {
|
||||||
|
assert_eq!(chat_id, expected_chat_id);
|
||||||
|
assert_eq!(msg_id, expected_msg_id);
|
||||||
|
assert_eq!(contact_id, expected_contact_id);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_send_reaction() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
|
||||||
|
let chat_alice = alice.create_chat(&bob).await;
|
||||||
|
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
|
||||||
|
let bob_msg = bob.recv_msg(&alice_msg).await;
|
||||||
|
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 1);
|
||||||
|
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 1);
|
||||||
|
|
||||||
|
let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
|
||||||
|
bob.recv_msg(&alice_msg2).await;
|
||||||
|
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 2);
|
||||||
|
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 2);
|
||||||
|
|
||||||
|
bob_msg.chat_id.accept(&bob).await?;
|
||||||
|
|
||||||
|
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
|
||||||
|
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
|
||||||
|
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 2);
|
||||||
|
|
||||||
|
let bob_reaction_msg = bob.pop_sent_msg().await;
|
||||||
|
let alice_reaction_msg = alice.recv_msg_opt(&bob_reaction_msg).await.unwrap();
|
||||||
|
assert_eq!(alice_reaction_msg.chat_id, DC_CHAT_ID_TRASH);
|
||||||
|
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 2);
|
||||||
|
|
||||||
|
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
|
||||||
|
assert_eq!(reactions.to_string(), "👍1");
|
||||||
|
let contacts = reactions.contacts();
|
||||||
|
assert_eq!(contacts.len(), 1);
|
||||||
|
let bob_id = contacts.get(0).unwrap();
|
||||||
|
let bob_reaction = reactions.get(*bob_id);
|
||||||
|
assert_eq!(bob_reaction.is_empty(), false);
|
||||||
|
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
|
||||||
|
assert_eq!(bob_reaction.as_str(), "👍");
|
||||||
|
expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Alice reacts to own message.
|
||||||
|
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
|
||||||
|
assert_eq!(reactions.to_string(), "👍2 😀1");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ use crate::mimeparser::{
|
|||||||
};
|
};
|
||||||
use crate::param::{Param, Params};
|
use crate::param::{Param, Params};
|
||||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
||||||
|
use crate::reaction::{set_msg_reaction, Reaction};
|
||||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||||
use crate::sql;
|
use crate::sql;
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
@@ -430,8 +431,9 @@ async fn add_parts(
|
|||||||
};
|
};
|
||||||
// incoming non-chat messages may be discarded
|
// incoming non-chat messages may be discarded
|
||||||
|
|
||||||
let location_kml_is = mime_parser.location_kml.is_some();
|
let is_location_kml = mime_parser.location_kml.is_some();
|
||||||
let is_mdn = !mime_parser.mdn_reports.is_empty();
|
let is_mdn = !mime_parser.mdn_reports.is_empty();
|
||||||
|
let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
|
||||||
let show_emails =
|
let show_emails =
|
||||||
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
|
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
|
||||||
|
|
||||||
@@ -450,7 +452,7 @@ async fn add_parts(
|
|||||||
ShowEmails::All => allow_creation = !is_mdn,
|
ShowEmails::All => allow_creation = !is_mdn,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
allow_creation = !is_mdn;
|
allow_creation = !is_mdn && !is_reaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the message introduces a new chat:
|
// check if the message introduces a new chat:
|
||||||
@@ -689,7 +691,8 @@ async fn add_parts(
|
|||||||
state = if seen
|
state = if seen
|
||||||
|| fetching_existing_messages
|
|| fetching_existing_messages
|
||||||
|| is_mdn
|
|| is_mdn
|
||||||
|| location_kml_is
|
|| is_reaction
|
||||||
|
|| is_location_kml
|
||||||
|| securejoin_seen
|
|| securejoin_seen
|
||||||
|| chat_id_blocked == Blocked::Yes
|
|| chat_id_blocked == Blocked::Yes
|
||||||
{
|
{
|
||||||
@@ -841,14 +844,15 @@ async fn add_parts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_mdn {
|
let orig_chat_id = chat_id;
|
||||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
let chat_id = if is_mdn || is_reaction {
|
||||||
}
|
|
||||||
|
|
||||||
let chat_id = chat_id.unwrap_or_else(|| {
|
|
||||||
info!(context, "No chat id for message (TRASH)");
|
|
||||||
DC_CHAT_ID_TRASH
|
DC_CHAT_ID_TRASH
|
||||||
});
|
} else {
|
||||||
|
chat_id.unwrap_or_else(|| {
|
||||||
|
info!(context, "No chat id for message (TRASH)");
|
||||||
|
DC_CHAT_ID_TRASH
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
// Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded.
|
// Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded.
|
||||||
let mut ephemeral_timer = if is_partial_download.is_some() {
|
let mut ephemeral_timer = if is_partial_download.is_some() {
|
||||||
@@ -1053,6 +1057,17 @@ async fn add_parts(
|
|||||||
let conn = context.sql.get_conn().await?;
|
let conn = context.sql.get_conn().await?;
|
||||||
|
|
||||||
for part in &mime_parser.parts {
|
for part in &mime_parser.parts {
|
||||||
|
if part.is_reaction {
|
||||||
|
set_msg_reaction(
|
||||||
|
context,
|
||||||
|
&mime_in_reply_to,
|
||||||
|
orig_chat_id.unwrap_or_default(),
|
||||||
|
from_id,
|
||||||
|
Reaction::from(part.msg.as_str()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut txt_raw = "".to_string();
|
let mut txt_raw = "".to_string();
|
||||||
let mut stmt = conn.prepare_cached(
|
let mut stmt = conn.prepare_cached(
|
||||||
r#"
|
r#"
|
||||||
@@ -1113,7 +1128,7 @@ INSERT INTO msgs
|
|||||||
|
|
||||||
// If you change which information is skipped if the message is trashed,
|
// If you change which information is skipped if the message is trashed,
|
||||||
// also change `MsgId::trash()` and `delete_expired_messages()`
|
// also change `MsgId::trash()` and `delete_expired_messages()`
|
||||||
let trash = chat_id.is_trash() || (location_kml_is && msg.is_empty());
|
let trash = chat_id.is_trash() || (is_location_kml && msg.is_empty());
|
||||||
|
|
||||||
stmt.execute(paramsv)
|
Proxy | SOCKS5 ([RFC 1928](https://tools.ietf.org/html/rfc1928))
|
||||||
Embedded media | MIME Document Series ([RFC 2045](https://tools.ietf.org/html/rfc2045), [RFC 2046](https://tools.ietf.org/html/rfc2046)), Content-Disposition Header ([RFC 2183](https://tools.ietf.org/html/rfc2183)), Multipart/Related ([RFC 2387](https://tools.ietf.org/html/rfc2387))
|
Embedded media | MIME Document Series ([RFC 2045](https://tools.ietf.org/html/rfc2045), [RFC 2046](https://tools.ietf.org/html/rfc2046)), Content-Disposition Header ([RFC 2183](https://tools.ietf.org/html/rfc2183)), Multipart/Related ([RFC 2387](https://tools.ietf.org/html/rfc2387))
|
||||||
Text and Quote encoding | Fixed, Flowed ([RFC 3676](https://tools.ietf.org/html/rfc3676))
|
Text and Quote encoding | Fixed, Flowed ([RFC 3676](https://tools.ietf.org/html/rfc3676))
|
||||||
|
Reactions | Reaction: Indicating Summary Reaction to a Message [RFC 9078](https://datatracker.ietf.org/doc/rfc9078/)
|
||||||
Filename encoding | Encoded Words ([RFC 2047](https://tools.ietf.org/html/rfc2047)), Encoded Word Extensions ([RFC 2231](https://tools.ietf.org/html/rfc2231))
|
Filename encoding | Encoded Words ([RFC 2047](https://tools.ietf.org/html/rfc2047)), Encoded Word Extensions ([RFC 2231](https://tools.ietf.org/html/rfc2231))
|
||||||
Identify server folders | IMAP LIST Extension ([RFC 6154](https://tools.ietf.org/html/rfc6154))
|
Identify server folders | IMAP LIST Extension ([RFC 6154](https://tools.ietf.org/html/rfc6154))
|
||||||
Push | IMAP IDLE ([RFC 2177](https://tools.ietf.org/html/rfc2177))
|
Push | IMAP IDLE ([RFC 2177](https://tools.ietf.org/html/rfc2177))
|
||||||
|
|||||||
Reference in New Issue
Block a user