Merge pull request #854 from deltachat/tweak-device-msg

tweak device messages
This commit is contained in:
björn petersen
2019-11-19 22:19:42 +01:00
committed by GitHub
6 changed files with 197 additions and 128 deletions

View File

@@ -1096,9 +1096,9 @@ void dc_set_draft (dc_context_t* context, uint32_t ch
* Add a message to the device-chat. * Add a message to the device-chat.
* Device-messages usually contain update information * Device-messages usually contain update information
* and some hints that are added during the program runs, multi-device etc. * and some hints that are added during the program runs, multi-device etc.
* * The device-message may be defined by a label;
* Device-messages may be added from the core, * if a message with the same label was added or skipped before,
* however, with this function, this can be done from the ui as well. * the message is not added again, even if the message was deleted in between.
* If needed, the device-chat is created before. * If needed, the device-chat is created before.
* *
* Sends the event #DC_EVENT_MSGS_CHANGED on success. * Sends the event #DC_EVENT_MSGS_CHANGED on success.
@@ -1106,33 +1106,50 @@ void dc_set_draft (dc_context_t* context, uint32_t ch
* *
* @memberof dc_context_t * @memberof dc_context_t
* @param context The context as created by dc_context_new(). * @param context The context as created by dc_context_new().
* @param msg Message to be added to the device-chat.
* The message appears to the user as an incoming message.
* @return The ID of the added message.
*/
uint32_t dc_add_device_msg (dc_context_t* context, dc_msg_t* msg);
/**
* Add a message only one time to the device-chat.
* The device-message is defined by a name.
* If a message with the same name was added before,
* the message is not added again.
* Use dc_add_device_msg() to add device-messages unconditionally.
*
* Sends the event #DC_EVENT_MSGS_CHANGED on success.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @param label A unique name for the message to add. * @param label A unique name for the message to add.
* The label is typically not displayed to the user and * The label is typically not displayed to the user and
* must be created from the characters `A-Z`, `a-z`, `0-9`, `_` or `-`. * must be created from the characters `A-Z`, `a-z`, `0-9`, `_` or `-`.
* If you pass NULL here, the message is added unconditionally.
* @param msg Message to be added to the device-chat. * @param msg Message to be added to the device-chat.
* The message appears to the user as an incoming message. * The message appears to the user as an incoming message.
* @return The ID of the added message, * If you pass NULL here, only the given label will be added
* this might be the id of an older message with the same name. * and block adding messages with that label in the future.
* @return The ID of the just added message,
* if the message was already added or no message to add is given, 0 is returned.
*
* Example:
* ~~~
* dc_msg_t* welcome_msg = dc_msg_new(DC_MSG_TEXT);
* dc_msg_set_text(welcome_msg, "great that you give this app a try!");
*
* dc_msg_t* changelog_msg = dc_msg_new(DC_MSG_TEXT);
* dc_msg_set_text(changelog_msg, "we have added 3 new emojis :)");
*
* if (dc_add_device_msg(context, "welcome", welcome_msg)) {
* // do not add the changelog on a new installations -
* // not now and not when this code is executed again
* dc_add_device_msg(context, "update-123", NULL);
* } else {
* // welcome message was not added now, this is an oder installation,
* // add a changelog
* dc_add_device_msg(context, "update-123", changelog_msg);
* }
* ~~~
*/ */
uint32_t dc_add_device_msg_once (dc_context_t* context, const char* label, dc_msg_t* msg); uint32_t dc_add_device_msg (dc_context_t* context, const char* label, dc_msg_t* msg);
/**
* Check if a device-message with a given label was ever added.
* Device-messages can be added dc_add_device_msg().
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @param label Label of the message to check.
* @return 1=A message with this label was added at some point,
* 0=A message with this label was never added.
*/
int dc_was_device_msg_ever_added (dc_context_t* context, const char* label);
/** /**
@@ -2760,9 +2777,7 @@ int dc_chat_is_self_talk (const dc_chat_t* chat);
* From the ui view, device-talks are not very special, * From the ui view, device-talks are not very special,
* the user can delete and forward messages, archive the chat, set notifications etc. * the user can delete and forward messages, archive the chat, set notifications etc.
* *
* Messages may be added from the core to the device chat, * Messages can be added to the device-talk using dc_add_device_msg()
* so the chat just pops up as usual.
* However, if needed the ui can also add messages using dc_add_device_msg()
* *
* @memberof dc_chat_t * @memberof dc_chat_t
* @param chat The chat object. * @param chat The chat object.

View File

@@ -812,40 +812,50 @@ pub unsafe extern "C" fn dc_set_draft(
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn dc_add_device_msg(context: *mut dc_context_t, msg: *mut dc_msg_t) -> u32 { pub unsafe extern "C" fn dc_add_device_msg(
if context.is_null() || msg.is_null() { context: *mut dc_context_t,
label: *const libc::c_char,
msg: *mut dc_msg_t,
) -> u32 {
if context.is_null() || (label.is_null() && msg.is_null()) {
eprintln!("ignoring careless call to dc_add_device_msg()"); eprintln!("ignoring careless call to dc_add_device_msg()");
return 0; return 0;
} }
let ffi_context = &mut *context; let ffi_context = &mut *context;
let ffi_msg = &mut *msg; let msg = if msg.is_null() {
None
} else {
let ffi_msg: &mut MessageWrapper = &mut *msg;
Some(&mut ffi_msg.message)
};
ffi_context ffi_context
.with_inner(|ctx| { .with_inner(|ctx| {
chat::add_device_msg(ctx, &mut ffi_msg.message) chat::add_device_msg(
.unwrap_or_log_default(ctx, "Failed to add device message") ctx,
to_opt_string_lossy(label).as_ref().map(|x| x.as_str()),
msg,
)
.unwrap_or_log_default(ctx, "Failed to add device message")
}) })
.map(|msg_id| msg_id.to_u32()) .map(|msg_id| msg_id.to_u32())
.unwrap_or(0) .unwrap_or(0)
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn dc_add_device_msg_once( pub unsafe extern "C" fn dc_was_device_msg_ever_added(
context: *mut dc_context_t, context: *mut dc_context_t,
label: *const libc::c_char, label: *const libc::c_char,
msg: *mut dc_msg_t, ) -> libc::c_int {
) -> u32 { if context.is_null() || label.is_null() {
if context.is_null() || label.is_null() || msg.is_null() { eprintln!("ignoring careless call to dc_was_device_msg_ever_added()");
eprintln!("ignoring careless call to dc_add_device_msg_once()");
return 0; return 0;
} }
let ffi_context = &mut *context; let ffi_context = &mut *context;
let ffi_msg = &mut *msg;
ffi_context ffi_context
.with_inner(|ctx| { .with_inner(|ctx| {
chat::add_device_msg_once(ctx, &to_string_lossy(label), &mut ffi_msg.message) chat::was_device_msg_ever_added(ctx, &to_string_lossy(label)).unwrap_or(false)
.unwrap_or_log_default(ctx, "Failed to add device message once") as libc::c_int
}) })
.map(|msg_id| msg_id.to_u32())
.unwrap_or(0) .unwrap_or(0)
} }

View File

@@ -837,7 +837,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
); );
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(arg1.to_string())); msg.set_text(Some(arg1.to_string()));
chat::add_device_msg(context, &mut msg)?; chat::add_device_msg(context, None, Some(&mut msg))?;
} }
"listmedia" => { "listmedia" => {
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");

View File

@@ -1951,78 +1951,78 @@ pub fn get_chat_id_by_grpid(context: &Context, grpid: impl AsRef<str>) -> (u32,
.unwrap_or((0, false, Blocked::Not)) .unwrap_or((0, false, Blocked::Not))
} }
pub fn add_device_msg(context: &Context, msg: &mut Message) -> Result<MsgId, Error> { pub fn add_device_msg(
add_device_msg_maybe_labelled(context, None, msg)
}
pub fn add_device_msg_once(
context: &Context,
label: &str,
msg: &mut Message,
) -> Result<MsgId, Error> {
add_device_msg_maybe_labelled(context, Some(label), msg)
}
fn add_device_msg_maybe_labelled(
context: &Context, context: &Context,
label: Option<&str>, label: Option<&str>,
msg: &mut Message, msg: Option<&mut Message>,
) -> Result<MsgId, Error> { ) -> Result<MsgId, Error> {
ensure!(
label.is_some() || msg.is_some(),
"device-messages need label, msg or both"
);
let (chat_id, _blocked) = let (chat_id, _blocked) =
create_or_lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE, Blocked::Not)?; create_or_lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE, Blocked::Not)?;
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device"); let mut msg_id = MsgId::new_unset();
// chat_id has an sql-index so it makes sense to add this although redundant
if let Some(label) = label { if let Some(label) = label {
if let Ok(msg_id) = context.sql.query_row( if was_device_msg_ever_added(context, label)? {
"SELECT id FROM msgs WHERE chat_id=? AND label=?", info!(context, "device-message {} already added", label);
params![chat_id, label],
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
},
) {
info!(
context,
"device-message {} already exist as {}", label, msg_id
);
return Ok(msg_id); return Ok(msg_id);
} }
} }
prepare_msg_blob(context, msg)?; if let Some(msg) = msg {
unarchive(context, chat_id)?; let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
prepare_msg_blob(context, msg)?;
unarchive(context, chat_id)?;
context.sql.execute( context.sql.execute(
"INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,param,rfc724_mid,label) \ "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,param,rfc724_mid) \
VALUES (?,?,?, ?,?,?, ?,?,?,?);", VALUES (?,?,?, ?,?,?, ?,?,?);",
params![ params![
chat_id, chat_id,
DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_DEVICE,
DC_CONTACT_ID_SELF, DC_CONTACT_ID_SELF,
dc_create_smeared_timestamp(context), dc_create_smeared_timestamp(context),
msg.type_0, msg.type_0,
MessageState::InFresh, MessageState::InFresh,
msg.text.as_ref().map_or("", String::as_str), msg.text.as_ref().map_or("", String::as_str),
msg.param.to_string(), msg.param.to_string(),
rfc724_mid, rfc724_mid,
label.unwrap_or_default(), ],
], )?;
)?;
let row_id = sql::get_rowid(context, &context.sql, "msgs", "rfc724_mid", &rfc724_mid); let row_id = sql::get_rowid(context, &context.sql, "msgs", "rfc724_mid", &rfc724_mid);
let msg_id = MsgId::new(row_id); msg_id = MsgId::new(row_id);
context.call_cb(Event::IncomingMsg { chat_id, msg_id }); }
info!(
context, if let Some(label) = label {
"device-message {} added as {}", context.sql.execute(
label.unwrap_or("without label"), "INSERT INTO devmsglabels (label) VALUES (?);",
msg_id params![label],
); )?;
}
if !msg_id.is_unset() {
context.call_cb(Event::IncomingMsg { chat_id, msg_id });
}
Ok(msg_id) Ok(msg_id)
} }
pub fn was_device_msg_ever_added(context: &Context, label: &str) -> Result<bool, Error> {
ensure!(!label.is_empty(), "empty label");
if let Ok(()) = context.sql.query_row(
"SELECT label FROM devmsglabels WHERE label=?",
params![label],
|_| Ok(()),
) {
return Ok(true);
}
Ok(false)
}
pub fn add_info_msg(context: &Context, chat_id: u32, text: impl AsRef<str>) { pub fn add_info_msg(context: &Context, chat_id: u32, text: impl AsRef<str>) {
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device"); let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
@@ -2131,18 +2131,18 @@ mod tests {
} }
#[test] #[test]
fn test_add_device_msg() { fn test_add_device_msg_unlabelled() {
let t = test_context(Some(Box::new(logging_cb))); let t = test_context(Some(Box::new(logging_cb)));
// add two device-messages // add two device-messages
let mut msg1 = Message::new(Viewtype::Text); let mut msg1 = Message::new(Viewtype::Text);
msg1.text = Some("first message".to_string()); msg1.text = Some("first message".to_string());
let msg1_id = add_device_msg(&t.ctx, &mut msg1); let msg1_id = add_device_msg(&t.ctx, None, Some(&mut msg1));
assert!(msg1_id.is_ok()); assert!(msg1_id.is_ok());
let mut msg2 = Message::new(Viewtype::Text); let mut msg2 = Message::new(Viewtype::Text);
msg2.text = Some("second message".to_string()); msg2.text = Some("second message".to_string());
let msg2_id = add_device_msg(&t.ctx, &mut msg2); let msg2_id = add_device_msg(&t.ctx, None, Some(&mut msg2));
assert!(msg2_id.is_ok()); assert!(msg2_id.is_ok());
assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap()); assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap());
@@ -2166,34 +2166,35 @@ mod tests {
} }
#[test] #[test]
fn test_add_device_msg_once() { fn test_add_device_msg_labelled() {
let t = test_context(Some(Box::new(logging_cb))); let t = test_context(Some(Box::new(logging_cb)));
// add two device-messages with the same label (second attempt is not added) // add two device-messages with the same label (second attempt is not added)
let mut msg1 = Message::new(Viewtype::Text); let mut msg1 = Message::new(Viewtype::Text);
msg1.text = Some("first message".to_string()); msg1.text = Some("first message".to_string());
let msg1_id = add_device_msg_once(&t.ctx, "any-label", &mut msg1); let msg1_id = add_device_msg(&t.ctx, Some("any-label"), Some(&mut msg1));
assert!(msg1_id.is_ok()); assert!(msg1_id.is_ok());
assert!(!msg1_id.as_ref().unwrap().is_unset());
let mut msg2 = Message::new(Viewtype::Text); let mut msg2 = Message::new(Viewtype::Text);
msg2.text = Some("second message".to_string()); msg2.text = Some("second message".to_string());
let msg2_id = add_device_msg_once(&t.ctx, "any-label", &mut msg2); let msg2_id = add_device_msg(&t.ctx, Some("any-label"), Some(&mut msg2));
assert!(msg2_id.is_ok()); assert!(msg2_id.is_ok());
assert_eq!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap()); assert!(msg2_id.as_ref().unwrap().is_unset());
// check added message // check added message
let msg2 = message::Message::load_from_db(&t.ctx, msg2_id.unwrap()); let msg1 = message::Message::load_from_db(&t.ctx, *msg1_id.as_ref().unwrap());
assert!(msg2.is_ok()); assert!(msg1.is_ok());
let msg2 = msg2.unwrap(); let msg1 = msg1.unwrap();
assert_eq!(msg1_id.unwrap(), msg2.id); assert_eq!(msg1_id.as_ref().unwrap(), &msg1.id);
assert_eq!(msg2.text.as_ref().unwrap(), "first message"); assert_eq!(msg1.text.as_ref().unwrap(), "first message");
assert_eq!(msg2.from_id, DC_CONTACT_ID_DEVICE); assert_eq!(msg1.from_id, DC_CONTACT_ID_DEVICE);
assert_eq!(msg2.to_id, DC_CONTACT_ID_SELF); assert_eq!(msg1.to_id, DC_CONTACT_ID_SELF);
assert!(!msg2.is_info()); assert!(!msg1.is_info());
assert!(!msg2.is_setupmessage()); assert!(!msg1.is_setupmessage());
// check device chat // check device chat
let chat_id = msg2.chat_id; let chat_id = msg1.chat_id;
assert_eq!(get_msg_cnt(&t.ctx, chat_id), 1); assert_eq!(get_msg_cnt(&t.ctx, chat_id), 1);
assert!(chat_id > DC_CHAT_ID_LAST_SPECIAL); assert!(chat_id > DC_CHAT_ID_LAST_SPECIAL);
let chat = Chat::load_from_db(&t.ctx, chat_id); let chat = Chat::load_from_db(&t.ctx, chat_id);
@@ -2204,6 +2205,50 @@ mod tests {
assert!(!chat.can_send()); assert!(!chat.can_send());
assert_eq!(chat.name, t.ctx.stock_str(StockMessage::DeviceMessages)); assert_eq!(chat.name, t.ctx.stock_str(StockMessage::DeviceMessages));
assert!(chat.get_profile_image(&t.ctx).is_some()); assert!(chat.get_profile_image(&t.ctx).is_some());
// delete device message, make sure it is not added again
message::delete_msgs(&t.ctx, &[*msg1_id.as_ref().unwrap()]);
let msg1 = message::Message::load_from_db(&t.ctx, *msg1_id.as_ref().unwrap());
assert!(msg1.is_err() || msg1.unwrap().chat_id == DC_CHAT_ID_TRASH);
let msg3_id = add_device_msg(&t.ctx, Some("any-label"), Some(&mut msg2));
assert!(msg3_id.is_ok());
assert!(msg2_id.as_ref().unwrap().is_unset());
}
#[test]
fn test_add_device_msg_label_only() {
let t = test_context(Some(Box::new(logging_cb)));
let res = add_device_msg(&t.ctx, Some(""), None);
assert!(res.is_err());
let res = add_device_msg(&t.ctx, Some("some-label"), None);
assert!(res.is_ok());
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("message text".to_string());
let msg_id = add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg));
assert!(msg_id.is_ok());
assert!(msg_id.as_ref().unwrap().is_unset());
let msg_id = add_device_msg(&t.ctx, Some("unused-label"), Some(&mut msg));
assert!(msg_id.is_ok());
assert!(!msg_id.as_ref().unwrap().is_unset());
}
#[test]
fn test_was_device_msg_ever_added() {
let t = test_context(Some(Box::new(logging_cb)));
add_device_msg(&t.ctx, Some("some-label"), None).ok();
assert!(was_device_msg_ever_added(&t.ctx, "some-label").unwrap());
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("message text".to_string());
add_device_msg(&t.ctx, Some("another-label"), Some(&mut msg)).ok();
assert!(was_device_msg_ever_added(&t.ctx, "another-label").unwrap());
assert!(!was_device_msg_ever_added(&t.ctx, "unused-label").unwrap());
assert!(was_device_msg_ever_added(&t.ctx, "").is_err());
} }
fn chatlist_len(ctx: &Context, listflags: usize) -> usize { fn chatlist_len(ctx: &Context, listflags: usize) -> usize {
@@ -2218,7 +2263,7 @@ mod tests {
let t = dummy_context(); let t = dummy_context();
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
msg.text = Some("foo".to_string()); msg.text = Some("foo".to_string());
let msg_id = add_device_msg(&t.ctx, &mut msg).unwrap(); let msg_id = add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap();
let chat_id1 = message::Message::load_from_db(&t.ctx, msg_id) let chat_id1 = message::Message::load_from_db(&t.ctx, msg_id)
.unwrap() .unwrap()
.chat_id; .chat_id;

View File

@@ -247,7 +247,7 @@ fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
go to the settings and enable \"Send copy to self\"." go to the settings and enable \"Send copy to self\"."
.to_string(), .to_string(),
); );
chat::add_device_msg_once(context, "bcc-self-hint", &mut msg)?; chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg))?;
} }
Ok(()) Ok(())
} }

View File

@@ -810,24 +810,23 @@ fn open(
)?; )?;
sql.set_raw_config_int(context, "dbversion", 55)?; sql.set_raw_config_int(context, "dbversion", 55)?;
} }
if dbversion < 57 { if dbversion < 59 {
info!(context, "[migration] v57"); info!(context, "[migration] v59");
// label is a unique name and is currently used for device-messages only. // records in the devmsglabels are kept when the message is deleted.
// in contrast to rfc724_mid and other fields, the label is generated on the device // so, msg_id may or may not exist.
// and allows reliable identifications this way.
sql.execute( sql.execute(
"ALTER TABLE msgs ADD COLUMN label TEXT DEFAULT '';", "CREATE TABLE devmsglabels (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT, msg_id INTEGER DEFAULT 0);",
params![], NO_PARAMS,
)?;
sql.execute(
"CREATE INDEX devmsglabels_index1 ON devmsglabels (label);",
NO_PARAMS,
)?; )?;
if exists_before_update && sql.get_raw_config_int(context, "bcc_self").is_none() { if exists_before_update && sql.get_raw_config_int(context, "bcc_self").is_none() {
sql.set_raw_config_int(context, "bcc_self", 1)?; sql.set_raw_config_int(context, "bcc_self", 1)?;
} }
sql.set_raw_config_int(context, "dbversion", 57)?;
}
if dbversion < 58 {
info!(context, "[migration] v58");
update_icons = true; update_icons = true;
sql.set_raw_config_int(context, "dbversion", 58)?; sql.set_raw_config_int(context, "dbversion", 59)?;
} }
// (2) updates that require high-level objects // (2) updates that require high-level objects