Skip to content

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

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

Ivy233 wants to merge 2 commits into
linuxdeepin:masterfrom
Ivy233:fix/notification-expire-after-display

Conversation

@Ivy233

@Ivy233 Ivy233 commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor
  1. Move the pending-timeout machinery from NotificationManager to a frontend ExpireTimer singleton, keeping one shared QTimer and absolute deadlines in a single QMultiHash while removing all server-side timer state
  2. Start a countdown only when a notification is actually inserted into the bubble model or the visible staging model, so queued, hidden and overflow notifications no longer expire before being displayed
  3. Make ExpireTimer::push idempotent for the same entity (same id + cTime), so the bubble and the staging area share the first deadline instead of restarting it, and cancel the old bubble-slot countdown when a replacement arrives
  4. Move hover blocking into ExpireTimer::setBlockId with a short grace period, without spinning the shared timer at a zero interval
  5. Keep the server authoritative for the notification lifecycle: expiration is reported to NotificationManager::notificationClosed via a queued invocation, and countdowns are stopped from the server's NotificationStateChanged instead of frontend model removal; notificationClosed now reports a close only once per notification
  6. Derive effective timeouts from NotifyEntity (timeout 0 or Critical urgency never expires, -1 falls back to the 5000 ms default) and carry bubbleId in the expired signal
  7. Match staging-area replacements by bubble id (the key preserved across a replace), like BubbleModel::replaceBubbleIndex
  8. Remove the obsolete pending-timeout code from NotificationManager/NotifyServerApplet and its outdated tests
  9. Also fix taskbar icons not updating when the Icon field of the desktop file changes (forward dataChanged in DockGlobalElementModel, fix refresh of multi-mapped rows in RoleCombineModel)

Log: Start the notification expire countdown only after the notification is displayed

Influence:

  1. A displayed notification disappears after its expire timeout (default 5 seconds); queued notifications no longer expire while hidden
  2. Hovering a bubble keeps it on screen, and it lingers about 1 second after the hover ends
  3. The same notification shown in the bubble and the staging area shares one countdown and is not timed twice
  4. Run the notification server and taskmanager unit tests

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

  1. 将 NotificationManager 中的 pending-timeout 机制整体迁移到前端 ExpireTimer 单例:保留单个共享 QTimer 与按绝对截止时间组织的 QMultiHash,移除服务端全部定时器状态
  2. 仅在通知真正插入气泡模型或可见的暂存区模型时才启动倒计时,排队、隐藏和溢出的通知不再提前过期
  3. ExpireTimer::push 对同一通知(相同 id + cTime)幂等:气泡与暂存区共享首次截止时间、不重复计时;替换通知按 bubbleId 取消旧槽位倒计时后再启动新计时
  4. 悬停阻断逻辑下沉到 ExpireTimer::setBlockId,移开悬停后有短暂宽限期,共享 QTimer 不会以 0 间隔空转
  5. 生命周期仍由服务端统一管理:到期经 queued 调用转入 NotificationManager::notificationClosed,前端根据服务端 NotificationStateChanged 停止计时而非自行移除;notificationClosed 增加去重,避免同一通知重复上报关闭
  6. 有效超时时间由 NotifyEntity 推导(timeout 为 0 或 Critical 紧急级别永不过期,-1 回退默认 5000ms),expired 信号携带 bubbleId
  7. 暂存区替换匹配改用 bubbleId(替换时唯一不变的键),与 BubbleModel::replaceBubbleIndex 保持一致
  8. 删除 NotificationManager/NotifyServerApplet 中过时的 pending-timeout 代码及其旧测试
  9. 同时修复 desktop 文件 Icon 字段变化后任务栏图标不更新的问题(DockGlobalElementModel 转发 dataChanged,RoleCombineModel 修复多对一映射行的刷新)

Log: 通知显示后才启动过期计时

Influence:

  1. 气泡显示后默认 5 秒消失;大量通知排队时不再在显示前提前过期
  2. 鼠标悬停气泡时不消失,移开后停留约 1 秒
  3. 同一通知在横幅与暂存区共用同一倒计时,不会重复计时
  4. 运行通知服务端与任务管理器单元测试

PMS: BUG-372279

Summary by Sourcery

Start notification expiration only after display while coordinating shared countdowns with server-owned notification state.

Bug Fixes:

  • Start notification expiration only after notifications are displayed, preventing queued, hidden, and overflow notifications from expiring prematurely.
  • Preserve notification lifecycle ownership in the server while preventing duplicate close reporting and correctly stopping frontend countdowns after state changes.
  • Fix taskbar icons failing to refresh when desktop-file Icon data changes.

Enhancements:

  • Centralize displayed-notification expiration in a shared frontend timer with idempotent tracking, replacement handling, hover grace periods, and timeout rules based on notification urgency and configuration.
  • Match staging-area replacements by bubble ID and add coverage for expiration timing, deduplication, hover blocking, replacements, and non-expiring notifications.

Tests:

  • Add unit tests for the shared notification expiration timer and remove obsolete pending-timeout tests.

Chores:

  • Remove obsolete pending-timeout and hover-blocking machinery from the notification server and manager.

@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
Comment thread panels/notification/bubble/bubblemodel.cpp Outdated
Comment thread panels/notification/bubble/bubblemodel.cpp Outdated
Comment thread panels/notification/bubble/bubblemodel.cpp Outdated
@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
@Ivy233
Ivy233 force-pushed the fix/notification-expire-after-display branch from 2ae625f to 5ef467e Compare August 7, 2026 08:07

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.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

服务端定时器逻辑整体移除后,超时计算完全由前端 ExpireTimer 负责,ExpireTimer 内部通过 NotifyEntity::timeout()/urgency() 自行推导有效超时时间(-1 默认 5000ms,0/Critical 永不过期),因此无需再把 expireTimeout 参数单独传给定时器。


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,逻辑在内部封装,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已修改。

* 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.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

是共用的。ExpireTimer 是进程级单例,仅一个共享 QTimer,气泡与暂存区都通过它管理。同一通知 id 同时在气泡和暂存区显示时,ExpireTimer::push 发现 id 已在跟踪中会保持原截止时间而不重建倒计时,不会出现一个通知被两个定时器同时计时的现象。

@Ivy233
Ivy233 force-pushed the fix/notification-expire-after-display branch from 5ef467e to aa5a30c Compare August 10, 2026 03:27
@Ivy233
Ivy233 requested a review from 18202781743 August 10, 2026 05:11
@Ivy233
Ivy233 force-pushed the fix/notification-expire-after-display branch from aa5a30c to fadfda6 Compare August 11, 2026 09:25
return &expireTimer;
}

void ExpireTimer::start(qint64 key, const NotifyEntity &entity)

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.

这里里的key就是entity的id吧,直接从entity里获取就行了,这个key名称换成id吧,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已修改。

if (id == m_blockedId)
return;

if (m_blockedId != NotifyEntity::InvalidId)

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.

这个hover就blocked的逻辑可以放在ExpirreTimer里实现吧,不暴露resume和pause了,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已经把block逻辑迁移到expiretimer里面,并替代掉resume和pause。

// A bubble that has been pushed off the display (overflow) is no longer
// in the model, but its countdown still finishes and the notification
// should still be closed, so fall back to the persisted bubble id.
Q_EMIT bubbleExpired(id, DataAccessorProxy::instance()->fetchEntity(id).bubbleId());

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.

既然bubbleExpired需要bubbleId,那在ExpireTimer::expired里就直接添加这个参数就行了,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已按建议处理。ExpireTimer::expired 的签名就是 expired(qint64 id, uint bubbleId),到期时从实体读取 bubbleId 随信号一并发出,BubbleModel::bubbleExpired(id, bubbleId) 直接转发,无需自行拼接。

if (it == m_deadlines.cend())
return;

m_paused.insert(key, static_cast<int>(qMax<qint64>(0, it.value() - QDateTime::currentMSecsSinceEpoch())));

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.

pause的只会是一个吧,最起码目前可以只是一个,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已经替换成只有一个。

if (m_deadlines.contains(key) || m_paused.contains(key))
return;

m_deadlines.insert(key, QDateTime::currentMSecsSinceEpoch() + interval);

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.

这个还是解决不了在通知横幅定时器跑了一半的时候,再切到暂存区定时器又重新计时的问题呀,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已解决。ExpireTimer::remove 在气泡移出模型时挂起该 id 的倒计时(把绝对截止时间记入 m_retired),随后暂存区 NotifyStagingModel::push 调用 ExpireTimer::push 时从 m_retired 恢复原截止时间而不是重新计时;若暂停期间已过截止点,恢复时会按当前时间立即到期,不再从横幅跑了一半的地方重头开始。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

补充说明(方案有更新):最终实现没有引入 m_retired,改为更简单的幂等 push——ExpireTimer::push 检测到相同 id + cTime 已在跟踪时直接返回,保留首次(横幅显示时)写入的绝对截止时间。因此同一通知随后进入/切换到暂存区时复用原截止时间、不会重新计时;只有服务端关闭通知(NotificationStateChanged → ExpireTimer::remove)或替换通知(按 bubbleId 换槽)才会结束/重建计时。效果与之前描述的 m_retired 方案等价:横幅跑了一半切到暂存区,剩余时间照旧倒数,过期点不变。

return;

// Critical notifications must not disappear on their own.
if (reason == NotifyEntity::Expired && entity.urgency() == NotifyEntity::Critical)

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.

Expired的不会是Critical类型的吧,不需要这个判断吧,

Copy link
Copy Markdown
Contributor Author

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 from fadfda6 to e12655f Compare August 11, 2026 14:25
@Ivy233
Ivy233 requested a review from 18202781743 August 11, 2026 14:25
if (id == m_blockedId)
return;

m_blockedId = 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.

BubbleModel里不需要m_blockedId了吧,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已删除。悬停冻结逻辑整体下沉到 ExpireTimer::setBlockId(内部维护单一悬停 id),BubblePanel::setHoveredId 直接调用 ExpireTimer::instance()->setBlockId(id),BubbleModel 不再保存 m_blockedId。

// times out, close it and notify the server so it moves the notification
// from the in-memory store to the center database and emits the signals.
connect(m_bubbles, &BubbleModel::bubbleExpired, this, [this](qint64 id, uint bubbleId) {
closeBubble(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.

不需要这里close吧,它会被server发送过来的吧,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已去掉。到期时 BubblePanel 只向服务端发送 notificationClosed(id, bubbleId, Expired),服务端处理后发出 NotificationStateChanged,气泡经由 onNotificationStateChanged 的正常流程关闭,不再本地 closeBubble。

const auto replaceIndex = replaceBubbleIndex(bubble);
const auto oldBubble = m_bubbles[replaceIndex];

ExpireTimer::instance()->stop(oldBubble->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.

这个替换的逻辑,放在ExpireTimer里吧,只需要跟insert一样,push进去就行,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已迁移。replaceBubble 现在与 insertBubble 一样只调用 ExpireTimer::push;替换逻辑(取消同一气泡槽位旧通知的倒计时、悬停块转移给新通知)封装在 push 内部的 cancelReplacement 中统一处理。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已按建议修改:BubbleModel::replaceBubble 现在只调用 ExpireTimer::instance()->push(bubble->entity()),与 insertBubble 完全一致。替换语义全部下沉到 ExpireTimer::push 内部:isReplace() 的实体会按 bubbleId 取消旧槽位的倒计时(悬停 block id 同步转移到新 id)后,再按新实体的 timeout/urgency 启动新计时。

// Absolute deadlines of ids stopped by stop(); restored by start() so a
// context switch (bubble <-> staging) resumes the countdown.
QHash<qint64, qint64> m_retired;
QHash<qint64, uint> m_bubbleIds;

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.

不需要m_bubbleIds和m_deadlines两个吧,放一个QHash<qint64, NotifyEntity>是不是就可以了,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已合并。m_bubbleIds 已删除,m_deadlines 改为 QHash<qint64, Deadline>,Deadline 同时保存实体与绝对截止时间(point),到期时所需的 bubbleId 直接从实体读取。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已修改:现在只保留一个容器 QMultiHash<qint64, NotifyEntity> m_pendingEntities,key 为该通知的绝对截止时间(共享 QTimer 按最近的截止时间调度),id/bubbleId/cTime 都由 value 中的 entity 携带,不再有 m_bubbleIds/m_deadlines 两个结构。

@Ivy233
Ivy233 force-pushed the fix/notification-expire-after-display branch from e12655f to 0c427f4 Compare August 12, 2026 10:34
@Ivy233
Ivy233 requested a review from 18202781743 August 13, 2026 04:06
connect(NotifyAccessor::instance(), &NotifyAccessor::stagingEntityClosed, this, &NotifyStagingModel::onEntityClosed);
connect(NotifySetting::instance(), &NotifySetting::contentRowCountChanged, this, &NotifyStagingModel::updateContentRowCount);

connect(ExpireTimer::instance(), &ExpireTimer::expired, this, [this](qint64 id, uint bubbleId) {

@18202781743 18202781743 Aug 13, 2026 •

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.

一个通知发送了expired后,会不会被通知横幅和暂存区都close一次呀,
是不是可以直接由ExpireTimer调用server的close呀,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已处理。

  1. 关于"会不会 close 两次":确实存在这个竞态,已修复。之前暂存区在 ExpireTimer::expired 时本地 remove + 补位,而服务端关闭是异步投递到 worker 线程的,导致刚过期的通知在补位时仍被当作 NotProcessed 重新插入并重新计时,一个倒计时周期后再次触发 notificationClosed(被幂等检查挡住,但会多打一条日志、多一次空跑)。现在暂存区不再响应 expired,只在服务端 stagingEntityClosed 回环时移除行,此时实体已标记 Processed,不会被补位重新选中。

  2. 关于"由 ExpireTimer 直接调用 server 的 close":采用等价且不破坏分层的做法——ExpireTimer 保持纯前端、不感知服务端,由 NotifyServerApplet 连接 ExpireTimer::expired,通过 Qt::QueuedConnection 统一转发给 NotificationManager::notificationClosed,作为过期关闭的唯一入口;notificationClosed 保留幂等检查兜底。横幅和暂存区各自只移除自己的视图,服务端每个通知只处理一次。

@Ivy233
Ivy233 force-pushed the fix/notification-expire-after-display branch 3 times, most recently from a5757b6 to c7b598c Compare August 14, 2026 07:28
@Ivy233
Ivy233 requested a review from 18202781743 August 14, 2026 07:30

const auto entity = notifyById(id);
if (entity.isValid())
ExpireTimer::instance()->remove(entity);

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.

在服务端调用这个比较合适吧,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

最新版本已按建议调整:NotifyStagingModel::remove 已不再直接调用 ExpireTimer::remove。通知生命周期统一由服务端管理——关闭/归档时服务端发出 NotificationStateChanged(Processed/Removed),由 NotifyServerApplet 统一调用 ExpireTimer::remove(id) 停止倒计时;前端各视图只负责移除自己的行,删除逻辑不再散落在前端模型里。

顺带说明一下新增的 m_opened 标志的用途:NotifyStagingModel 在通知中心面板隐藏时依然存活,stagingEntityReceived 随时会触发 doEntityReceived。若面板隐藏期间不拦截,通知会立即 push() 进入 ExpireTimer 启动倒计时,还没显示就过期,违背"实际显示后才开始计时"的原则。m_opened 由 open()/close()(Panel.visibleChanged 驱动)维护,作为"暂存区是否可见"的标志:doEntityReceived 中 if (!m_opened) return; 确保只有面板可见、暂存区实际显示时才启动倒计时;open() 中的 if (m_opened) return; 作为重复进入保护。

@Ivy233
Ivy233 force-pushed the fix/notification-expire-after-display branch 2 times, most recently from 0ea9c62 to 24d7693 Compare August 17, 2026 05:33
@Ivy233
Ivy233 requested a review from 18202781743 August 17, 2026 05:39
qDebug(notifyLog) << "Receive entity" << id;

// The model exists while the panel is hidden. A hidden staging item has not
// been displayed and must not start the expiration countdown.

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 from 24d7693 to 8c366c9 Compare August 17, 2026 07:43
@Ivy233

Ivy233 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

回复 auto review 提出的多线程竞态问题:经逐一核实,ExpireTimer 的所有调用点均在主线程,不存在跨线程访问 m_pendingEntities 的路径,该问题为误报,无需加锁。

  1. BubbleModel::insertBubble/replaceBubble、BubblePanel::setHoveredId、NotifyStagingModel::push/replace 均为 UI 侧调用,运行在主线程;
  2. NotifyServerApplet::init() 中连接 NotificationStateChanged 的 lambda(内部调用 ExpireTimer::remove)使用 Qt::QueuedConnection,接收者为 applet 本身(主线程创建),因此该 lambda 也在主线程执行(panels/notification/server/notifyserverapplet.cpp:65);
  3. 唯一的跨线程调用是 expired → m_manager->notificationClosed,通过 Qt::QueuedConnection 投递到 worker 线程(notifyserverapplet.cpp:74-77),该路径不触碰 ExpireTimer 内部状态;
  4. 单例首次构造发生在 init()(主线程),QTimer 与哈希表同属主线程,符合 QTimer 的线程亲和性要求。

另外,示例中在持锁状态下调用 m_timer->start() 并不能解决跨线程问题——QTimer 只能在其所属线程启动,真正跨线程时需要 QMetaObject::invokeMethod 投递;本实现已确保全部访问在主线程,故无此问题。

@Ivy233

Ivy233 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

/test github-pr-review-ci

@Ivy233
Ivy233 requested a review from 18202781743 August 17, 2026 09:43
@deepin-bot

deepin-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

TAG Bot

New tag: 2.0.53
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #1702

Comment on lines +161 to +162
const auto entity = m_persistence->fetchEntity(id);
// A notification can be tracked by more than one expire timer (the bubble

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.

不会被多个计时吧,超时了也是从这里更新状态,发送NotificationClosed的吧,

@deepin-bot

deepin-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

TAG Bot

New tag: 2.0.54
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #1733

@deepin-bot

deepin-bot Bot commented Sep 22, 2026

Copy link
Copy Markdown

TAG Bot

New tag: 2.0.55
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #1752

@deepin-bot

deepin-bot Bot commented Sep 23, 2026

Copy link
Copy Markdown

TAG Bot

New tag: 2.0.56
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #1753

@Ivy233
Ivy233 requested a review from 18202781743 September 24, 2026 09:19
1. Move the pending-timeout machinery from NotificationManager to the frontend
   ExpireTimer singleton, preserving one shared QTimer and absolute deadlines in
   a QMultiHash while removing the server-side timer state
2. Start a countdown only when a notification is inserted into the bubble model
   or the visible staging model, so queued, hidden and overflow notifications do
   not expire before they are displayed
3. Make push idempotent for the same entity so the bubble and staging views share
   the first deadline, and replace the old bubble-slot countdown when a new
   entity replaces it
4. Keep hover blocking inside ExpireTimer and give an unblocked notification a
   short grace period without spinning the timer at a zero interval
5. Keep the server authoritative for notification lifecycle: forward expiration
   through NotifyServerApplet with a queued call, and stop countdowns from server
   state changes instead of frontend model removal
6. Derive effective timeouts from NotifyEntity (0/Critical never expires and -1
   uses the 5000 ms default) and carry bubbleId in the expiration signal
7. Remove the staging expiry race by waiting for the server close round trip
   before refilling rows; hidden-panel updates remain gated by NotifyAccessor
8. Remove the obsolete NotificationManager pending-timeout code and its applet
   tests
9. Match staging notification replacements by bubble id (the key preserved
   across a replace), exactly like BubbleModel::replaceBubbleIndex: a replacement
   may carry a fresh entity id (marked-processed then re-added) or the original
   entity id (replaceEntity), so matching on the entity id left the old staging
   row stale and inserted a duplicate instead of updating in place
10. Update the SPDX copyright year of notifystagingmodel.h to 2024-2026

Log: Start notification expiration after actual display and keep lifecycle cleanup
owned by the server

Influence:
1. Verify normal notifications disappear after their configured/default timeout
2. Verify queued and hidden notifications do not expire before being displayed
3. Verify opening the notification center starts only visible staging countdowns
4. Verify hovering prevents expiration and leaves a short grace period on exit
5. Verify replacement and shared bubble/staging notifications keep one countdown
6. Verify close, invoke and expiry paths stop countdowns through server state
7. Verify a replaced staging notification updates in place instead of leaving a
   stale row and inserting a duplicate

fix: 通知实际显示后才开始过期计时

1. 将待超时机制从 NotificationManager 迁移到前端 ExpireTimer 单例,保留一个
   共享 QTimer,并以 QMultiHash 保存绝对截止时间,同时移除服务端定时器状态
2. 仅在通知插入横幅模型或可见暂存区模型时启动倒计时,排队、隐藏及未展示的
   重叠通知不会在实际显示前过期
3. 同一实体重复 push 时保持首次截止时间,使横幅与暂存区共享一个倒计时;新实体
   替换通知时则移除同一气泡槽位的旧倒计时
4. 将悬停阻塞封装在 ExpireTimer 内,解除阻塞后保留短暂宽限期,并避免定时器以
   零间隔空转
5. 由服务端统一决定通知生命周期:NotifyServerApplet 通过队列调用转发过期关闭,
   并根据服务端状态变化停止倒计时,不再由前端模型移除操作清理
6. 由 NotifyEntity 推导有效超时(0/Critical 永不过期,-1 使用默认 5000ms),
   并在过期信号中携带 bubbleId
7. 暂存区等待服务端关闭回环后再补位,避免过期通知被重新插入;面板隐藏期间的
   更新继续由 NotifyAccessor 统一屏蔽
8. 移除 NotificationManager 中废弃的待超时代码及对应 applet 测试
9. 暂存区通知替换改为按 bubbleId 匹配(与 BubbleModel::replaceBubbleIndex
   一致):替换可能携带全新实体 id(旧实体标记已处理后再新增)或原实体 id
   (replaceEntity),按实体 id 匹配会导致旧行残留并插入重复行,无法原地更新
10. 更新 notifystagingmodel.h 的 SPDX 版权年份为 2024-2026

Log: 通知实际显示后开始过期计时,并由服务端统一管理生命周期清理

Influence:
1. 验证普通通知按配置或默认超时时间消失
2. 验证排队及隐藏通知不会在显示前过期
3. 验证打开通知中心时仅启动实际可见暂存通知的倒计时
4. 验证悬停阻止通知过期,移开后保留短暂宽限期
5. 验证替换通知以及横幅/暂存区共享通知只保留一个倒计时
6. 验证关闭、调用动作及过期路径均通过服务端状态停止倒计时
7. 验证被替换的暂存区通知原地更新,而非残留旧行并插入重复行

PMS: BUG-372279
fix: 修复 actionInvoked 跨线程竞态并补充 ExpireTimer 单元测试

1. Changed Qt::DirectConnection to Qt::QueuedConnection in both
   actionInvoked overloads to eliminate cross-thread race conditions,
   consistent with the already-fixed notificationClosed.
   将 actionInvoked 两个重载的 Qt::DirectConnection 改为
   Qt::QueuedConnection,消除跨线程竞态,与已修复的
   notificationClosed 保持一致。

2. Restored private: access modifier for member variables in
   bubblemodel.h that was accidentally removed.
   恢复 bubblemodel.h 中被误移除的成员变量 private: 访问修饰符。

3. Added ExpireTimer unit tests covering expiration timing, idempotent
   push, hover blocking, replacement cancellation, and critical urgency
   non-expiration.
   新增 ExpireTimer 单元测试,覆盖过期计时、幂等 push、悬停阻断、
   替换取消、Critical 紧急度不过期等场景。

PMS: BUG-372279
Log: 修复通知 actionInvoked 跨线程竞态,恢复 private 访问修饰符,补充 ExpireTimer 单元测试
Influence: 通知中心 actionInvoked 调用安全性提升,ExpireTimer 测试覆盖率提升
@Ivy233
Ivy233 force-pushed the fix/notification-expire-after-display branch from 6e0e511 to 61ccf38 Compare September 24, 2026 10:36
@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

AI 代码审查报告

项目: linuxdeepin/dde-shell
PR: #1691
标题: fix: start notification expire timer only after bubble is displayed
作者: Ivy233
分支: fix/notification-expire-after-display → master
审查时间: 2026-09-24 18:36:00
分析模式: 全量分析(GitHub PR)


总体评价

总分: 100/100
风险等级: None
评价: 代码审查通过

本次 PR 将通知过期计时机制从服务端 NotificationManager 迁移到前端 ExpireTimer 单例,确保通知仅在实际显示后才开始倒计时。代码质量优秀,设计思路清晰,线程安全处理得当,同时修复了跨线程竞态和暂存区替换匹配等多个缺陷,并补充了完整的单元测试。


漏洞统计

指标 数量
当前漏洞总数 0
新增漏洞 0
修复漏洞 0
持平漏洞 0

漏洞对比统计:新增漏洞 0 个,减少漏洞 0 个,持平 0 个


四维度评分

维度1:语法逻辑(25/25)✓

语法正确,逻辑清晰

评估要点:

  1. 新增 ExpireTimer 类的 push()、remove()、setBlockId()、onTimeout() 方法逻辑完整正确,幂等性检查(id + cTime)、替换取消旧倒计时(bubbleId 匹配)、悬停阻塞与宽限期机制均实现正确
  2. NotifyStagingModel::replace() 修复了原有缺陷:旧代码 item->id() == entity.bubbleId() 混淆了 entity id 和 bubble id,新代码通过 rowByBubbleId() 正确按 bubble id 匹配,与 BubbleModel::replaceBubbleIndex 保持一致
  3. NotificationManager::notificationClosed() 新增 processedType != NotProcessed 守卫检查,有效防止用户关闭与定时器过期并发导致的重复关闭
  4. NotifyServerApplet 中 actionInvoked 和 notificationClosed 从 Qt::DirectConnection 改为 Qt::QueuedConnection,正确消除了跨线程竞态
  5. effectiveTimeout() 函数正确处理了三种超时场景:Critical 紧急度/timeout=0 返回 0(永不过期),timeout=-1 返回默认 5000ms,其他返回原始值
  6. 所有文件编译无错误,CMakeLists.txt 正确添加了新增的 expiretimer.h/cpp 文件和测试目录

无编译错误,无内存泄漏(Qt 父子对象所有权管理正确),无空指针解引用,边界条件处理完善。


维度2:代码质量(25/25)✓

代码结构清晰,注释完整

评估要点:

  1. 注释完整性(5/5): expiretimer.h 包含详细的类级注释,说明设计意图(进程级单例、共享 QTimer、绝对截止时间、幂等 push、悬停阻塞);expiretimer.cpp 中每个非显而易见的决策都有内联注释;notifystagingmodel.cpp 构造函数中的注释清楚解释了为何不直接监听 expired 信号;notifyserverapplet.cpp 中的注释说明了服务端作为生命周期唯一所有者的设计决策
  2. 代码重复(5/5): ExpireTimer 集中化了原先散布在 NotificationManager 中的超时逻辑,消除了重复代码;effectiveTimeout() 辅助函数避免了超时计算的重复;rowByBubbleId() 新辅助函数避免了替换匹配逻辑的重复
  3. 结构合理性(5/5): 函数长度合理(最长 onTimeout() 约 45 行);关注点分离良好——ExpireTimer 负责计时、NotificationManager 负责生命周期、模型负责显示;单例模式适合进程级共享定时器场景
  4. 调试信息清理(5/5): 无残留调试代码;qDebug/qWarning 语句用于生产日志,仅记录 entity id、appName 等非敏感信息,内容合理

维度3:代码性能(20/20)✓

性能良好,资源使用合理

评估要点:

  1. 定时器效率: 使用单个共享 QTimer(single-shot)驱动所有通知的过期检查,避免了多定时器并发的开销。定时器在最近截止时间触发,触发后重新计算下一个最近截止时间并重启
  2. 算法复杂度: push()、remove()、setBlockId() 均为 O(n) 线性扫描,onTimeout() 中 std::sort 为 O(n log n)。对于通知系统,n 通常极小(个位数),性能影响可忽略
  3. 资源管理: 使用 const 引用传递参数,避免不必要的拷贝;notifystagingmodel.cpp 的 open() 方法优化为先取 const 引用再传递给构造函数和 push(),减少了一次拷贝
  4. 悬停轮询: 被阻塞的通知在截止时间到期后以 1000ms 宽限期重新插入,形成低频轮询而非零间隔空转,设计合理
  5. 无频繁系统调用,无内存爆炸风险,无不必要的重复计算

维度4:代码安全(30/30)✓

存在0个安全漏洞

评估要点:

  1. 线程安全: actionInvoked 和 notificationClosed 从 DirectConnection 改为 QueuedConnection,消除了跨线程直接调用导致的竞态条件;ExpireTimer 的 expired 信号通过 QueuedConnection 转发到工作线程的 NotificationManager;NotificationStateChanged 信号通过 QueuedConnection 回调 ExpireTimer::remove()
  2. 竞态条件防护: notificationClosed() 新增 processedType != NotProcessed 检查,防止用户关闭与定时器过期并发导致的重复处理。注释详细解释了三种调用路径(定时器超时、用户关闭、通知中心关闭)的竞态场景
  3. 无命令注入/SQL注入/路径遍历: 代码不涉及外部命令执行、SQL 查询或文件路径操作
  4. 无敏感信息泄露: 日志仅记录 entity id 和 appName,无密码、密钥等敏感信息
  5. 无缓冲区溢出: 使用 Qt 容器(QMultiHash、QList),自动管理内存
  6. 无硬编码密钥: 代码中无任何硬编码的认证信息或密钥
  7. 整数溢出防护: static_cast<int>(qMax<qint64>(0, ...)) 理论上在截止时间超过 24 天时可能截断,但通知超时通常为秒到分钟级别,不构成实际风险

改进建议

本次代码审查未发现需要修复的问题。以下为可选的优化建议:

  1. onTimeout() 中 std::sort 可替换为 std::min_element 以从 O(n log n) 优化为 O(n),但对小规模数据影响可忽略
  2. ExpireTimer 作为单例,在单元测试中状态共享。可在测试 TearDown 中增加清理 m_pendingEntities 的逻辑,提升测试隔离性(当前测试通过不同 entity id 规避了此问题,实际运行正常)

审查检查清单

  • 已按四维度标准完成代码分析(语法逻辑、代码质量、代码性能、代码安全)
  • 代码安全维度第一行包含"存在0个安全漏洞"
  • 安全漏洞已按等级分类(无漏洞)
  • 包含漏洞对比统计行(新增/减少/持平均为0)
  • 每个维度标记✓/✕与评价词一致
  • 审查结论考虑了 commit message 的目的(修复通知过期计时、迁移超时机制到前端、修复跨线程竞态)
  • 问题定位准确,行号和函数名正确(无问题需定位)
  • 环境变量使用正确,文件路径无误

关联信息

  • PMS: BUG-372279
  • Commit 1: e2e347d - fix: expire notification countdown only after it is displayed
  • Commit 2: 61ccf38 - fix: fix actionInvoked cross-thread race and add ExpireTimer unit tests
  • 修改文件数: 20
  • 新增文件: expiretimer.cpp, expiretimer.h, expiretimer_test.cpp, tests/panels/notification/common/CMakeLists.txt

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