mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 09:26:29 +03:00
Implement reactions
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### API-Changes
|
### API-Changes
|
||||||
|
- added reactions support #3644
|
||||||
|
|
||||||
### 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(),
|
||||||
|
|||||||
@@ -205,6 +205,9 @@ pub enum MessageViewtype {
|
|||||||
/// Text message.
|
/// Text message.
|
||||||
Text,
|
Text,
|
||||||
|
|
||||||
|
/// Reaction emoji.
|
||||||
|
Reaction,
|
||||||
|
|
||||||
/// Image message.
|
/// Image message.
|
||||||
/// If the image is an animated GIF, the type `Viewtype.Gif` should be used.
|
/// If the image is an animated GIF, the type `Viewtype.Gif` should be used.
|
||||||
Image,
|
Image,
|
||||||
@@ -242,6 +245,7 @@ impl From<Viewtype> for MessageViewtype {
|
|||||||
match viewtype {
|
match viewtype {
|
||||||
Viewtype::Unknown => MessageViewtype::Unknown,
|
Viewtype::Unknown => MessageViewtype::Unknown,
|
||||||
Viewtype::Text => MessageViewtype::Text,
|
Viewtype::Text => MessageViewtype::Text,
|
||||||
|
Viewtype::Reaction => MessageViewtype::Reaction,
|
||||||
Viewtype::Image => MessageViewtype::Image,
|
Viewtype::Image => MessageViewtype::Image,
|
||||||
Viewtype::Gif => MessageViewtype::Gif,
|
Viewtype::Gif => MessageViewtype::Gif,
|
||||||
Viewtype::Sticker => MessageViewtype::Sticker,
|
Viewtype::Sticker => MessageViewtype::Sticker,
|
||||||
@@ -260,6 +264,7 @@ impl From<MessageViewtype> for Viewtype {
|
|||||||
match viewtype {
|
match viewtype {
|
||||||
MessageViewtype::Unknown => Viewtype::Unknown,
|
MessageViewtype::Unknown => Viewtype::Unknown,
|
||||||
MessageViewtype::Text => Viewtype::Text,
|
MessageViewtype::Text => Viewtype::Text,
|
||||||
|
MessageViewtype::Reaction => Viewtype::Reaction,
|
||||||
MessageViewtype::Image => Viewtype::Image,
|
MessageViewtype::Image => Viewtype::Image,
|
||||||
MessageViewtype::Gif => Viewtype::Gif,
|
MessageViewtype::Gif => Viewtype::Gif,
|
||||||
MessageViewtype::Sticker => Viewtype::Sticker,
|
MessageViewtype::Sticker => Viewtype::Sticker,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -1812,7 +1812,10 @@ pub async fn prepare_msg(context: &Context, chat_id: ChatId, msg: &mut Message)
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||||
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
|
if msg.viewtype == Viewtype::Text
|
||||||
|
|| msg.viewtype == Viewtype::VideochatInvitation
|
||||||
|
|| msg.viewtype == Viewtype::Reaction
|
||||||
|
{
|
||||||
// the caller should check if the message text is empty
|
// the caller should check if the message text is empty
|
||||||
} else if msg.viewtype.has_file() {
|
} else if msg.viewtype.has_file() {
|
||||||
let blob = msg
|
let blob = msg
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1082,6 +1083,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);
|
||||||
}
|
}
|
||||||
@@ -1762,6 +1768,9 @@ pub enum Viewtype {
|
|||||||
/// The text of the message is set using dc_msg_set_text() and retrieved with dc_msg_get_text().
|
/// The text of the message is set using dc_msg_set_text() and retrieved with dc_msg_get_text().
|
||||||
Text = 10,
|
Text = 10,
|
||||||
|
|
||||||
|
/// RFC 9078 reaction.
|
||||||
|
Reaction = 11,
|
||||||
|
|
||||||
/// Image message.
|
/// Image message.
|
||||||
/// If the image is an animated GIF, the type DC_MSG_GIF should be used.
|
/// If the image is an animated GIF, the type DC_MSG_GIF should be used.
|
||||||
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
|
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
|
||||||
@@ -1821,6 +1830,7 @@ impl Viewtype {
|
|||||||
match self {
|
match self {
|
||||||
Viewtype::Unknown => false,
|
Viewtype::Unknown => false,
|
||||||
Viewtype::Text => false,
|
Viewtype::Text => false,
|
||||||
|
Viewtype::Reaction => false,
|
||||||
Viewtype::Image => true,
|
Viewtype::Image => true,
|
||||||
Viewtype::Gif => true,
|
Viewtype::Gif => true,
|
||||||
Viewtype::Sticker => true,
|
Viewtype::Sticker => true,
|
||||||
|
|||||||
@@ -1122,6 +1122,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.viewtype == Viewtype::Reaction {
|
||||||
|
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;
|
||||||
|
|||||||
@@ -465,7 +465,10 @@ impl MimeMessage {
|
|||||||
| Viewtype::Video
|
| Viewtype::Video
|
||||||
| Viewtype::File
|
| Viewtype::File
|
||||||
| Viewtype::Webxdc => true,
|
| Viewtype::Webxdc => true,
|
||||||
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
|
Viewtype::Unknown
|
||||||
|
| Viewtype::Text
|
||||||
|
| Viewtype::Reaction
|
||||||
|
| Viewtype::VideochatInvitation => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if need_drop {
|
if need_drop {
|
||||||
@@ -551,7 +554,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.typ != Viewtype::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 +919,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 +953,26 @@ impl MimeMessage {
|
|||||||
warn!(context, "Missing attachment");
|
warn!(context, "Missing attachment");
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
mime::TEXT if msg_type == Viewtype::Reaction => {
|
||||||
|
// 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::Reaction,
|
||||||
|
mimetype: Some(mime_type),
|
||||||
|
msg: decoded_data,
|
||||||
|
..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,
|
||||||
@@ -1652,7 +1679,11 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
|
|||||||
|
|
||||||
let viewtype = match mimetype.type_() {
|
let viewtype = match mimetype.type_() {
|
||||||
mime::TEXT => {
|
mime::TEXT => {
|
||||||
if !is_attachment_disposition(mail) {
|
if mail.get_content_disposition().disposition
|
||||||
|
== DispositionType::Extension("reaction".to_string())
|
||||||
|
{
|
||||||
|
Viewtype::Reaction
|
||||||
|
} else if !is_attachment_disposition(mail) {
|
||||||
match mimetype.subtype() {
|
match mimetype.subtype() {
|
||||||
mime::PLAIN | mime::HTML => Viewtype::Text,
|
mime::PLAIN | mime::HTML => Viewtype::Text,
|
||||||
_ => Viewtype::File,
|
_ => Viewtype::File,
|
||||||
@@ -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].typ, Viewtype::Reaction);
|
||||||
|
assert_eq!(
|
||||||
|
mime_message
|
||||||
|
.get_header(HeaderDef::InReplyTo)
|
||||||
|
.and_then(|msgid| parse_message_id(msgid).ok())
|
||||||
|
.unwrap(),
|
||||||
|
"12345@example.org"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
428
src/reaction.rs
Normal file
428
src/reaction.rs
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
//! # 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 ase 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 = true;
|
||||||
|
}
|
||||||
|
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::Reaction);
|
||||||
|
reaction_msg.text = Some(reaction.as_str().to_string());
|
||||||
|
|
||||||
|
set_msg_id_reaction(context, msg_id, msg.chat_id, ContactId::SELF, reaction).await?;
|
||||||
|
send_msg(context, chat_id, &mut reaction_msg).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 reacing 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::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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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;
|
||||||
|
|
||||||
|
bob_msg.chat_id.accept(&bob).await?;
|
||||||
|
|
||||||
|
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
|
||||||
|
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);
|
||||||
|
|
||||||
|
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(), "👍");
|
||||||
|
|
||||||
|
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,12 @@ 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.typ == Viewtype::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();
|
||||||
|
|
||||||
@@ -689,7 +694,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,7 +847,7 @@ async fn add_parts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_mdn {
|
if is_mdn || is_reaction {
|
||||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1053,6 +1059,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.typ == Viewtype::Reaction {
|
||||||
|
set_msg_reaction(
|
||||||
|
context,
|
||||||
|
&mime_in_reply_to,
|
||||||
|
chat_id,
|
||||||
|
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 +1130,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