feat(update): 新增版本更新系统通知
This commit is contained in:
1 parent
3e4448e3e5
commit
c48c50fca3
16 files changed
+218
-5
No files matched your search
@@ -190,6 +190,8 @@ git push
|
||||
- `Packages/XStatsKit/Sources/XStatsUI/Rest`:本机休息计时、幕布、Mini HUD 与合成声音。
|
||||
- `Packages/XStatsKit/Tests`:Swift 单元测试。
|
||||
|
||||
更新提醒:通知设置中的“新版本更新”默认开启,沿用自动更新检查调度;后台发现未跳过的新版本时发送系统通知,不主动打开升级窗口。“运行时静默”与“从不”继续保持静默,手动检查仍显示更新窗口。仅在通知成功提交后记录最近提醒版本,跨重启去重;权限拒绝或发送失败不消耗提醒机会。点击通知打开更新窗口,重启后尚无清单时重新检查。通知偏好参与设置备份,提醒记录仅留在本机。
|
||||
|
||||
菜单栏绘图:每个状态项仅保存上一轮绘图读数,相关数值未变化时复用图像;历史风格继续比较历史数据,设置、语言、外观与布局变化必须使缓存失效。悬停提示独立更新。设置页实时预览在主窗口关闭或最小化时停止观察采样数据,快照渲染不受影响。验证需包含后台 CPU 对照,以及风格、充电状态和历史曲线的刷新边界。
|
||||
|
||||
硬件能力:风扇相关入口仅在成功读取到大于 0 的风扇数量后显示;尚未采样、数量未知或明确为 0 时隐藏。未知不等同于无风扇,仍沿既有需求采样重试;已确认的数量保留至本次运行结束,短暂读取失败与转速为 0 不导致入口消失。能力过滤不改写用户保存的展示偏好。
|
||||
|
||||
@@ -1608,3 +1608,6 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依
|
||||
续 متابعة
|
||||
GitHub 项目 مشروع GitHub
|
||||
赞助 XStats ادعم XStats
|
||||
新版本更新 تحديثات الإصدارات الجديدة
|
||||
后台发现新版本时通知,每个版本仅提醒一次 الإشعار عند العثور على تحديث في الخلفية، مرة واحدة لكل إصدار
|
||||
点击查看更新内容并安装。 انقر للاطلاع على المستجدات وتثبيت التحديث.
|
||||
|
@@ -1608,3 +1608,6 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依
|
||||
续 Forts.
|
||||
GitHub 项目 GitHub-Projekt
|
||||
赞助 XStats XStats unterstützen
|
||||
新版本更新 Neue Versionen
|
||||
后台发现新版本时通知,每个版本仅提醒一次 Bei im Hintergrund gefundenen Updates einmal pro Version benachrichtigen
|
||||
点击查看更新内容并安装。 Klicken, um Neuerungen anzusehen und das Update zu installieren.
|
||||
|
@@ -1608,3 +1608,6 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依
|
||||
续 cont.
|
||||
GitHub 项目 Proyecto en GitHub
|
||||
赞助 XStats Apoyar XStats
|
||||
新版本更新 Nuevas versiones
|
||||
后台发现新版本时通知,每个版本仅提醒一次 Notificar una vez por versión al detectar actualizaciones en segundo plano
|
||||
点击查看更新内容并安装。 Haz clic para ver las novedades e instalar la actualización.
|
||||
|
@@ -1608,3 +1608,6 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依
|
||||
续 suite
|
||||
GitHub 项目 Projet GitHub
|
||||
赞助 XStats Soutenir XStats
|
||||
新版本更新 Nouvelles versions
|
||||
后台发现新版本时通知,每个版本仅提醒一次 Notifier une fois par version lorsqu’une mise à jour est détectée en arrière-plan
|
||||
点击查看更新内容并安装。 Cliquez pour voir les nouveautés et installer la mise à jour.
|
||||
|
@@ -1608,3 +1608,6 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依
|
||||
续 続き
|
||||
GitHub 项目 GitHub プロジェクト
|
||||
赞助 XStats XStats を支援
|
||||
新版本更新 新しいバージョン
|
||||
后台发现新版本时通知,每个版本仅提醒一次 バックグラウンドで更新を検出したとき、バージョンごとに一度だけ通知
|
||||
点击查看更新内容并安装。 クリックして更新内容を確認し、インストールします。
|
||||
|
@@ -1608,3 +1608,6 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依
|
||||
续 계속
|
||||
GitHub 项目 GitHub 프로젝트
|
||||
赞助 XStats XStats 후원
|
||||
新版本更新 새 버전 업데이트
|
||||
后台发现新版本时通知,每个版本仅提醒一次 백그라운드에서 업데이트를 찾으면 버전마다 한 번만 알림
|
||||
点击查看更新内容并安装。 클릭하여 업데이트 내용을 확인하고 설치하세요.
|
||||
|
@@ -1609,3 +1609,6 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依
|
||||
续 續
|
||||
GitHub 项目 GitHub 專案
|
||||
赞助 XStats 贊助 XStats
|
||||
新版本更新 新版本更新
|
||||
后台发现新版本时通知,每个版本仅提醒一次 背景發現新版本時通知,每個版本僅提醒一次
|
||||
点击查看更新内容并安装。 點擊查看更新內容並安裝。
|
||||
|
@@ -188,6 +188,9 @@ CPU 温度达到 {},已恢复系统自动控制 CPU reached {}; fans returned
|
||||
CPU 温度过高 CPU is too hot
|
||||
CPU 负载偏高 CPU load is high
|
||||
CPU 负载很高 CPU load is very high
|
||||
新版本更新 New version updates
|
||||
后台发现新版本时通知,每个版本仅提醒一次 Notify when a background check finds an update, once per version
|
||||
点击查看更新内容并安装。 Click to see what’s new and install.
|
||||
CPU 过热 CPU Overheating
|
||||
CPU:{}(以单核满载为 100%) CPU: {} (100% = one full core)
|
||||
Cloudflare 接入节点(机场代码) Cloudflare edge location (airport code)
|
||||
|
||||
@@ -266,6 +266,18 @@ public final class AppController: NSObject, NSApplicationDelegate {
|
||||
self?.calendarMenuBar.dismiss()
|
||||
self?.updateWindow.show()
|
||||
}
|
||||
model.updates.onUpdateAvailable = { [weak self] version in
|
||||
await self?.model.alerts.notifyUpdate(version: version) ?? false
|
||||
}
|
||||
model.alerts.openUpdates = { [weak self] in
|
||||
guard let self else { return }
|
||||
if self.model.updates.release != nil {
|
||||
self.model.updates.onPrompt()
|
||||
} else {
|
||||
// 通知可跨应用重启保留;重新检查后展示当前有效的更新信息。
|
||||
self.checkForUpdatesFromMenu()
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
|
||||
self?.model.updates.checkIfNeeded()
|
||||
}
|
||||
|
||||
@@ -162,8 +162,16 @@ struct NotificationSettings: View {
|
||||
}
|
||||
|
||||
SettingsGroup(caption: tr("发生这些状况时发送系统通知,点通知打开对应页面")) {
|
||||
ForEach(Array(AlertKind.allCases.enumerated()), id: \.element) { index, kind in
|
||||
GroupRow(showsDivider: index > 0) {
|
||||
GroupRow(showsDivider: false) {
|
||||
SettingRow(title: tr("新版本更新"), subtitle: tr("后台发现新版本时通知,每个版本仅提醒一次"), icon: "arrow.down.circle") {
|
||||
DSToggle(isOn: Binding(get: { settings.notifyUpdates }, set: { enabled in
|
||||
settings.notifyUpdates = enabled
|
||||
if enabled, alerts.authorization == .unknown { alerts.requestAuthorization() }
|
||||
}), label: tr("新版本更新"))
|
||||
}
|
||||
}
|
||||
ForEach(AlertKind.allCases) { kind in
|
||||
GroupRow {
|
||||
SettingRow(title: kind.title, subtitle: kind.detail, icon: kind.symbol) {
|
||||
DSToggle(isOn: Binding(get: { settings.enabledAlerts.contains(kind) },
|
||||
set: { on in
|
||||
|
||||
@@ -114,6 +114,7 @@ public final class AlertController: NSObject {
|
||||
public private(set) var authorization: Authorization = .unknown
|
||||
|
||||
@ObservationIgnored var openTab: (PanelTab) -> Void = { _ in }
|
||||
@ObservationIgnored var openUpdates: () -> Void = {}
|
||||
@ObservationIgnored private let settings: AppSettings
|
||||
@ObservationIgnored private let defaults: UserDefaults
|
||||
@ObservationIgnored private var trackers: [AlertKind: AlertTracker] = [:]
|
||||
@@ -271,6 +272,37 @@ public final class AlertController: NSObject {
|
||||
|
||||
// MARK: 通知
|
||||
|
||||
/// 默认开启的更新提醒在真正发现版本时才请求权限,不新增轮询或启动时弹窗。
|
||||
func notifyUpdate(version: String) async -> Bool {
|
||||
guard settings.notifyUpdates, let center else { return false }
|
||||
do {
|
||||
let status = await center.notificationSettings().authorizationStatus
|
||||
let granted: Bool
|
||||
if status == .notDetermined {
|
||||
granted = try await center.requestAuthorization(options: [.alert, .sound])
|
||||
} else {
|
||||
granted = status == .authorized || status == .provisional
|
||||
}
|
||||
authorization = granted ? .allowed : .denied
|
||||
// 等待系统授权期间用户可能关闭开关。
|
||||
guard granted, settings.notifyUpdates else { return false }
|
||||
try await center.add(Self.updateNotification(version: version))
|
||||
return true
|
||||
} catch {
|
||||
Log.update.error("更新通知发送失败:\(error.localizedDescription, privacy: .public)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func updateNotification(version: String) -> UNNotificationRequest {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = tr("发现新版本 \(version)")
|
||||
content.body = tr("点击查看更新内容并安装。")
|
||||
content.sound = .default
|
||||
content.userInfo = ["updates": true]
|
||||
return UNNotificationRequest(identifier: "xstats.updateAvailable", content: content, trigger: nil)
|
||||
}
|
||||
|
||||
func refreshAuthorization() {
|
||||
guard let center else { return }
|
||||
center.getNotificationSettings { [weak self] settings in
|
||||
@@ -337,7 +369,12 @@ extension AlertController: UNUserNotificationCenterDelegate {
|
||||
nonisolated public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
let tab = (response.notification.request.content.userInfo["tab"] as? String).flatMap(PanelTab.init(rawValue:))
|
||||
let updates = response.notification.request.content.userInfo["updates"] as? Bool == true
|
||||
completionHandler()
|
||||
if updates {
|
||||
Task { @MainActor [weak self] in self?.openUpdates() }
|
||||
return
|
||||
}
|
||||
guard let tab else { return }
|
||||
Task { @MainActor [weak self] in self?.openTab(tab) }
|
||||
}
|
||||
|
||||
@@ -603,6 +603,10 @@ public final class AppSettings {
|
||||
public var publicIPLookup: Bool {
|
||||
didSet { defaults.set(publicIPLookup, forKey: Keys.publicIPLookup) }
|
||||
}
|
||||
/// 自动检查发现新版本时发送系统通知,默认开启。
|
||||
public var notifyUpdates: Bool {
|
||||
didSet { defaults.set(notifyUpdates, forKey: Keys.notifyUpdates) }
|
||||
}
|
||||
/// 打开了系统通知的状况
|
||||
public var enabledAlerts: Set<AlertKind> {
|
||||
didSet { defaults.set(enabledAlerts.map(\.rawValue).sorted(), forKey: Keys.enabledAlerts) }
|
||||
@@ -775,6 +779,7 @@ public final class AppSettings {
|
||||
hotKeys = Dictionary(uniqueKeysWithValues: storedHotKeys.compactMap { key, value in
|
||||
HotKeyAction(rawValue: key).map { ($0, value) }
|
||||
})
|
||||
notifyUpdates = defaults.object(forKey: Keys.notifyUpdates) as? Bool ?? true
|
||||
enabledAlerts = Set(defaults.stringArray(forKey: Keys.enabledAlerts)?.compactMap(AlertKind.init(rawValue:)) ?? [])
|
||||
alertCPUTemperature = Self.alertTemperatureOptions.contains(defaults.integer(forKey: Keys.alertCPUTemperature))
|
||||
? defaults.integer(forKey: Keys.alertCPUTemperature) : 95
|
||||
@@ -872,6 +877,7 @@ public final class AppSettings {
|
||||
static let hotKeys = "hotKeys"
|
||||
static let language = "language"
|
||||
static let enabledAlerts = "enabledAlerts"
|
||||
static let notifyUpdates = "notifyUpdates"
|
||||
static let alertCPUTemperature = "alertCPUTemperature"
|
||||
static let alertCPULoad = "alertCPULoad"
|
||||
static let cpuChartSeconds = "cpuChartSeconds"
|
||||
|
||||
@@ -49,6 +49,7 @@ public struct SettingsDocument: Codable, Equatable, Sendable {
|
||||
public var probeTarget: String?
|
||||
public var publicIPLookup: Bool?
|
||||
public var enabledAlerts: [String]?
|
||||
public var notifyUpdates: Bool?
|
||||
public var alertCPUTemperature: Int?
|
||||
public var alertCPULoad: Int?
|
||||
public var language: String?
|
||||
@@ -99,6 +100,7 @@ extension AppSettings {
|
||||
doc.probeTarget = probeTarget.rawValue
|
||||
doc.publicIPLookup = publicIPLookup
|
||||
doc.enabledAlerts = enabledAlerts.map(\.rawValue).sorted()
|
||||
doc.notifyUpdates = notifyUpdates
|
||||
doc.alertCPUTemperature = alertCPUTemperature
|
||||
doc.alertCPULoad = alertCPULoad
|
||||
doc.language = language.rawValue
|
||||
@@ -121,6 +123,7 @@ extension AppSettings {
|
||||
value.flatMap { options.contains($0) ? $0 : nil }
|
||||
}
|
||||
|
||||
assign(\.notifyUpdates, doc.notifyUpdates)
|
||||
assign(\.calendarEnabled, doc.calendarEnabled)
|
||||
assign(\.calendarPreferences, doc.calendarPreferences)
|
||||
assign(\.restEnabled, doc.restEnabled)
|
||||
|
||||
@@ -28,8 +28,10 @@ public final class UpdateController {
|
||||
didSet { defaults.set(skippedVersion, forKey: Keys.skippedVersion) }
|
||||
}
|
||||
|
||||
/// 自动检查发现未跳过的新版本时调用,由 AppController 打开升级提示窗口
|
||||
/// 手动检查发现新版本时调用,由 AppController 打开升级提示窗口
|
||||
@ObservationIgnored var onPrompt: () -> Void = {}
|
||||
/// 后台发现新版本时请求系统通知;成功提交后才记录去重状态。
|
||||
@ObservationIgnored var onUpdateAvailable: (String) async -> Bool = { _ in false }
|
||||
/// 安装完成后退出应用,等进程结束再打开新版
|
||||
@ObservationIgnored var terminate: () -> Void = { NSApp.terminate(nil) }
|
||||
|
||||
@@ -37,6 +39,7 @@ public final class UpdateController {
|
||||
@ObservationIgnored private let defaults: UserDefaults
|
||||
@ObservationIgnored private var task: Task<Void, Never>?
|
||||
@ObservationIgnored private let launchedAt: Date
|
||||
@ObservationIgnored private var isNotifying = false
|
||||
|
||||
init(settings: AppSettings, defaults: UserDefaults = .standard, launchedAt: Date = Date()) {
|
||||
self.settings = settings
|
||||
@@ -93,8 +96,7 @@ public final class UpdateController {
|
||||
release = latest
|
||||
phase = .available
|
||||
Log.update.notice("发现新版本 \(latest.version, privacy: .public)")
|
||||
// 手动检查时总是提示;自动检查时跳过用户选择忽略的版本
|
||||
if userInitiated || (!suppressPrompt && latest.version != skippedVersion) { onPrompt() }
|
||||
await announceUpdate(version: latest.version, userInitiated: userInitiated, suppressPrompt: suppressPrompt)
|
||||
case .failure(let error):
|
||||
Log.update.error("检查更新失败:\(error.localizedDescription, privacy: .public)")
|
||||
// 自动检查失败不打扰用户,只在关于页里显示
|
||||
@@ -103,6 +105,21 @@ public final class UpdateController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 手动检查始终展示结果;后台通知遵循静默策略、开关、跳过版本及跨重启去重。
|
||||
func announceUpdate(version: String, userInitiated: Bool, suppressPrompt: Bool) async {
|
||||
if userInitiated {
|
||||
onPrompt()
|
||||
return
|
||||
}
|
||||
guard !suppressPrompt, settings.notifyUpdates, version != skippedVersion,
|
||||
!isNotifying else { return }
|
||||
if let last = defaults.string(forKey: Keys.notifiedVersion), !UpdateFeed.isNewer(version, than: last) { return }
|
||||
// 通知权限弹窗期间允许查看更新,但不能重复提交后台通知。
|
||||
isNotifying = true
|
||||
defer { isNotifying = false }
|
||||
if await onUpdateAvailable(version) { defaults.set(version, forKey: Keys.notifiedVersion) }
|
||||
}
|
||||
|
||||
private static func fetch(currentVersion: String) async -> Result<UpdateRelease, UpdateError> {
|
||||
do {
|
||||
let installationID = try InstallationIdentity().hashedID()
|
||||
@@ -248,5 +265,6 @@ public final class UpdateController {
|
||||
private enum Keys {
|
||||
static let lastChecked = "updateLastChecked"
|
||||
static let skippedVersion = "updateSkippedVersion"
|
||||
static let notifiedVersion = "updateNotifiedVersion"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright (C) 2026 ysicing
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import XStatsUI
|
||||
|
||||
@MainActor
|
||||
struct UpdateNotificationTests {
|
||||
private let defaults = UserDefaults(suiteName: "UpdateNotificationTests.\(UUID().uuidString)")!
|
||||
|
||||
@Test func preferenceDefaultsOnAndPreservesAnExplicitOptOut() throws {
|
||||
// 已有安装的其他通知偏好不影响新开关的默认值。
|
||||
defaults.set([String](), forKey: "enabledAlerts")
|
||||
let settings = AppSettings(defaults: defaults)
|
||||
#expect(settings.notifyUpdates)
|
||||
settings.notifyUpdates = false
|
||||
#expect(!AppSettings(defaults: defaults).notifyUpdates)
|
||||
let data = try JSONEncoder().encode(settings.exportDocument())
|
||||
let restored = AppSettings(defaults: UserDefaults(suiteName: UUID().uuidString)!)
|
||||
restored.apply(try JSONDecoder().decode(SettingsDocument.self, from: data))
|
||||
#expect(!restored.notifyUpdates)
|
||||
restored.apply(try JSONDecoder().decode(SettingsDocument.self, from: Data("{}".utf8)))
|
||||
#expect(!restored.notifyUpdates)
|
||||
}
|
||||
|
||||
@Test func successfulNotificationIsDeduplicatedAcrossRestarts() async {
|
||||
let settings = AppSettings(defaults: defaults)
|
||||
let first = UpdateController(settings: settings, defaults: defaults)
|
||||
var versions: [String] = []
|
||||
first.onUpdateAvailable = { versions.append($0); return true }
|
||||
await first.announceUpdate(version: "1.0.0", userInitiated: false, suppressPrompt: false)
|
||||
await first.announceUpdate(version: "1.0.0", userInitiated: false, suppressPrompt: false)
|
||||
let restarted = UpdateController(settings: settings, defaults: defaults)
|
||||
restarted.onUpdateAvailable = { versions.append($0); return true }
|
||||
await restarted.announceUpdate(version: "1.0.0", userInitiated: false, suppressPrompt: false)
|
||||
await restarted.announceUpdate(version: "1.0.1", userInitiated: false, suppressPrompt: false)
|
||||
await restarted.announceUpdate(version: "1.0.0", userInitiated: false, suppressPrompt: false)
|
||||
#expect(versions == ["1.0.0", "1.0.1"])
|
||||
}
|
||||
|
||||
@Test func deniedOrFailedSubmissionCanBeRetried() async {
|
||||
let controller = UpdateController(settings: AppSettings(defaults: defaults), defaults: defaults)
|
||||
var attempts = 0
|
||||
controller.onUpdateAvailable = { _ in attempts += 1; return attempts > 1 }
|
||||
for _ in 0..<3 {
|
||||
await controller.announceUpdate(version: "1.0.0", userInitiated: false, suppressPrompt: false)
|
||||
}
|
||||
#expect(attempts == 2)
|
||||
}
|
||||
|
||||
@Test func backgroundRespectsOptOutQuietModeAndSkippedVersion() async {
|
||||
defaults.set("1.0.0", forKey: "updateSkippedVersion")
|
||||
let settings = AppSettings(defaults: defaults)
|
||||
let controller = UpdateController(settings: settings, defaults: defaults)
|
||||
var versions: [String] = []
|
||||
controller.onUpdateAvailable = { versions.append($0); return true }
|
||||
settings.notifyUpdates = false
|
||||
await controller.announceUpdate(version: "1.0.1", userInitiated: false, suppressPrompt: false)
|
||||
settings.notifyUpdates = true
|
||||
await controller.announceUpdate(version: "1.0.1", userInitiated: false, suppressPrompt: true)
|
||||
await controller.announceUpdate(version: "1.0.0", userInitiated: false, suppressPrompt: false)
|
||||
#expect(versions.isEmpty)
|
||||
await controller.announceUpdate(version: "1.0.1", userInitiated: false, suppressPrompt: false)
|
||||
#expect(versions == ["1.0.1"])
|
||||
}
|
||||
|
||||
@Test func manualCheckAlwaysPresentsWithoutSendingNotification() async {
|
||||
defaults.set("1.0.0", forKey: "updateSkippedVersion")
|
||||
defaults.set("1.0.0", forKey: "updateNotifiedVersion")
|
||||
let settings = AppSettings(defaults: defaults)
|
||||
settings.notifyUpdates = false
|
||||
let controller = UpdateController(settings: settings, defaults: defaults)
|
||||
var prompts = 0
|
||||
var notifications = 0
|
||||
controller.onPrompt = { prompts += 1 }
|
||||
controller.onUpdateAvailable = { _ in notifications += 1; return true }
|
||||
await controller.announceUpdate(version: "1.0.0", userInitiated: true, suppressPrompt: true)
|
||||
#expect(prompts == 1)
|
||||
#expect(notifications == 0)
|
||||
}
|
||||
|
||||
@Test func pendingAuthorizationDoesNotAllowDuplicateNotifications() async {
|
||||
let controller = UpdateController(settings: AppSettings(defaults: defaults), defaults: defaults)
|
||||
var notifications = 0
|
||||
controller.onUpdateAvailable = { [weak controller] version in
|
||||
notifications += 1
|
||||
await controller?.announceUpdate(version: version, userInitiated: false, suppressPrompt: false)
|
||||
return true
|
||||
}
|
||||
await controller.announceUpdate(version: "1.0.0", userInitiated: false, suppressPrompt: false)
|
||||
#expect(notifications == 1)
|
||||
}
|
||||
|
||||
@Test func notificationRoutesToUpdatesAndReplacesPreviousVersion() {
|
||||
let first = AlertController.updateNotification(version: "1.0.0")
|
||||
let next = AlertController.updateNotification(version: "1.0.1")
|
||||
#expect(first.identifier == next.identifier)
|
||||
#expect(next.content.title.contains("1.0.1"))
|
||||
#expect(next.content.userInfo["updates"] as? Bool == true)
|
||||
#expect(next.trigger == nil)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user