Фикс: пуш-уведомления — убраны кастомные in-app баннеры, Desktop-active suppression, NSE timeout safety
This commit is contained in:
@@ -7,7 +7,6 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
A1B2C3D4E5F60718293A4B5C /* DeliveryReliabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5D /* DeliveryReliabilityTests.swift */; };
|
|
||||||
3146EDCE68162995CB5D1034 /* BehaviorParityFixtureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FC5C4F7E26FAFEC47C1D51 /* BehaviorParityFixtureTests.swift */; };
|
3146EDCE68162995CB5D1034 /* BehaviorParityFixtureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FC5C4F7E26FAFEC47C1D51 /* BehaviorParityFixtureTests.swift */; };
|
||||||
3C4D5E6F708192A3B4C5D6E7 /* AttachmentParityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D5E6F708192A3B4C5 /* AttachmentParityTests.swift */; };
|
3C4D5E6F708192A3B4C5D6E7 /* AttachmentParityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D5E6F708192A3B4C5 /* AttachmentParityTests.swift */; };
|
||||||
4C9BDB443750F7003CFB705C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 272B862BE4D99E7DD751CC3E /* Foundation.framework */; };
|
4C9BDB443750F7003CFB705C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 272B862BE4D99E7DD751CC3E /* Foundation.framework */; };
|
||||||
@@ -16,12 +15,9 @@
|
|||||||
853F29992F4B63D20092AD05 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 853F29982F4B63D20092AD05 /* Lottie */; };
|
853F29992F4B63D20092AD05 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 853F29982F4B63D20092AD05 /* Lottie */; };
|
||||||
853F29A02F4B63D20092AD05 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 853F29A12F4B63D20092AD05 /* P256K */; };
|
853F29A02F4B63D20092AD05 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 853F29A12F4B63D20092AD05 /* P256K */; };
|
||||||
85E887F72F6DC9460032774C /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = D1DB00022F8C00010092AD05 /* GRDB */; };
|
85E887F72F6DC9460032774C /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = D1DB00022F8C00010092AD05 /* GRDB */; };
|
||||||
|
A1B2C3D4E5F60718293A4B5C /* DeliveryReliabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5D /* DeliveryReliabilityTests.swift */; };
|
||||||
B7F1C2D34A5E67890ABCDEF1 /* CryptoParityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9A1B2C3D4E5F60718293A4B /* CryptoParityTests.swift */; };
|
B7F1C2D34A5E67890ABCDEF1 /* CryptoParityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9A1B2C3D4E5F60718293A4B /* CryptoParityTests.swift */; };
|
||||||
C8E2D3F45B6A78901BCDEF12 /* MessageDecodeHardeningTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1B2C3D4E5F60718293A4B /* MessageDecodeHardeningTests.swift */; };
|
C8E2D3F45B6A78901BCDEF12 /* MessageDecodeHardeningTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1B2C3D4E5F60718293A4B /* MessageDecodeHardeningTests.swift */; };
|
||||||
F0B1C2D3E4F5061728394A41 /* PendingChatRouteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A41 /* PendingChatRouteTests.swift */; };
|
|
||||||
F0B1C2D3E4F5061728394A42 /* PushNotificationPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A42 /* PushNotificationPacketTests.swift */; };
|
|
||||||
F0B1C2D3E4F5061728394A43 /* ForegroundNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A43 /* ForegroundNotificationTests.swift */; };
|
|
||||||
F0B1C2D3E4F5061728394A44 /* PushNotificationAuditTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A44 /* PushNotificationAuditTests.swift */; };
|
|
||||||
CC5AD9236E3B3BA95A0C29EC /* DBTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D3AF08B754B66DE17AF486D /* DBTestSupport.swift */; };
|
CC5AD9236E3B3BA95A0C29EC /* DBTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D3AF08B754B66DE17AF486D /* DBTestSupport.swift */; };
|
||||||
D0BD72A9646880B604F1AC3C /* RosettaNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A182B0EDE5C68E7C6F1FB6D1 /* RosettaNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
D0BD72A9646880B604F1AC3C /* RosettaNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A182B0EDE5C68E7C6F1FB6D1 /* RosettaNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
D60B2E657D691F256B5B7FD4 /* SchemaParityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F4769EEC8ABADB3AA98D3A5 /* SchemaParityTests.swift */; };
|
D60B2E657D691F256B5B7FD4 /* SchemaParityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F4769EEC8ABADB3AA98D3A5 /* SchemaParityTests.swift */; };
|
||||||
@@ -29,6 +25,10 @@
|
|||||||
E20000032F8D11110092AD05 /* WebRTC.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E20000042F8D11110092AD05 /* WebRTC.xcframework */; };
|
E20000032F8D11110092AD05 /* WebRTC.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E20000042F8D11110092AD05 /* WebRTC.xcframework */; };
|
||||||
E20000062F8D11110092AD05 /* WebRTC.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E20000042F8D11110092AD05 /* WebRTC.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
E20000062F8D11110092AD05 /* WebRTC.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E20000042F8D11110092AD05 /* WebRTC.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
EC5DFA298C697AE235323240 /* MigrationHarnessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAA4AD95B61886B5A22EF0D /* MigrationHarnessTests.swift */; };
|
EC5DFA298C697AE235323240 /* MigrationHarnessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAA4AD95B61886B5A22EF0D /* MigrationHarnessTests.swift */; };
|
||||||
|
F0B1C2D3E4F5061728394A41 /* PendingChatRouteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A41 /* PendingChatRouteTests.swift */; };
|
||||||
|
F0B1C2D3E4F5061728394A42 /* PushNotificationPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A42 /* PushNotificationPacketTests.swift */; };
|
||||||
|
F0B1C2D3E4F5061728394A43 /* ForegroundNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A43 /* ForegroundNotificationTests.swift */; };
|
||||||
|
F0B1C2D3E4F5061728394A44 /* PushNotificationAuditTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A44 /* PushNotificationAuditTests.swift */; };
|
||||||
F1A000012F6F00010092AD05 /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */ = {isa = PBXBuildFile; productRef = F1A000022F6F00010092AD05 /* FirebaseAnalyticsWithoutAdIdSupport */; };
|
F1A000012F6F00010092AD05 /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */ = {isa = PBXBuildFile; productRef = F1A000022F6F00010092AD05 /* FirebaseAnalyticsWithoutAdIdSupport */; };
|
||||||
F1A000032F6F00010092AD05 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = F1A000042F6F00010092AD05 /* FirebaseMessaging */; };
|
F1A000032F6F00010092AD05 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = F1A000042F6F00010092AD05 /* FirebaseMessaging */; };
|
||||||
F1A000062F6F00010092AD05 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = F1A000072F6F00010092AD05 /* FirebaseCrashlytics */; };
|
F1A000062F6F00010092AD05 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = F1A000072F6F00010092AD05 /* FirebaseCrashlytics */; };
|
||||||
@@ -99,7 +99,6 @@
|
|||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
0F43A41D5496A62870E307FC /* NotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
0F43A41D5496A62870E307FC /* NotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||||
A1B2C3D4E5F60718293A4B5D /* DeliveryReliabilityTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeliveryReliabilityTests.swift; sourceTree = "<group>"; };
|
|
||||||
1A2B3C4D5E6F708192A3B4C5 /* AttachmentParityTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AttachmentParityTests.swift; sourceTree = "<group>"; };
|
1A2B3C4D5E6F708192A3B4C5 /* AttachmentParityTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AttachmentParityTests.swift; sourceTree = "<group>"; };
|
||||||
272B862BE4D99E7DD751CC3E /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
|
272B862BE4D99E7DD751CC3E /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||||
2B3C4D5E6F708192A3B4C5D6 /* SearchParityTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SearchParityTests.swift; sourceTree = "<group>"; };
|
2B3C4D5E6F708192A3B4C5D6 /* SearchParityTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SearchParityTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -109,6 +108,7 @@
|
|||||||
853F29622F4B50410092AD05 /* Rosetta.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Rosetta.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
853F29622F4B50410092AD05 /* Rosetta.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Rosetta.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
93685A4F330DCD1B63EF121F /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
93685A4F330DCD1B63EF121F /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
A182B0EDE5C68E7C6F1FB6D1 /* RosettaNotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = RosettaNotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
A182B0EDE5C68E7C6F1FB6D1 /* RosettaNotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = RosettaNotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
A1B2C3D4E5F60718293A4B5D /* DeliveryReliabilityTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeliveryReliabilityTests.swift; sourceTree = "<group>"; };
|
||||||
C9FC5C4F7E26FAFEC47C1D51 /* BehaviorParityFixtureTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BehaviorParityFixtureTests.swift; sourceTree = "<group>"; };
|
C9FC5C4F7E26FAFEC47C1D51 /* BehaviorParityFixtureTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BehaviorParityFixtureTests.swift; sourceTree = "<group>"; };
|
||||||
D9A1B2C3D4E5F60718293A4B /* CryptoParityTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CryptoParityTests.swift; sourceTree = "<group>"; };
|
D9A1B2C3D4E5F60718293A4B /* CryptoParityTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CryptoParityTests.swift; sourceTree = "<group>"; };
|
||||||
DBAA4AD95B61886B5A22EF0D /* MigrationHarnessTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MigrationHarnessTests.swift; sourceTree = "<group>"; };
|
DBAA4AD95B61886B5A22EF0D /* MigrationHarnessTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MigrationHarnessTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -641,7 +641,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Rosetta/Rosetta.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Rosetta/Rosetta.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 32;
|
CURRENT_PROJECT_VERSION = 33;
|
||||||
DEVELOPMENT_TEAM = QN8Z263QGX;
|
DEVELOPMENT_TEAM = QN8Z263QGX;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -657,7 +657,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.1;
|
MARKETING_VERSION = 1.3.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev;
|
PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -681,7 +681,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Rosetta/Rosetta.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Rosetta/Rosetta.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 32;
|
CURRENT_PROJECT_VERSION = 33;
|
||||||
DEVELOPMENT_TEAM = QN8Z263QGX;
|
DEVELOPMENT_TEAM = QN8Z263QGX;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -697,7 +697,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.1;
|
MARKETING_VERSION = 1.3.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev;
|
PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -841,7 +841,7 @@
|
|||||||
C19929D9466573F31997B2C0 /* Release */,
|
C19929D9466573F31997B2C0 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
853F295D2F4B50410092AD05 /* Build configuration list for PBXProject "Rosetta" */ = {
|
853F295D2F4B50410092AD05 /* Build configuration list for PBXProject "Rosetta" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
@@ -850,7 +850,7 @@
|
|||||||
853F296C2F4B50420092AD05 /* Release */,
|
853F296C2F4B50420092AD05 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
853F296D2F4B50420092AD05 /* Build configuration list for PBXNativeTarget "Rosetta" */ = {
|
853F296D2F4B50420092AD05 /* Build configuration list for PBXNativeTarget "Rosetta" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
@@ -859,7 +859,7 @@
|
|||||||
853F296F2F4B50420092AD05 /* Release */,
|
853F296F2F4B50420092AD05 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
B5D2E60ADEB8AE2E8F7615C6 /* Build configuration list for PBXNativeTarget "RosettaNotificationService" */ = {
|
B5D2E60ADEB8AE2E8F7615C6 /* Build configuration list for PBXNativeTarget "RosettaNotificationService" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
@@ -868,7 +868,7 @@
|
|||||||
0140D6320A9CF4B5E933E0B1 /* Debug */,
|
0140D6320A9CF4B5E933E0B1 /* Debug */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
LA00000062F8D22220092AD05 /* Build configuration list for PBXNativeTarget "RosettaLiveActivityWidget" */ = {
|
LA00000062F8D22220092AD05 /* Build configuration list for PBXNativeTarget "RosettaLiveActivityWidget" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
@@ -877,7 +877,7 @@
|
|||||||
LA00000082F8D22220092AD05 /* Release */,
|
LA00000082F8D22220092AD05 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Release"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
|
|||||||
@@ -1,93 +1,10 @@
|
|||||||
import AudioToolbox
|
import Foundation
|
||||||
import AVFAudio
|
|
||||||
import Combine
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
/// Manages in-app notification banners (Telegram parity).
|
/// Foreground notification suppression logic.
|
||||||
/// Shows custom overlay instead of system banners when app is in foreground.
|
/// Determines whether a notification should be silenced (active chat or muted).
|
||||||
@MainActor
|
enum InAppNotificationManager {
|
||||||
final class InAppNotificationManager: ObservableObject {
|
|
||||||
|
|
||||||
static let shared = InAppNotificationManager()
|
/// Returns `true` if the notification should be suppressed (no banner).
|
||||||
|
|
||||||
@Published private(set) var currentNotification: InAppNotification?
|
|
||||||
|
|
||||||
private var dismissTask: Task<Void, Never>?
|
|
||||||
private var soundPlayer: AVAudioPlayer?
|
|
||||||
|
|
||||||
private static let autoDismissSeconds: UInt64 = 5
|
|
||||||
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
// MARK: - Data Model
|
|
||||||
|
|
||||||
struct InAppNotification: Identifiable, Equatable {
|
|
||||||
let id: UUID
|
|
||||||
let senderKey: String
|
|
||||||
let senderName: String
|
|
||||||
let messagePreview: String
|
|
||||||
let avatar: UIImage?
|
|
||||||
let avatarColorIndex: Int
|
|
||||||
let initials: String
|
|
||||||
|
|
||||||
static func == (lhs: InAppNotification, rhs: InAppNotification) -> Bool {
|
|
||||||
lhs.id == rhs.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Public API
|
|
||||||
|
|
||||||
/// Called from `willPresent` — extracts sender info and shows banner if appropriate.
|
|
||||||
func show(userInfo: [AnyHashable: Any]) {
|
|
||||||
let senderKey = userInfo["dialog"] as? String
|
|
||||||
?? AppDelegate.extractSenderKey(from: userInfo)
|
|
||||||
|
|
||||||
// --- Suppression logic (Telegram parity) ---
|
|
||||||
guard !senderKey.isEmpty else { return }
|
|
||||||
guard !MessageRepository.shared.isDialogActive(senderKey) else { return }
|
|
||||||
|
|
||||||
let mutedKeys = UserDefaults(suiteName: "group.com.rosetta.dev")?
|
|
||||||
.stringArray(forKey: "muted_chats_keys") ?? []
|
|
||||||
guard !mutedKeys.contains(senderKey) else { return }
|
|
||||||
|
|
||||||
// --- Resolve display data ---
|
|
||||||
let contactNames = UserDefaults(suiteName: "group.com.rosetta.dev")?
|
|
||||||
.dictionary(forKey: "contact_display_names") as? [String: String] ?? [:]
|
|
||||||
let name = contactNames[senderKey]
|
|
||||||
?? firstString(userInfo, keys: ["title", "sender_name", "from_title", "name"])
|
|
||||||
?? "Rosetta"
|
|
||||||
|
|
||||||
let preview = extractMessagePreview(from: userInfo)
|
|
||||||
let avatar = AvatarRepository.shared.loadAvatar(publicKey: senderKey)
|
|
||||||
let dialog = DialogRepository.shared.dialogs[senderKey]
|
|
||||||
|
|
||||||
let notification = InAppNotification(
|
|
||||||
id: UUID(),
|
|
||||||
senderKey: senderKey,
|
|
||||||
senderName: name,
|
|
||||||
messagePreview: preview,
|
|
||||||
avatar: avatar,
|
|
||||||
avatarColorIndex: dialog?.avatarColorIndex ?? abs(senderKey.hashValue) % 11,
|
|
||||||
initials: dialog?.initials ?? String(name.prefix(1)).uppercased()
|
|
||||||
)
|
|
||||||
|
|
||||||
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
|
|
||||||
currentNotification = notification
|
|
||||||
}
|
|
||||||
|
|
||||||
playNotificationSound()
|
|
||||||
playHaptic()
|
|
||||||
scheduleAutoDismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
func dismiss() {
|
|
||||||
dismissTask?.cancel()
|
|
||||||
withAnimation(.easeOut(duration: 0.25)) {
|
|
||||||
currentNotification = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Testable: checks whether a notification should be suppressed.
|
|
||||||
static func shouldSuppress(senderKey: String) -> Bool {
|
static func shouldSuppress(senderKey: String) -> Bool {
|
||||||
if senderKey.isEmpty { return true }
|
if senderKey.isEmpty { return true }
|
||||||
if MessageRepository.shared.isDialogActive(senderKey) { return true }
|
if MessageRepository.shared.isDialogActive(senderKey) { return true }
|
||||||
@@ -96,48 +13,4 @@ final class InAppNotificationManager: ObservableObject {
|
|||||||
if mutedKeys.contains(senderKey) { return true }
|
if mutedKeys.contains(senderKey) { return true }
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
|
||||||
|
|
||||||
private func scheduleAutoDismiss() {
|
|
||||||
dismissTask?.cancel()
|
|
||||||
dismissTask = Task {
|
|
||||||
try? await Task.sleep(nanoseconds: Self.autoDismissSeconds * 1_000_000_000)
|
|
||||||
guard !Task.isCancelled else { return }
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func playNotificationSound() {
|
|
||||||
// System "Tink" haptic feedback sound — lightweight, no custom mp3 needed.
|
|
||||||
AudioServicesPlaySystemSound(1057)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func playHaptic() {
|
|
||||||
// UIImpactFeedbackGenerator style tap via AudioServices.
|
|
||||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
|
||||||
generator.impactOccurred()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func extractMessagePreview(from userInfo: [AnyHashable: Any]) -> String {
|
|
||||||
// Try notification body directly.
|
|
||||||
if let body = userInfo["body"] as? String, !body.isEmpty { return body }
|
|
||||||
// Try aps.alert (can be string or dict).
|
|
||||||
if let aps = userInfo["aps"] as? [String: Any] {
|
|
||||||
if let alert = aps["alert"] as? String, !alert.isEmpty { return alert }
|
|
||||||
if let alertDict = aps["alert"] as? [String: Any],
|
|
||||||
let body = alertDict["body"] as? String, !body.isEmpty { return body }
|
|
||||||
}
|
|
||||||
return "New message"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func firstString(_ dict: [AnyHashable: Any], keys: [String]) -> String? {
|
|
||||||
for key in keys {
|
|
||||||
if let value = dict[key] as? String,
|
|
||||||
!value.trimmingCharacters(in: .whitespaces).isEmpty {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,8 @@ enum ReleaseNotes {
|
|||||||
version: appVersion,
|
version: appVersion,
|
||||||
body: """
|
body: """
|
||||||
|
|
||||||
**Пуш-уведомления — Telegram-parity и стабильность**
|
**Пуш-уведомления**
|
||||||
Группировка по чатам (threadIdentifier). Фикс исчезновения части уведомлений при тапе по пушу. NSE фильтрует повторные уведомления от одного отправителя и использует приоритет реальной аватарки из App Group (fallback: letter-avatar).
|
Только системные баннеры iOS — убраны кастомные in-app оверлеи, звуки и вибрации. Desktop-suppression: если читаешь на компьютере, телефон молчит 30 секунд.
|
||||||
|
|
||||||
**Дедупликация и валидация протокола**
|
|
||||||
Трёхуровневая защита от дублей (queue + process + DB). Улучшена валидация входящих пакетов для защиты от некорректных данных при синхронизации. Forward Picker UI parity.
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
/// Telegram-style in-app notification banner with glass background.
|
|
||||||
/// Shows sender avatar, name, and message preview. Supports swipe-up
|
|
||||||
/// to dismiss and tap to navigate to the chat.
|
|
||||||
struct InAppNotificationBanner: View {
|
|
||||||
|
|
||||||
let notification: InAppNotificationManager.InAppNotification
|
|
||||||
let onTap: () -> Void
|
|
||||||
let onDismiss: () -> Void
|
|
||||||
|
|
||||||
@State private var dragOffset: CGFloat = 0
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
AvatarView(
|
|
||||||
initials: notification.initials,
|
|
||||||
colorIndex: notification.avatarColorIndex,
|
|
||||||
size: 40,
|
|
||||||
image: notification.avatar
|
|
||||||
)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(notification.senderName)
|
|
||||||
.font(.system(size: 15, weight: .semibold))
|
|
||||||
.foregroundStyle(Color(RosettaColors.Adaptive.text))
|
|
||||||
.lineLimit(1)
|
|
||||||
|
|
||||||
Text(notification.messagePreview)
|
|
||||||
.font(.system(size: 14))
|
|
||||||
.foregroundStyle(Color(RosettaColors.Adaptive.textSecondary))
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 14)
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.background { TelegramGlassRoundedRect(cornerRadius: 16) }
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.offset(y: min(dragOffset, 0))
|
|
||||||
.gesture(
|
|
||||||
DragGesture(minimumDistance: 8)
|
|
||||||
.onChanged { value in
|
|
||||||
// Only allow dragging upward (negative Y).
|
|
||||||
dragOffset = min(value.translation.height, 0)
|
|
||||||
}
|
|
||||||
.onEnded { value in
|
|
||||||
if value.translation.height < -30 {
|
|
||||||
onDismiss()
|
|
||||||
} else {
|
|
||||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
|
|
||||||
dragOffset = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
.onTapGesture { onTap() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ struct MainTabView: View {
|
|||||||
@State private var isSettingsEditPresented = false
|
@State private var isSettingsEditPresented = false
|
||||||
@State private var isSettingsDetailPresented = false
|
@State private var isSettingsDetailPresented = false
|
||||||
@StateObject private var callManager = CallManager.shared
|
@StateObject private var callManager = CallManager.shared
|
||||||
@StateObject private var notificationManager = InAppNotificationManager.shared
|
|
||||||
|
|
||||||
// Add Account — presented as fullScreenCover so Settings stays alive.
|
// Add Account — presented as fullScreenCover so Settings stays alive.
|
||||||
// Using optional AuthScreen as the item ensures the correct screen is
|
// Using optional AuthScreen as the item ensures the correct screen is
|
||||||
@@ -71,39 +70,6 @@ struct MainTabView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-app notification banner overlay (Telegram parity).
|
|
||||||
// Slides from top, auto-dismisses after 5s, tap navigates to chat.
|
|
||||||
Group {
|
|
||||||
if let notification = notificationManager.currentNotification {
|
|
||||||
VStack {
|
|
||||||
InAppNotificationBanner(
|
|
||||||
notification: notification,
|
|
||||||
onTap: {
|
|
||||||
let route = ChatRoute(
|
|
||||||
publicKey: notification.senderKey,
|
|
||||||
title: notification.senderName,
|
|
||||||
username: "",
|
|
||||||
verified: 0
|
|
||||||
)
|
|
||||||
notificationManager.dismiss()
|
|
||||||
if selectedTab != .chats {
|
|
||||||
selectedTab = .chats
|
|
||||||
}
|
|
||||||
NotificationCenter.default.post(
|
|
||||||
name: .openChatFromNotification,
|
|
||||||
object: route
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onDismiss: { notificationManager.dismiss() }
|
|
||||||
)
|
|
||||||
.padding(.top, 4)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.transition(.move(edge: .top).combined(with: .opacity))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.zIndex(9)
|
|
||||||
|
|
||||||
// Full-screen device verification overlay (observation-isolated).
|
// Full-screen device verification overlay (observation-isolated).
|
||||||
// Covers nav bar, search bar, and tab bar — desktop parity.
|
// Covers nav bar, search bar, and tab bar — desktop parity.
|
||||||
DeviceConfirmOverlay()
|
DeviceConfirmOverlay()
|
||||||
|
|||||||
@@ -273,6 +273,21 @@ final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCent
|
|||||||
dialogKey = String(dialogKey.dropFirst("#group:".count))
|
dialogKey = String(dialogKey.dropFirst("#group:".count))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Desktop-active suppression: mark this dialog as "recently read on another device".
|
||||||
|
// NSE checks this flag — if a new message arrives for the same dialog within 30s,
|
||||||
|
// it suppresses the notification (user is actively reading on Desktop).
|
||||||
|
// NOTE: When server sends mutable-content:1 for READ, NSE also writes this flag.
|
||||||
|
// Both writes are idempotent (same dialogKey → same timestamp). Badge decrement
|
||||||
|
// is safe: NSE removes notifications first, AppDelegate finds 0 remaining → no double-decrement.
|
||||||
|
if let shared = UserDefaults(suiteName: "group.com.rosetta.dev") {
|
||||||
|
let now = Date().timeIntervalSince1970
|
||||||
|
var recentlyRead = shared.dictionary(forKey: "nse_recently_read_dialogs") as? [String: Double] ?? [:]
|
||||||
|
recentlyRead[dialogKey] = now
|
||||||
|
// Evict stale entries (> 60s) to prevent unbounded growth.
|
||||||
|
recentlyRead = recentlyRead.filter { now - $0.value < 60 }
|
||||||
|
shared.set(recentlyRead, forKey: "nse_recently_read_dialogs")
|
||||||
|
}
|
||||||
|
|
||||||
let center = UNUserNotificationCenter.current()
|
let center = UNUserNotificationCenter.current()
|
||||||
center.getDeliveredNotifications { delivered in
|
center.getDeliveredNotifications { delivered in
|
||||||
let idsToRemove = delivered
|
let idsToRemove = delivered
|
||||||
@@ -474,8 +489,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCent
|
|||||||
|
|
||||||
// MARK: - UNUserNotificationCenterDelegate
|
// MARK: - UNUserNotificationCenterDelegate
|
||||||
|
|
||||||
/// Handle foreground notifications — always suppress system banner,
|
/// Handle foreground notifications — show system banner unless chat is active or muted.
|
||||||
/// show custom in-app overlay instead (Telegram parity).
|
|
||||||
func userNotificationCenter(
|
func userNotificationCenter(
|
||||||
_ center: UNUserNotificationCenter,
|
_ center: UNUserNotificationCenter,
|
||||||
willPresent notification: UNNotification,
|
willPresent notification: UNNotification,
|
||||||
@@ -483,14 +497,14 @@ final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCent
|
|||||||
Void
|
Void
|
||||||
) {
|
) {
|
||||||
let userInfo = notification.request.content.userInfo
|
let userInfo = notification.request.content.userInfo
|
||||||
|
let senderKey = userInfo["dialog"] as? String
|
||||||
|
?? Self.extractSenderKey(from: userInfo)
|
||||||
|
|
||||||
// Always suppress system banner — custom in-app overlay handles display.
|
if InAppNotificationManager.shouldSuppress(senderKey: senderKey) {
|
||||||
completionHandler([])
|
completionHandler([])
|
||||||
|
return
|
||||||
// Trigger in-app notification banner (suppression logic inside manager).
|
|
||||||
Task { @MainActor in
|
|
||||||
InAppNotificationManager.shared.show(userInfo: userInfo)
|
|
||||||
}
|
}
|
||||||
|
completionHandler([.banner, .sound])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines whether a foreground notification should be suppressed.
|
/// Determines whether a foreground notification should be suppressed.
|
||||||
@@ -501,14 +515,10 @@ final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCent
|
|||||||
let senderKey = userInfo["dialog"] as? String
|
let senderKey = userInfo["dialog"] as? String
|
||||||
?? extractSenderKey(from: userInfo)
|
?? extractSenderKey(from: userInfo)
|
||||||
|
|
||||||
// Always suppress system banner — custom in-app overlay handles display.
|
|
||||||
// InAppNotificationManager.shouldSuppress() has the full suppression logic.
|
|
||||||
if InAppNotificationManager.shouldSuppress(senderKey: senderKey) {
|
if InAppNotificationManager.shouldSuppress(senderKey: senderKey) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
return [.banner, .sound]
|
||||||
// Even for non-suppressed notifications, return [] — we show our own banner.
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle notification tap — navigate to the sender's chat or expand call.
|
/// Handle notification tap — navigate to the sender's chat or expand call.
|
||||||
|
|||||||
@@ -260,7 +260,15 @@ final class NotificationService: UNNotificationServiceExtension {
|
|||||||
|
|
||||||
override func serviceExtensionTimeWillExpire() {
|
override func serviceExtensionTimeWillExpire() {
|
||||||
if let handler = contentHandler, let content = bestAttemptContent {
|
if let handler = contentHandler, let content = bestAttemptContent {
|
||||||
content.sound = .default
|
// Read pushes must stay silent even on timeout — no sound, no alert.
|
||||||
|
let pushType = content.userInfo["type"] as? String ?? ""
|
||||||
|
if pushType == "read" {
|
||||||
|
content.sound = nil
|
||||||
|
content.title = ""
|
||||||
|
content.body = ""
|
||||||
|
} else {
|
||||||
|
content.sound = .default
|
||||||
|
}
|
||||||
handler(content)
|
handler(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import UserNotifications
|
|||||||
|
|
||||||
// MARK: - Foreground Notification Suppression Tests
|
// MARK: - Foreground Notification Suppression Tests
|
||||||
|
|
||||||
/// Tests for the in-app notification banner suppression logic (Telegram parity).
|
/// Tests for foreground notification suppression logic.
|
||||||
/// System banners are always suppressed (`willPresent` returns `[]`).
|
/// `willPresent` returns `[.banner, .sound]` by default, `[]` for active/muted chats.
|
||||||
/// `InAppNotificationManager.shouldSuppress()` decides whether the
|
|
||||||
/// custom in-app banner should be shown or hidden.
|
|
||||||
@MainActor
|
@MainActor
|
||||||
struct ForegroundNotificationTests {
|
struct ForegroundNotificationTests {
|
||||||
|
|
||||||
@@ -17,21 +15,23 @@ struct ForegroundNotificationTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - System Banner Always Suppressed
|
// MARK: - System Banner Presentation
|
||||||
|
|
||||||
@Test("System banner always suppressed — foregroundPresentationOptions returns []")
|
@Test("Non-suppressed chat shows system banner with sound")
|
||||||
func systemBannerAlwaysSuppressed() {
|
func nonSuppressedShowsBanner() {
|
||||||
clearActiveDialogs()
|
clearActiveDialogs()
|
||||||
let userInfo: [AnyHashable: Any] = ["dialog": "02aaa", "title": "Alice"]
|
let userInfo: [AnyHashable: Any] = ["dialog": "02aaa", "title": "Alice"]
|
||||||
let options = AppDelegate.foregroundPresentationOptions(for: userInfo)
|
let options = AppDelegate.foregroundPresentationOptions(for: userInfo)
|
||||||
#expect(options == [])
|
#expect(options == [.banner, .sound])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("System banner suppressed even for inactive chats")
|
@Test("Active chat suppresses system banner")
|
||||||
func systemBannerSuppressedInactive() {
|
func activeChatSuppressesBanner() {
|
||||||
clearActiveDialogs()
|
clearActiveDialogs()
|
||||||
|
MessageRepository.shared.setDialogActive("02bbb", isActive: true)
|
||||||
let userInfo: [AnyHashable: Any] = ["dialog": "02bbb"]
|
let userInfo: [AnyHashable: Any] = ["dialog": "02bbb"]
|
||||||
#expect(AppDelegate.foregroundPresentationOptions(for: userInfo) == [])
|
#expect(AppDelegate.foregroundPresentationOptions(for: userInfo) == [])
|
||||||
|
MessageRepository.shared.setDialogActive("02bbb", isActive: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - In-App Banner: Active Chat → Suppress
|
// MARK: - In-App Banner: Active Chat → Suppress
|
||||||
|
|||||||
@@ -297,6 +297,39 @@ struct PushNotificationDesktopSuppressionTests {
|
|||||||
let shouldSuppress = recentlyRead[senderKey].map { now - $0 < Self.recentlyReadWindow } ?? false
|
let shouldSuppress = recentlyRead[senderKey].map { now - $0 < Self.recentlyReadWindow } ?? false
|
||||||
#expect(shouldSuppress == false)
|
#expect(shouldSuppress == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - AppDelegate → App Group flag (READ push writes nse_recently_read_dialogs)
|
||||||
|
|
||||||
|
@Test("handleReadPush stores recently-read flag in App Group for NSE")
|
||||||
|
func readPushStoresRecentlyReadFlagInAppGroup() {
|
||||||
|
let shared = UserDefaults(suiteName: "group.com.rosetta.dev")
|
||||||
|
let key = "nse_recently_read_dialogs"
|
||||||
|
let originalData = shared?.dictionary(forKey: key)
|
||||||
|
|
||||||
|
// Simulate what handleReadPush now does: write the recently-read flag.
|
||||||
|
let dialogKey = "02test_desktop_read_flag"
|
||||||
|
let now = Date().timeIntervalSince1970
|
||||||
|
var recentlyRead = shared?.dictionary(forKey: key) as? [String: Double] ?? [:]
|
||||||
|
recentlyRead[dialogKey] = now
|
||||||
|
recentlyRead = recentlyRead.filter { now - $0.value < 60 }
|
||||||
|
shared?.set(recentlyRead, forKey: key)
|
||||||
|
|
||||||
|
// Verify flag exists and is recent.
|
||||||
|
let stored = shared?.dictionary(forKey: key) as? [String: Double] ?? [:]
|
||||||
|
#expect(stored[dialogKey] != nil)
|
||||||
|
if let ts = stored[dialogKey] {
|
||||||
|
#expect(abs(ts - now) < 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify NSE suppression logic would fire for this dialog.
|
||||||
|
if let lastReadTime = stored[dialogKey] {
|
||||||
|
let elapsed = now - lastReadTime
|
||||||
|
#expect(elapsed < Self.recentlyReadWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup.
|
||||||
|
shared?.set(originalData, forKey: key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Read Push Group Key Normalization Tests
|
// MARK: - Read Push Group Key Normalization Tests
|
||||||
@@ -657,13 +690,11 @@ struct PushInAppBannerSuppressionExtendedTests {
|
|||||||
#expect(InAppNotificationManager.shouldSuppress(senderKey: "02normal") == false)
|
#expect(InAppNotificationManager.shouldSuppress(senderKey: "02normal") == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("System presentation options always return empty set")
|
@Test("System presentation returns banner+sound for non-suppressed chats")
|
||||||
func systemPresentationAlwaysEmpty() {
|
func systemPresentationShowsBanner() {
|
||||||
clearState()
|
clearState()
|
||||||
// Even for non-suppressed chats, system banner is always suppressed
|
|
||||||
// (custom in-app banner shown instead)
|
|
||||||
let options = AppDelegate.foregroundPresentationOptions(for: ["dialog": "02any_user"])
|
let options = AppDelegate.foregroundPresentationOptions(for: ["dialog": "02any_user"])
|
||||||
#expect(options == [])
|
#expect(options == [.banner, .sound])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user