WinterGram/submodules/TelegramUI/Sources/WidgetDataContext.swift
isaac 86d1456552 Postbox -> TelegramEngine waves 107-137 (squashed)
31 waves of consumer-side migration from `import Postbox` to TelegramEngine
typealiases. Net: 173 import drops + 39 BUILD-dep drops + 1 new typealias
(`EngineStoryId = StoryId`, wave 113).

Wave shapes used:
- Orphan-import sweeps (107, 108, 128): drop `import Postbox` from files
  whose only Postbox-symbol reference was the import line itself, then
  resolve build failures. Methodology requires token-level (`grep -oE`)
  filtering, not line-level, to avoid masking real Postbox usage on lines
  that also contain `Namespaces.X` references.
- Identifier-swap mini-waves (109-127, 129-134, 136-137): rename
  Postbox-typealiased identifiers to engine equivalents
  (PeerId -> EnginePeer.Id, MessageId -> EngineMessage.Id,
  MediaId -> EngineMedia.Id, MessageIndex -> EngineMessage.Index,
  StoryId -> EngineStoryId, ItemCollectionId -> EngineItemCollectionId,
  PreferencesEntry -> EnginePreferencesEntry,
  FetchResourceSourceType/Error -> EngineFetchResourceSourceType/Error,
  MemoryBuffer -> EngineMemoryBuffer, MessageTags -> EngineMessage.Tags,
  MessageAttribute -> EngineMessage.Attribute,
  TempBox -> EngineTempBox).
- Asset-string FP-only orphans (124).
- Typealias addition + drain (113): added `EngineStoryId` typealias to
  TelegramCore, then drained 3+11 consumer sites.

Hard blockers identified during these waves (must restore `import Postbox`
when present): MediaResource[A-Za-z]* (any suffix -- the literal
`MediaResource` matches don't catch MediaResourceData/MediaResourceId/etc.),
Postbox/MediaBox/MediaResource raw types, PostboxCoding/PostboxEncoder/
PostboxDecoder, TempBoxFile, ValueBoxKey, PostboxView, combinedView,
HashFunctions, postboxLog, openPostbox, declareEncodable, PeerView,
MessageHistoryView, MessageHistoryThreadData, CachedPeerData, RenderedPeer,
SelectivePrivacyPeer, SimpleDictionary, ItemCollectionInfosView,
ItemCollectionItem, ItemCollectionItemIndex, ItemCollectionViewEntryIndex,
ChatListIndex, ChatListEntrySummaryComponents, CodableEntry,
MessageHistoryThread, MessageHistoryAnchorIndex,
MessageHistoryEntryLocation, PeerStoryStats, PeerNameIndex,
PeerSummaryCounterTags, ChatListTotalUnreadStateCategory/Stats,
arePeersEqual. Protocol-shape blockers: bare `Peer`/`Message`/`Media`
in function signatures, generic args, enum-case payloads, or dict value
types (e.g., `[PeerId: Peer]`, `case messages([Message])`,
`Signal<(Peer?, ...), NoError>`).

`replace_all PeerId -> EnginePeer.Id` is dangerous: mangles compound
names like `failedPeerId`, `ContactListPeerId`, `nextRemoteMediaId`,
`replyToMessageId`. Pre-flight grep `\b[a-z][a-zA-Z]*PeerId\b` and only
replace_all if 0 matches.

Also removes unneeded design/plan docs from a separate (link-highlighting)
feature branch:
- docs/superpowers/plans/2026-05-02-link-highlighting-modern-path-fixes.md
- docs/superpowers/specs/2026-05-02-link-highlighting-modern-path-fixes-design.md

Squashed commits: 6d82c2980d..e6de5d53a3 (59 commits, including
per-wave content commits and per-wave CLAUDE.md bumps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:28:50 +02:00

330 lines
14 KiB
Swift

import Foundation
import SwiftSignalKit
import TelegramCore
import WidgetItems
import TelegramPresentationData
import NotificationsPresentationData
import WidgetKit
import TelegramUIPreferences
import WidgetItemsUtils
import AccountContext
import AppLock
import GeneratedSources
@available(iOSApplicationExtension 14.0, iOS 14.0, *)
private extension SelectFriendsIntent {
var configurationHash: String {
var result = "widget"
if let items = self.friends {
for item in items {
if let identifier = item.identifier {
result.append("+\(identifier)")
}
}
}
return result
}
}
private final class WidgetReloadManager {
private var inForeground = false
private var inForegroundDisposable: Disposable?
private var isReloadRequested = false
private var lastBackgroundReload: Double?
init(inForeground: Signal<Bool, NoError>) {
self.inForegroundDisposable = (inForeground
|> distinctUntilChanged
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
guard let strongSelf = self else {
return
}
if strongSelf.inForeground != value {
strongSelf.inForeground = value
if value {
strongSelf.performReloadIfNeeded()
}
}
})
}
deinit {
self.inForegroundDisposable?.dispose()
}
func requestReload() {
self.isReloadRequested = true
if self.inForeground {
self.performReloadIfNeeded()
} else {
let timestamp = CFAbsoluteTimeGetCurrent()
if let lastBackgroundReloadValue = self.lastBackgroundReload {
if abs(lastBackgroundReloadValue - timestamp) > 25.0 * 60.0 {
self.lastBackgroundReload = timestamp
performReloadIfNeeded()
}
} else {
self.lastBackgroundReload = timestamp
performReloadIfNeeded()
}
}
}
private func performReloadIfNeeded() {
if !self.isReloadRequested {
return
}
self.isReloadRequested = false
DispatchQueue.global(qos: .background).async {
if #available(iOSApplicationExtension 14.0, iOS 14.0, *) {
#if arch(arm64) || arch(i386) || arch(x86_64)
WidgetCenter.shared.reloadAllTimelines()
#endif
}
}
}
}
final class WidgetDataContext {
private let reloadManager: WidgetReloadManager
private var disposable: Disposable?
private var widgetPresentationDataDisposable: Disposable?
private var notificationPresentationDataDisposable: Disposable?
init(basePath: String, inForeground: Signal<Bool, NoError>, activeAccounts: Signal<[Account], NoError>, presentationData: Signal<PresentationData, NoError>, appLockContext: AppLockContextImpl) {
self.reloadManager = WidgetReloadManager(inForeground: inForeground)
let queue = Queue()
let updatedAdditionalPeerIds: Signal<[AccountRecordId: Set<EnginePeer.Id>], NoError> = Signal { subscriber in
if #available(iOSApplicationExtension 14.0, iOS 14.0, *) {
#if arch(arm64) || arch(x86_64)
WidgetCenter.shared.getCurrentConfigurations { result in
var peerIds: [AccountRecordId: Set<EnginePeer.Id>] = [:]
func processFriend(_ item: Friend) {
guard let identifier = item.identifier else {
return
}
guard let index = identifier.firstIndex(of: ":") else {
return
}
guard let accountIdValue = Int64(identifier[identifier.startIndex ..< index]) else {
return
}
guard let peerIdValue = Int64(identifier[identifier.index(after: index)...]) else {
return
}
let accountId = AccountRecordId(rawValue: accountIdValue)
let peerId = EnginePeer.Id(peerIdValue)
if peerIds[accountId] == nil {
peerIds[accountId] = Set()
}
peerIds[accountId]?.insert(peerId)
}
if case let .success(infos) = result {
for info in infos {
if let configuration = info.configuration as? SelectFriendsIntent {
if let items = configuration.friends {
for item in items {
processFriend(item)
}
}
} else if let configuration = info.configuration as? SelectAvatarFriendsIntent {
if let items = configuration.friends {
for item in items {
processFriend(item)
}
}
}
}
}
subscriber.putNext(peerIds)
subscriber.putCompletion()
}
#else
subscriber.putNext([:])
subscriber.putCompletion()
#endif
} else {
subscriber.putNext([:])
subscriber.putCompletion()
}
return EmptyDisposable
}
|> runOn(queue)
|> then(
Signal<[AccountRecordId: Set<EnginePeer.Id>], NoError>.complete()
|> delay(10.0, queue: queue)
)
|> restart
self.disposable = (combineLatest(queue: queue,
updatedAdditionalPeerIds |> distinctUntilChanged,
activeAccounts |> distinctUntilChanged(isEqual: { lhs, rhs in
if lhs.count != rhs.count {
return false
}
for i in 0 ..< lhs.count {
if lhs[i] !== rhs[i] {
return false
}
}
return true
})
)
|> mapToSignal { peerIdsByAccount, accounts -> Signal<[WidgetDataPeer], NoError> in
var accountSignals: [Signal<[WidgetDataPeer], NoError>] = []
for (accountId, peerIds) in peerIdsByAccount {
var accountValue: Account?
for value in accounts {
if value.id == accountId {
accountValue = value
break
}
}
guard let account = accountValue else {
continue
}
if peerIds.isEmpty {
continue
}
accountSignals.append(TelegramEngine(account: account).data.subscribe(EngineDataMap(
peerIds.map(TelegramEngine.EngineData.Item.Messages.TopMessage.init)
))
|> map { topMessages -> [WidgetDataPeer] in
var result: [WidgetDataPeer] = []
for (peerId, message) in topMessages {
guard let message = message else {
continue
}
guard let peer = message.peers[message.id.peerId] else {
continue
}
var name: String = ""
var lastName: String?
if let user = peer as? TelegramUser {
if let firstName = user.firstName {
name = firstName
lastName = user.lastName
} else if let lastName = user.lastName {
name = lastName
} else if let phone = user.phone, !phone.isEmpty {
name = phone
}
} else {
name = peer.debugDisplayTitle
}
var isForum = false
if let peer = peer as? TelegramChannel, peer.isForumOrMonoForum {
isForum = true
}
result.append(WidgetDataPeer(id: peerId.toInt64(), name: name, lastName: lastName, letters: [], avatarPath: nil, badge: nil, message: WidgetDataPeer.Message(accountPeerId: account.peerId, message: message), isForum: isForum))
}
result.sort(by: { lhs, rhs in
return lhs.id < rhs.id
})
return result
})
}
return combineLatest(queue: queue, accountSignals)
|> map { lists -> [WidgetDataPeer] in
var result: [WidgetDataPeer] = []
for list in lists {
result.append(contentsOf: list)
}
result.sort(by: { lhs, rhs in
return lhs.id < rhs.id
})
return result
}
}
|> distinctUntilChanged
|> deliverOnMainQueue).startStrict(next: { [weak self] _ in
self?.reloadManager.requestReload()
})
self.widgetPresentationDataDisposable = (presentationData
|> map { presentationData -> WidgetPresentationData in
return WidgetPresentationData(
widgetChatsGalleryTitle: presentationData.strings.Widget_ChatsGalleryTitle,
widgetChatsGalleryDescription: presentationData.strings.Widget_ChatsGalleryDescription,
widgetShortcutsGalleryTitle: presentationData.strings.Widget_ShortcutsGalleryTitle,
widgetShortcutsGalleryDescription: presentationData.strings.Widget_ShortcutsGalleryDescription,
widgetLongTapToEdit: presentationData.strings.Widget_LongTapToEdit,
widgetUpdatedTodayAt: presentationData.strings.Widget_UpdatedTodayAt,
widgetUpdatedAt: presentationData.strings.Widget_UpdatedAt,
messageAuthorYou: presentationData.strings.DialogList_You,
messagePhoto: presentationData.strings.Message_Photo,
messageVideo: presentationData.strings.Message_Video,
messageAnimation: presentationData.strings.Message_Animation,
messageVoice: presentationData.strings.Message_Audio,
messageVideoMessage: presentationData.strings.Message_VideoMessage,
messageSticker: presentationData.strings.Message_Sticker,
messageVoiceCall: presentationData.strings.Watch_Message_Call,
messageVideoCall: presentationData.strings.Watch_Message_Call,
messageLocation: presentationData.strings.Message_Location,
autodeleteTimerUpdated: presentationData.strings.Widget_MessageAutoremoveTimerUpdated,
autodeleteTimerRemoved: presentationData.strings.Widget_MessageAutoremoveTimerRemoved,
generalLockedTitle: presentationData.strings.Intents_ErrorLockedTitle,
generalLockedText: presentationData.strings.Intents_ErrorLockedText,
chatSavedMessages: presentationData.strings.DialogList_SavedMessages
)
}
|> distinctUntilChanged).startStrict(next: { value in
let path = widgetPresentationDataPath(rootPath: basePath)
if let data = try? JSONEncoder().encode(value) {
let _ = try? data.write(to: URL(fileURLWithPath: path), options: [.atomic])
} else {
let _ = try? FileManager.default.removeItem(atPath: path)
}
if #available(iOSApplicationExtension 14.0, iOS 14.0, *) {
#if arch(arm64) || arch(i386) || arch(x86_64)
WidgetCenter.shared.reloadAllTimelines()
#endif
}
})
self.notificationPresentationDataDisposable = (presentationData
|> map { presentationData -> NotificationsPresentationData in
var incomingCallString = presentationData.strings.PUSH_PHONE_CALL_REQUEST("").string
if let range = incomingCallString.range(of: "|") {
incomingCallString = String(incomingCallString[range.upperBound...])
}
return NotificationsPresentationData(
applicationLockedMessageString: presentationData.strings.PUSH_LOCKED_MESSAGE("").string,
incomingCallString: incomingCallString
)
}
|> distinctUntilChanged).startStrict(next: { value in
let path = notificationsPresentationDataPath(rootPath: basePath)
if let data = try? JSONEncoder().encode(value) {
let _ = try? data.write(to: URL(fileURLWithPath: path), options: [.atomic])
} else {
let _ = try? FileManager.default.removeItem(atPath: path)
}
})
}
deinit {
self.disposable?.dispose()
self.widgetPresentationDataDisposable?.dispose()
self.notificationPresentationDataDisposable?.dispose()
}
}