Skip to content

fix: start notification expire timer only after bubble is displayed - #1691

Open
Ivy233 wants to merge 1 commit into
linuxdeepin:masterfrom
Ivy233:fix/notification-expire-after-display
Open

fix: start notification expire timer only after bubble is displayed#1691
Ivy233 wants to merge 1 commit into
linuxdeepin:masterfrom
Ivy233:fix/notification-expire-after-display

Conversation

@Ivy233

@Ivy233 Ivy233 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
  1. Remove the immediate pushPendingEntity call from Notify(), so the expire timer no longer starts as soon as a notification is received
  2. Add a bubbleDisplayed signal to BubbleModel, emitted when a bubble is actually inserted into the display model
  3. Connect bubbleDisplayed in BubblePanel and forward it to the notification server through notificationDisplayed
  4. Add a timeout() accessor to NotifyEntity to read the client provided expire timeout
  5. Implement NotificationManager::notificationDisplayed to schedule the timeout only when the bubble is shown, avoiding premature expiry while notifications are still queued
  6. Forward notificationDisplayed via Qt::QueuedConnection in NotifyServerApplet so the pending timeout timer is started in the worker thread it belongs to
  7. Add unit tests for notificationDisplayed

Log: Defer the notification expire timer until the bubble is actually displayed on screen

Influence:

  1. Verify a notification disappears after the default 5 seconds when displayed normally
  2. Verify a notification no longer expires before it is shown when many notifications are queued
  3. Verify hovering over a bubble prevents it from expiring
  4. Run the notification server unit tests

fix: 通知气泡显示后才启动过期计时

  1. 移除 Notify() 中立即调用 pushPendingEntity 的逻辑,通知收到后不再 马上启动过期计时
  2. BubbleModel 中新增 bubbleDisplayed 信号,在气泡实际插入显示模型 时发出
  3. BubblePanel 中连接 bubbleDisplayed,通过 notificationDisplayed 转发给通知服务端
  4. NotifyEntity 新增 timeout() 访问器,用于读取客户端传入的过期时间
  5. 实现 NotificationManager::notificationDisplayed,仅在气泡显示时才调度 超时,避免通知在排队期间提前过期
  6. NotifyServerApplet 中通过 Qt::QueuedConnection 转发 notificationDisplayed,确保过期定时器在其所属的 worker 线程中启动
  7. notificationDisplayed 补充单元测试

Log: 将通知过期计时推迟到气泡真正显示之后

Influence:

  1. 验证正常显示的通知在默认 5 秒后消失
  2. 验证大量通知排队时,通知不会在显示前提前过期
  3. 验证鼠标悬停气泡时通知不会过期消失
  4. 运行通知服务端单元测试

PMS: BUG-372279

Summary by Sourcery

Defer starting notification expiry timers until bubbles are actually displayed on screen.

New Features:

  • Emit a BubbleModel::bubbleDisplayed signal when a notification bubble is inserted or replaced in the display model.
  • Expose NotifyEntity::timeout() to retrieve the client-provided expiry timeout.
  • Add a NotifyServerApplet::notificationDisplayed slot and forward display events to NotificationManager via queued invocation.

Bug Fixes:

  • Prevent notifications from expiring while still queued by scheduling expiry only after the bubble is shown.
  • Ensure notification expiry timers run in the correct worker thread so they fire reliably.

Enhancements:

  • Introduce NotificationManager::notificationDisplayed to start pending expiry timers based on actual display events instead of receipt time.

Tests:

  • Add unit tests covering NotifyServerApplet::notificationDisplayed, including edge-case IDs.

@deepin-ci-robot

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: Ivy233

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sourcery-ai

sourcery-ai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR defers starting the notification expiration timer until a bubble is actually inserted into the UI model, wiring a new bubbleDisplayed signal through BubblePanel to NotificationManager, which now schedules timeouts based on the stored client expire timeout only when notifications are displayed, with thread-safe forwarding from the applet and added unit tests.

Sequence diagram for deferred notification timeout start when bubble is displayed

sequenceDiagram
    participant BubbleModel
    participant BubblePanel
    participant NotifyServerApplet
    participant NotificationManager

    BubbleModel->>BubbleModel: insertBubble / replaceBubble
    BubbleModel-->>BubblePanel: bubbleDisplayed(id)
    BubblePanel->>NotifyServerApplet: notificationDisplayed(id)
    NotifyServerApplet->>NotificationManager: notificationDisplayed(id)
    NotificationManager->>NotificationManager: fetchEntity(id)
    NotificationManager->>NotificationManager: pushPendingEntity(entity, entity.timeout())
Loading

File-Level Changes

Change Details Files
Start notification expiration only when the bubble is displayed instead of on receipt.
  • Removed immediate scheduling of pending notification timeouts in Notify() based on hints expireTimeout.
  • Added a notificationDisplayed(qint64) slot in NotificationManager that fetches the entity, checks validity/urgency, reads its timeout(), and calls pushPendingEntity only for non-critical, expiring notifications.
  • Introduced a timeout() accessor on NotifyEntity to expose the stored expire timeout instead of passing it around separately.
panels/notification/server/notificationmanager.cpp
panels/notification/server/notificationmanager.h
panels/notification/common/notifyentity.cpp
panels/notification/common/notifyentity.h
Signal when a bubble is actually shown and propagate that to the notification server with correct threading semantics.
  • Added a bubbleDisplayed(qint64) signal to BubbleModel and emit it when inserting or replacing bubbles in the model.
  • Connected BubbleModel::bubbleDisplayed in BubblePanel to forward the ID to the notification server via notificationDisplayed using a direct connection.
  • Implemented NotifyServerApplet::notificationDisplayed to forward the call into NotificationManager::notificationDisplayed using Qt::QueuedConnection so the timeout QTimer starts on the worker thread.
panels/notification/bubble/bubblemodel.h
panels/notification/bubble/bubblemodel.cpp
panels/notification/bubble/bubblepanel.cpp
panels/notification/server/notifyserverapplet.h
panels/notification/server/notifyserverapplet.cpp
Add unit coverage for the new notificationDisplayed path to ensure robustness for various IDs.
  • Added a basic NotifyServerApplet test that calls notificationDisplayed with a valid ID to ensure no crashes.
  • Added edge-case tests that call notificationDisplayed with 0, -1, and max qint64, verifying the applet handles these IDs without throwing.
tests/panels/notification/server/notifyserverapplet_test.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@Ivy233
Ivy233 marked this pull request as ready for review August 6, 2026 06:59

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @Ivy233, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@Ivy233
Ivy233 force-pushed the fix/notification-expire-after-display branch from 430dd63 to fcf70d8 Compare August 6, 2026 11:52
if (interval <= 0)
return;

auto *timer = new QTimer(this);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这样每个通知都new 个超时的timer?

timer->setSingleShot(true);
timer->setInterval(interval);
connect(timer, &QTimer::timeout, this, [this, id, bubbleId = bubble->bubbleId()] {
m_timeoutTimers.remove(id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timer没delete?

}
}

void BubbleModel::setBlockedId(qint64 id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这样的话,进入了暂存区域的通知没有超时的机制了,

@Ivy233
Ivy233 force-pushed the fix/notification-expire-after-display branch 2 times, most recently from 0b0eb8c to 2ae625f Compare August 7, 2026 06:21
@Ivy233
Ivy233 requested a review from 18202781743 August 7, 2026 06:25
The notification expire timer previously ran in the notification server
(worker thread) and was started when the notification was received, so a
notification waiting in a long display queue could expire before its
bubble was shown. The timeout is now owned by the bubble frontend and
only starts once the bubble is actually displayed.

1. Add an ExpireTimer helper that tracks expire deadlines for many keys
with a single shared single-shot QTimer and a deadline map, so no QTimer
is allocated per notification and nothing leaks on expiry.
2. BubbleModel starts a per-bubble expire timer when a bubble is shown
(insert/replace) using the client expireTimeout (0: never expire, -1:
5000ms default, Critical urgency: never expire). On expiry it emits
bubbleExpired(id, bubbleId).
3. BubblePanel closes the expired bubble and calls
notificationClosed(id, bubbleId, Expired) on the server, which moves the
notification from the in-memory store to the center database and emits
NotificationStateChanged(Processed) and the DBus NotificationClosed
signal.
4. Move the hover block to the frontend: BubbleModel::setBlockedId
pauses/resumes the expire timer so hovering keeps the bubble on screen,
keeping the 1s grace after unhover and the replaced-bubble blocking
behavior.
5. Give the notification center staging model its own expire timer:
notifications shown in the staging area also time out, because the
bubble panel is disabled while the center window is open so the
bubble-side timers are not running.
6. Remove the server-side timeout bookkeeping: notificationDisplayed,
setBlockClosedId, pushPendingEntity, onHandingPendingEntities,
removePendingEntity and the pending-timeout QTimer.
7. Expose BubbleItem::timeout() and NotifyEntity::timeout()/urgency()
for the frontend timeout computation and unify the urgency lookup.
8. Remove the obsolete NotificationDisplayed/SetBlockClosedId tests.

Log: Fixed notifications expiring before their bubble was displayed.

Influence:
1. Send several notifications at once and verify each bubble stays for
the full expire timeout.
2. Verify critical notifications and expireTimeout 0 never close.
3. Hover a bubble and verify it does not expire, then closes 1s after
unhover.
4. Verify expired bubbles are moved to the notification center and the
DBus NotificationClosed(Expired) signal is emitted.
5. Open the notification center and verify staged notifications expire
after their timeout and leave the staging area.
6. Run the notification server unit tests.

fix: 将通知过期计时迁移到气泡前端

通知过期计时此前在通知服务器(工作线程)中运行,收到通知时即启动,导致
长显示队列中的通知可能在显示前就已过期。现将超时逻辑交由气泡前端持有,
气泡真正显示后才开始计时。

1. 新增 ExpireTimer 工具类:用一个共享的单次 QTimer 加 deadline 哈希
管理多个键的过期时间,不再为每个通知分配 QTimer,到期后也不会泄漏对象。
2. BubbleModel 在气泡显示(插入/替换)时根据客户端 expireTimeout 启动
过期计时(0:永不过期,-1:默认 5000ms,Critical 优先级:永不过期),
到期时发出 bubbleExpired(id, bubbleId)。
3. 到期后 BubblePanel 关闭气泡并调用服务器 notificationClosed(id,
bubbleId, Expired),将通知从内存存储迁移到通知中心数据库,并发送
NotificationStateChanged(Processed) 与 DBus NotificationClosed 信号。
4. 将悬停阻塞移到前端:BubbleModel::setBlockedId 暂停/恢复过期计时,
使悬停时气泡不关闭,并保留取消悬停后 1s 缓冲及替换气泡继续阻塞的行为。
5. 为通知中心暂存模型增加过期计时:暂存区展示的通知同样会超时,
因为中心窗口打开时气泡面板被禁用,气泡侧的计时器不会运行。
6. 移除服务端超时簿记:notificationDisplayed、setBlockClosedId、
pushPendingEntity、onHandingPendingEntities、removePendingEntity 以及
pending-timeout 定时器。
7. 为前端超时计算暴露 BubbleItem::timeout() 与 NotifyEntity::timeout()/
urgency(),并统一两处 urgency 取值方式。
8. 移除过时的 NotificationDisplayed/SetBlockClosedId 测试。

Log: 修复通知在气泡显示前就过期的问题。

Influence:
1. 一次性发送多条通知,验证每个气泡都能保持完整的过期时间。
2. 验证 Critical 通知与 expireTimeout 为 0 的通知永不过期。
3. 悬停气泡验证其不关闭,取消悬停 1s 后关闭。
4. 验证过期气泡进入通知中心,并发出 DBus NotificationClosed(Expired)。
5. 打开通知中心,验证暂存区的通知到期后超时并移出暂存区。
6. 运行通知服务器单元测试。

PMS: BUG-372279
@Ivy233
Ivy233 force-pushed the fix/notification-expire-after-display branch from 2ae625f to 5ef467e Compare August 7, 2026 08:07
@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

★ 总体评分:65分

■ 【总体评价】

代码实现了通知超时逻辑从服务端到视图层的下移重构,但存在导致无法编译的致命语法错误
逻辑重构思路清晰且健壮,但因 std::min_element 类型不匹配导致编译失败扣35分

■ 【详细分析】

  • 1.语法逻辑(存在致命错误)✕

expiretimer.cppExpireTimer::schedule() 函数中,m_deadlines 的类型为 QHash<qint64, qint64>,其迭代器解引用得到的类型是 QPair<qint64, qint64>。但传入 std::min_element 的 lambda 表达式参数被声明为 const qint64 &,这会导致类型不匹配,无法通过编译。
潜在问题:代码完全无法编译通过,阻塞整个模块的构建与测试
建议:将 lambda 参数类型修改为 const QHash<qint64, qint64>::value_type & 或使用 auto,并在 lambda 内部通过 .value() 获取超时时间戳进行比较

  • 2.代码质量(一般)✕

bubblepanel.cppBubblePanel::init() 函数中,处理 bubbleExpired 信号时,使用了字符串形式的 QMetaObject::invokeMethod(m_notificationServer, "notificationClosed", ...) 进行调用。由于 m_notificationServer 的类型在编译期是完全已知的,使用字符串调用丧失了编译期的类型检查,且在参数类型不匹配时仅会在运行时产生警告或静默失败。
潜在问题:重构后引入了不必要的运行时反射调用,降低了代码的可维护性和类型安全性
建议:直接通过 m_notificationServer->notificationClosed(id, bubbleId, NotifyEntity::Expired) 进行强类型调用

  • 3.代码性能(无性能问题)✓

ExpireTimer 使用单个 QTimer 驱动多个 key 的超时管理,schedule()onTimeout() 中对 QHash 的遍历复杂度为 O(N)。考虑到桌面通知场景下同时存在的气泡数量极少(通常不超过10个),O(N) 的开销完全可以忽略不计,整体设计高效且避免了为每个通知创建独立定时器的开销。
建议:无需优化

  • 4.代码安全(存在0个安全漏洞)✓

漏洞对比统计:新增漏洞 0 个,减少漏洞 0 个,持平 0 个
重构移除了服务端复杂的 QMultiHash 和竞态条件处理逻辑,下放至视图层后状态更加内聚,未引入命令注入、越界或权限绕过等安全风险,整体安全。
建议:保持当前安全设计

■ 【改进建议代码示例】

// expiretimer.cpp
void ExpireTimer::schedule()
{
    if (m_deadlines.isEmpty()) {
        m_timer->stop();
        return;
    }

    // 修复:使用正确的 value_type 解引用 QHash 迭代器,并通过 .value() 获取 deadline 进行比较
    auto it = std::min_element(m_deadlines.cbegin(), m_deadlines.cend(),
                               [](const QHash<qint64, qint64>::value_type &lhs, const QHash<qint64, qint64>::value_type &rhs) {
                                   return lhs.value() < rhs.value();
                               });
    const qint64 remaining = qMax<qint64>(0, it.value() - QDateTime::currentMSecsSinceEpoch());
    m_timer->start(static_cast<int>(remaining));
}

// bubblepanel.cpp
bool BubblePanel::init()
{
    // ... 前置代码保持不变 ...

    connect(m_bubbles, &BubbleModel::bubbleExpired, this, [this](qint64 id, uint bubbleId) {
        closeBubble(id);
        // 修复:移除字符串形式的 invokeMethod,改为直接进行强类型调用
        m_notificationServer->notificationClosed(id, bubbleId, NotifyEntity::Expired);
    });

    return true;
}

@deepin-ci-robot

Copy link
Copy Markdown

@Ivy233: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
github-pr-review-ci 5ef467e link true /test github-pr-review-ci

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here.


Q_EMIT NotificationStateChanged(entity.id(), entity.processedType());

bool critical = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个逻辑去掉了,那定时器没拿这个参数?


bool contains(qint64 key) const;
// Milliseconds left for key, or 0 when it is not tracked.
int remaining(qint64 key) const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

按操作来定义接口吧,不用搞这么通用的,然后让调用者去组合,
这里只有start,stop,clear吧,resume类似传递需要停止的entity,逻辑在内部封装,

* that key. All bookkeeping lives in hash maps, so no QTimer is allocated per
* key and nothing leaks when a key expires or is stopped.
*/
class ExpireTimer : public QObject

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

暂存区和通知横幅的是不是共用同一个定时器管理的呀,不然这里会不会一个通知有两个定时器在弄呀,

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants