Skip to content

Commit 25b507c

Browse files
committed
Notifications: story notifications and Stories settings
1 parent b89905f commit 25b507c

17 files changed

Lines changed: 602 additions & 98 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "icon.png",
5+
"idiom" : "universal",
6+
"scale" : "1x"
7+
},
8+
{
9+
"filename" : "icon@2x.png",
10+
"idiom" : "universal",
11+
"scale" : "2x"
12+
},
13+
{
14+
"idiom" : "universal",
15+
"scale" : "3x"
16+
}
17+
],
18+
"info" : {
19+
"author" : "xcode",
20+
"version" : 1
21+
}
22+
}
872 Bytes
Loading
1.69 KB
Loading

‎Telegram-Mac/account/SharedNotificationManager.swift‎

Lines changed: 180 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,13 @@ struct LockNotificationsData : Equatable {
114114
final class SharedNotificationBindings {
115115
let navigateToChat:(Account, PeerId) -> Void
116116
let navigateToThread:(Account, MessageId, MessageId?, MessageHistoryThreadData?) -> Void // threadId, fromId
117+
let navigateToStory:(Account, PeerId) -> Void
117118
let updateCurrectController:()->Void
118119
let applyMaxReadIndexInteractively:(MessageIndex)->Void
119-
init(navigateToChat: @escaping(Account, PeerId) -> Void, navigateToThread: @escaping(Account, MessageId, MessageId?, MessageHistoryThreadData?) -> Void, updateCurrectController: @escaping()->Void, applyMaxReadIndexInteractively:@escaping(MessageIndex)->Void) {
120+
init(navigateToChat: @escaping(Account, PeerId) -> Void, navigateToThread: @escaping(Account, MessageId, MessageId?, MessageHistoryThreadData?) -> Void, navigateToStory: @escaping(Account, PeerId) -> Void, updateCurrectController: @escaping()->Void, applyMaxReadIndexInteractively:@escaping(MessageIndex)->Void) {
120121
self.navigateToChat = navigateToChat
121122
self.navigateToThread = navigateToThread
123+
self.navigateToStory = navigateToStory
122124
self.updateCurrectController = updateCurrectController
123125
self.applyMaxReadIndexInteractively = applyMaxReadIndexInteractively
124126
}
@@ -156,6 +158,7 @@ final class SharedNotificationManager : NSObject, NSUserNotificationCenterDelega
156158
}
157159

158160
private let disposableDict: DisposableDict<AccountRecordId> = DisposableDict()
161+
private let storyDisposableDict: DisposableDict<AccountRecordId> = DisposableDict()
159162
private let inAppSettingsDisposable = MetaDisposable()
160163
private let lockTimeoutDisposable = MetaDisposable()
161164
let accountManager: AccountManager<TelegramAccountManagerTypes>
@@ -256,6 +259,7 @@ final class SharedNotificationManager : NSObject, NSUserNotificationCenterDelega
256259
let activeIds = Set(accounts.accounts.map { $0.0 })
257260
for (id, _) in self.activeAccounts.accounts where !activeIds.contains(id) {
258261
self.disposableDict.set(nil, forKey: id)
262+
self.storyDisposableDict.set(nil, forKey: id)
259263
}
260264
for account in accounts.accounts {
261265
self.startNotifyListener(with: account.1, primary: account.0 == accounts.primary?.id)
@@ -811,22 +815,185 @@ final class SharedNotificationManager : NSObject, NSUserNotificationCenterDelega
811815

812816
notification.userInfo = dict
813817

814-
_ = UNUserNotifications.authorizationStatus.start(next: { status in
815-
switch status {
816-
case .authorized:
817-
UNUserNotifications.current?.add(notification)
818-
case .notDetermined:
819-
UNUserNotifications.current?.authorize { manager in
820-
manager.add(notification)
821-
}
822-
default:
823-
break
824-
}
825-
})
818+
self.deliver(notification)
826819
}
827820
}
828821
}
829822
}), forKey: account.id)
823+
824+
startStoryNotifyListener(with: account, primary: primary)
825+
}
826+
827+
private func deliver(_ notification: NSUserNotification) {
828+
_ = UNUserNotifications.authorizationStatus.start(next: { status in
829+
switch status {
830+
case .authorized:
831+
UNUserNotifications.current?.add(notification)
832+
case .notDetermined:
833+
UNUserNotifications.current?.authorize { manager in
834+
manager.add(notification)
835+
}
836+
default:
837+
break
838+
}
839+
})
840+
}
841+
842+
private struct StoryNotification {
843+
let peer: EnginePeer
844+
let hideSender: Bool
845+
let sound: PeerMessageSound
846+
}
847+
848+
private func startStoryNotifyListener(with account: Account, primary: Bool) {
849+
let engine = TelegramEngine(account: account)
850+
let screenLocked = self.screenLocked
851+
let accountManager = self.accountManager
852+
let knownTimestamps = Atomic<[PeerId: Int32]?>(value: nil)
853+
854+
let newStoryPeers: Signal<[EnginePeer], NoError> = engine.messages.storySubscriptions(isHidden: false) |> map { subscriptions -> [EnginePeer] in
855+
var current: [PeerId: Int32] = [:]
856+
for item in subscriptions.items {
857+
current[item.peer.id] = item.lastTimestamp
858+
}
859+
guard let previous = knownTimestamps.swap(current) else {
860+
return []
861+
}
862+
let minTimestamp = Int32(Date().timeIntervalSince1970) - 60
863+
return subscriptions.items.filter { item in
864+
return item.peer.id != account.peerId && item.hasUnseen && item.lastTimestamp >= minTimestamp && item.lastTimestamp > (previous[item.peer.id] ?? 0)
865+
}.map { $0.peer }
866+
} |> filter { !$0.isEmpty }
867+
868+
let topPeerIds: Signal<Set<PeerId>, NoError> = engine.peers.recentPeers() |> take(1) |> map { recentPeers in
869+
guard case let .peers(peers) = recentPeers else {
870+
return Set()
871+
}
872+
return Set(peers.prefix(5).map { $0.id })
873+
}
874+
875+
let signal = newStoryPeers |> mapToQueue { peers -> Signal<([StoryNotification], InAppNotificationSettings, Bool, Peer, [PeerId: NSImage], [(PeerMessageSound, String?)]), NoError> in
876+
return combineLatest(appNotificationSettings(accountManager: accountManager) |> take(1), topPeerIds, screenLocked.get() |> take(1), account.postbox.loadedPeerWithId(account.peerId) |> take(1)) |> mapToSignal { inAppSettings, topPeerIds, lockData, accountPeer in
877+
guard inAppSettings.enabled && inAppSettings.muteUntil < Int32(Date().timeIntervalSince1970) else {
878+
return .complete()
879+
}
880+
return account.postbox.transaction { transaction -> [StoryNotification] in
881+
let globalSettings = transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications)?.get(GlobalNotificationSettings.self)?.effective ?? GlobalNotificationSettingsSet.defaultSettings
882+
let globalStorySettings = globalSettings.privateChats.storySettings
883+
var result: [StoryNotification] = []
884+
for peer in peers {
885+
let storySettings = (transaction.getPeerNotificationSettings(id: peer.id) as? TelegramPeerNotificationSettings)?.storySettings ?? PeerStoryNotificationSettings.default
886+
let enabled: Bool
887+
switch storySettings.mute {
888+
case .muted:
889+
enabled = false
890+
case .unmuted:
891+
enabled = true
892+
case .default:
893+
switch globalStorySettings.mute {
894+
case .muted:
895+
enabled = false
896+
case .unmuted:
897+
enabled = true
898+
case .default:
899+
enabled = topPeerIds.contains(peer.id)
900+
}
901+
}
902+
if !enabled {
903+
continue
904+
}
905+
let hideSender: Bool
906+
switch storySettings.hideSender {
907+
case .hide:
908+
hideSender = true
909+
case .show:
910+
hideSender = false
911+
case .default:
912+
hideSender = globalStorySettings.hideSender == .hide
913+
}
914+
let sound = storySettings.sound != .default ? storySettings.sound : effectiveCategorySound(globalStorySettings.sound)
915+
result.append(StoryNotification(peer: peer, hideSender: hideSender, sound: sound))
916+
}
917+
return result
918+
} |> mapToSignal { notifications -> Signal<([StoryNotification], InAppNotificationSettings, Bool, Peer, [PeerId: NSImage], [(PeerMessageSound, String?)]), NoError> in
919+
if notifications.isEmpty {
920+
return .complete()
921+
}
922+
let photos: [Signal<(PeerId, CGImage?), NoError>] = notifications.map { notification in
923+
let peer = notification.peer._asPeer()
924+
return peerAvatarImage(account: account, photo: .peer(peer, peer.smallProfileImage, peer.namePeerColor, peer.displayLetters, nil, nil), genCap: false) |> take(1) |> map { (peer.id, $0.0) }
925+
}
926+
var sounds: [PeerMessageSound] = []
927+
for notification in notifications where !sounds.contains(notification.sound) {
928+
sounds.append(notification.sound)
929+
}
930+
let deviceTone = getNotificationToneFile(account: account, sound: inAppSettings.tone)
931+
|> timeout(3.0, queue: Queue.concurrentDefaultQueue(), alternate: .single("default"))
932+
let paths: [Signal<(PeerMessageSound, String?), NoError>] = sounds.map { sound in
933+
switch sound {
934+
case .none:
935+
return .single((sound, nil))
936+
case .default:
937+
return deviceTone |> map { (sound, $0) }
938+
default:
939+
return getNotificationToneFile(account: account, sound: sound)
940+
|> timeout(3.0, queue: Queue.concurrentDefaultQueue(), alternate: deviceTone)
941+
|> map { (sound, $0) }
942+
}
943+
}
944+
return combineLatest(combineLatest(photos), combineLatest(paths)) |> take(1) |> map { photos, paths in
945+
var images: [PeerId: NSImage] = [:]
946+
for (peerId, image) in photos {
947+
if let image = image {
948+
images[peerId] = NSImage(cgImage: image, size: NSMakeSize(50, 50))
949+
}
950+
}
951+
return (notifications, inAppSettings, lockData.isLocked, accountPeer, images, paths)
952+
}
953+
}
954+
}
955+
}
956+
957+
storyDisposableDict.set((signal |> deliverOnMainQueue).start(next: { [weak self] notifications, inAppSettings, screenIsLocked, accountPeer, images, soundPaths in
958+
guard let self else {
959+
return
960+
}
961+
if !primary, !inAppSettings.notifyAllAccounts {
962+
return
963+
}
964+
if !self.shouldPresent(accountId: account.id, wasScheduled: false) {
965+
return
966+
}
967+
for story in notifications {
968+
let hidden = story.hideSender || screenIsLocked
969+
var title = hidden ? appName : story.peer._asPeer().displayTitle
970+
if self.activeAccounts.accounts.count > 1 && !screenIsLocked {
971+
title += " → \(accountPeer.addressName ?? accountPeer.displayTitle)"
972+
}
973+
let notification = NSUserNotification()
974+
notification.identifier = "story_\(story.peer.id.toInt64())"
975+
notification.title = title
976+
notification.informativeText = hidden ? strings().notificationStoryHidden : strings().notificationStoryPosted
977+
notification.contentImage = hidden ? nil : images[story.peer.id]
978+
notification.hasReplyButton = false
979+
notification.hasActionButton = false
980+
if #available(macOS 10.14, *), !screenIsLocked {
981+
switch inAppSettings.tone {
982+
case .none:
983+
notification.soundName = nil
984+
default:
985+
notification.soundName = soundPaths.first(where: { $0.0 == story.sound })?.1
986+
}
987+
}
988+
notification.userInfo = [
989+
"story.peer.id": story.peer.id.id._internalGetInt64Value(),
990+
"story.peer.namespace": story.peer.id.namespace._internalGetInt32Value(),
991+
"accountId": account.id.int64,
992+
"timestamp": Int32(Date().timeIntervalSince1970)
993+
]
994+
self.deliver(notification)
995+
}
996+
}), forKey: account.id)
830997
}
831998

832999

‎Telegram-Mac/app/AppDelegate.swift‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,18 @@ class AppDelegate: NSResponder, NSApplicationDelegate, NSUserNotificationCenterD
919919
NSApp.activate(ignoringOtherApps: true)
920920
window.deminiaturize(nil)
921921
}
922+
}, navigateToStory: { account, peerId in
923+
if let contextValue = self.accountContext(for: account.id) {
924+
closeAllModals(window: contextValue.window)
925+
contextValue.window.makeKeyAndOrderFront(nil)
926+
NSApp.activate(ignoringOtherApps: true)
927+
contextValue.window.deminiaturize(nil)
928+
StoryModalController.ShowStories(context: contextValue, isHidden: false, initialId: StoryInitialIndex(peerId: peerId, id: nil, messageId: nil, takeControl: nil), singlePeer: true)
929+
} else {
930+
sharedContext.switchToAccount(id: account.id, action: .story(peerId))
931+
NSApp.activate(ignoringOtherApps: true)
932+
window.deminiaturize(nil)
933+
}
922934
}, updateCurrectController: {
923935
if let contextValue = self.contextValue {
924936
contextValue.context.bindings.rootNavigation().controller.updateController()

‎Telegram-Mac/app/ApplicationContext.swift‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import InAppSettings
1111
import IOKit
1212
import CodeSyntax
1313
import SettingsUI
14+
import StoriesUI
1415
import PrivateCallScreen
1516
import DetectSpeech
1617
import WalletContext
@@ -215,6 +216,7 @@ final class UnauthorizedApplicationContext {
215216
enum ApplicationContextLaunchAction {
216217
case navigate(ViewController)
217218
case preferences
219+
case story(PeerId)
218220
}
219221

220222

@@ -909,6 +911,10 @@ final class AuthorizedApplicationContext: NSObject, SplitViewDelegate {
909911
_ready.set(leftController.chatList.ready.get())
910912
self.leftController.tabController.select(index: self.leftController.chatIndex)
911913
}
914+
case let .story(peerId):
915+
self.launchAction = .story(peerId)
916+
_ready.set(leftController.chatList.ready.get())
917+
self.leftController.tabController.select(index: self.leftController.chatIndex)
912918
case let .thread(threadId, fromId, threadData, _):
913919
self.leftController.tabController.select(index: self.leftController.chatIndex)
914920
self._ready.set(self.leftController.chatList.ready.get())
@@ -1089,6 +1095,9 @@ final class AuthorizedApplicationContext: NSObject, SplitViewDelegate {
10891095
context.bindings.rootNavigation().push(controller, context.layout == .single)
10901096
case .preferences:
10911097
leftController.tabController.select(index: leftController.settingsIndex)
1098+
case let .story(peerId):
1099+
leftController.tabController.select(index: leftController.chatIndex)
1100+
StoryModalController.ShowStories(context: context, isHidden: false, initialId: StoryInitialIndex(peerId: peerId, id: nil, messageId: nil, takeControl: nil), singlePeer: true)
10921101
}
10931102
self.launchAction = nil
10941103
} else {
1.39 KB
Binary file not shown.

‎Telegram-Mac/packages-integration/hooks/TelegramThemeHooksInstall.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,7 @@ private func themeHookImage(_ image: TelegramThemeImage) -> NSImage {
782782
case .iconSettingsNotifyGroupChats: return NSImage(resource: .iconSettingsNotifyGroupChats)
783783
case .iconSettingsNotifyPrivateChats: return NSImage(resource: .iconSettingsNotifyPrivateChats)
784784
case .iconSettingsNotifyReactions: return NSImage(resource: .iconSettingsNotifyReactions)
785+
case .iconSettingsNotifyStories: return NSImage(resource: .iconSettingsNotifyStories)
785786
case .iconSettingsProfile: return NSImage(resource: .iconSettingsProfile)
786787
case .iconSettingsProxy: return NSImage(resource: .iconSettingsProxy)
787788
case .iconSettingsSecurity: return NSImage(resource: .iconSettingsSecurity)

‎Telegram-Mac/utils/InAppLinks.swift‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,8 +1846,25 @@ func execute(inapp:inAppLink, window: Window? = nil, isExternal: Bool = false, a
18461846
controller = NotificationCategorySettingsController(context: context, category: .groupChats, focusOnItemTag: chatSubSection.flatMap(notificationCategoryEntryTag))
18471847
case let .channels(chatSubSection):
18481848
controller = NotificationCategorySettingsController(context: context, category: .channels, focusOnItemTag: chatSubSection.flatMap(notificationCategoryEntryTag))
1849-
case .stories:
1850-
controller = NotificationPreferencesController(context)
1849+
case let .stories(storiesSubSection):
1850+
let tag: NotificationCategoryEntryTag?
1851+
switch storiesSubSection {
1852+
case .new:
1853+
tag = .show
1854+
case .important:
1855+
tag = .important
1856+
case .show_sender:
1857+
tag = .preview
1858+
case .sound:
1859+
tag = .sound
1860+
case .add_exception:
1861+
tag = .addException
1862+
case .delete_exceptions:
1863+
tag = .deleteExceptions
1864+
case .none:
1865+
tag = nil
1866+
}
1867+
controller = NotificationCategorySettingsController(context: context, category: .stories, focusOnItemTag: tag)
18511868
case let .reactions(reactionsSubSection):
18521869
let tag: ReactionNotificationEntryTag?
18531870
switch reactionsSubSection {

‎Telegram-Mac/utils/UNUserNotifications.swift‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ class UNUserNotifications : NSObject {
7070
}
7171

7272
fileprivate func activateNotification(userInfo:[AnyHashable : Any], replyText: String? = nil) {
73+
if let id = userInfo["story.peer.id"] as? Int64, let namespace = userInfo["story.peer.namespace"] as? Int32, let accountId = userInfo["accountId"] as? Int64 {
74+
guard let account = manager.activeAccounts.accounts.first(where: { $0.0 == AccountRecordId(rawValue: accountId) })?.1 else {
75+
return
76+
}
77+
let peerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(namespace), id: PeerId.Id._internalFromInt64Value(id))
78+
self.bindings.navigateToStory(account, peerId)
79+
return
80+
}
7381
if let messageId = getNotificationMessageId(userInfo: userInfo, for: "reply"), let accountId = userInfo["accountId"] as? Int64 {
7482

7583
let accountId = AccountRecordId(rawValue: accountId)

0 commit comments

Comments
 (0)