Skip to content

Declining a call desyncs read states between devices #8631

Open

Description

@Amzd
  • Operating System (Linux/Mac/Windows/iOS/Android): *
  • Core Version: 2.59.0
  • Client Version: 2.59.1

Expected behavior

Declining a call on one device should not impact read state of other messages on a different device.

Actual behavior

Declining a call desyncs read states and marks whole chat as noticed on other devices which is not nice UX

  • Receive a message from alice
  • Receive an incoming call from alice on 2 devices (eg iOS and desktop)
  • Decline on desktop (without opening the chat)
  • Now the initial message is no longer shown as new on iOS and the notifications will be cleared
  • On the device where the call is declined any unread messages before the call are still correctly counted as fresh

(the same happens when you decline from the iOS instead of desktop but less annoying because I don't rely on notifications as much on desktop)

Screenshots

Logs

Activity

  1. added
    bugSomething is not working
    on Aug 26, 2026
  2. self-assigned this
    on Aug 27, 2026
  3. link2xt commented on Aug 27, 2026

    @link2xt
    Collaborator

    This is likely caused by marking the call message as seen when ending the call (end_call is used for declining as well):

    markseen_msgs(self, vec![call_id]).await?;

    This sends and MDN (read receipt) to self which marks all messages above as seen:

    core/src/receive_imf.rs

    Lines 911 to 980 in a344cab

    if !mime_parser.incoming && !context.get_config_bool(Config::TeamProfile).await? {
    let mut updated_chats = BTreeMap::new();
    let mut archived_chats_maybe_noticed = false;
    for report in &mime_parser.mdn_reports {
    for msg_rfc724_mid in report
    .original_message_id
    .iter()
    .chain(&report.additional_message_ids)
    {
    let Some(msg_id) = rfc724_mid_exists(context, msg_rfc724_mid).await? else {
    continue;
    };
    let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
    continue;
    };
    if msg.state < MessageState::InFresh || msg.state >= MessageState::InSeen {
    continue;
    }
    if !mime_parser.was_encrypted() && msg.get_showpadlock() {
    warn!(context, "MDN: Not encrypted. Ignoring.");
    continue;
    }
    message::update_msg_state(context, msg_id, MessageState::InSeen).await?;
    if let Err(e) = msg_id.start_ephemeral_timer(context).await {
    error!(context, "start_ephemeral_timer for {msg_id}: {e:#}.");
    }
    if !mime_parser.has_chat_version() {
    continue;
    }
    archived_chats_maybe_noticed |= msg.state < MessageState::InNoticed
    && msg.chat_visibility == ChatVisibility::Archived;
    updated_chats
    .entry(msg.chat_id)
    .and_modify(|pos| *pos = cmp::max(*pos, (msg.timestamp_sort, msg.id)))
    .or_insert((msg.timestamp_sort, msg.id));
    }
    }
    for (chat_id, (timestamp_sort, msg_id)) in updated_chats {
    context
    .sql
    .execute(
    "
    UPDATE msgs SET state=? WHERE
    state=? AND
    hidden=0 AND
    chat_id=? AND
    (timestamp,id)<(?,?)",
    (
    MessageState::InNoticed,
    MessageState::InFresh,
    chat_id,
    timestamp_sort,
    msg_id,
    ),
    )
    .await
    .context("UPDATE msgs.state")?;
    if chat_id.get_fresh_msg_cnt(context).await? == 0 {
    // Removes all notifications for the chat in UIs.
    context.emit_event(EventType::MsgsNoticed(chat_id));
    } else {
    context.emit_msgs_changed_without_msg_id(chat_id);
    }
    chatlist_events::emit_chatlist_item_changed(context, chat_id);
    }
    if archived_chats_maybe_noticed {
    context.on_archived_chats_maybe_noticed();
    }
    }
    }

    I think the solution might be not to mark the call message as read when the call is ended, but mark it as noticed locally instead, and also mark it as noticed when a copy of the message declining a call is received on a second device.

  4. Amzd commented on Aug 28, 2026

    @Amzd
    CollaboratorAuthor

    I do like that you get read check marks on caller side when the call was declined

  5. link2xt commented on Aug 28, 2026

    @link2xt
    Collaborator

    I do like that you get read check marks on caller side when the call was declined

    If we want to keep double checkmark when someone declines a call, we can either:

    1. Special-case MDN processing on the receiver side, and if it is an MDN for a call, not mark all messages above as noticed. This looks hacky, also because MDN might mean the user has actually opened a chat and looked at the message, so this will introduce an opposite bug where looking at a page full of missed calls will not mark the chat as noticed on another device.
    2. Mark call message as read when we receive any response (ending a call or answering it). But need to make sure we don't do it if read receipt was not requested, otherwise user with read receipts disabled will get double checkmarks, but only on calls.

    You can also see on the call that it was declined (and not canceled) even without read receipts, so the information that callee has tapped "decline" is there anyway, so the difference will be that there will be no double checkmark here:
    Image
    You can still see that in the first case callee interacted with the call, and in the second case did not click anything or was offline.

  6. Amzd commented on Sep 9, 2026

    @Amzd
    CollaboratorAuthor

    Okay the checkmarks are actually less important than this bug so lets just remove the two checkmarks for now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething is not working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions