From d0760c41f31c95ed870403f6a5bd28defd4caf74 Mon Sep 17 00:00:00 2001 From: Grayson Brewer Date: Thu, 23 Jul 2026 22:42:34 -0500 Subject: [PATCH] Watch: pre-cache favorite and frequently used stickers Stickers were downloaded on-demand only when visible and cancelled when scrolled off-screen, causing them to re-download every time the picker opened. This adds: - StickerPickerStore.prefetchStickers() that pre-downloads all favorite and recent sticker render files at priority 4 (highest) - Pinned file IDs protected from cancellation by cancelFileDownload() - TGBridgeStickersSignals.prefetchRecentStickersWithLimit: that triggers the full image download pipeline for recent stickers through TGFileCache - Re-enabled the in-memory caching layer for recent stickers in the legacy WatchKit extension - TGStickersController now calls prefetch after loading the sticker list --- .../Watch/Bridge/TGBridgeStickersSignals.h | 2 + .../Watch/Bridge/TGBridgeStickersSignals.m | 38 +++++++++++++------ .../Watch/Extension/TGStickersController.m | 1 + .../Stickers/StickerPicker.swift | 8 ++++ .../Stickers/StickerPickerStore.swift | 29 ++++++++++++++ .../Stickers/StickerPickerView.swift | 1 + 6 files changed, 67 insertions(+), 12 deletions(-) diff --git a/Telegram/Watch/Bridge/TGBridgeStickersSignals.h b/Telegram/Watch/Bridge/TGBridgeStickersSignals.h index 3b549b2aa8c..87c8cc20a2d 100644 --- a/Telegram/Watch/Bridge/TGBridgeStickersSignals.h +++ b/Telegram/Watch/Bridge/TGBridgeStickersSignals.h @@ -7,4 +7,6 @@ + (NSURL *)stickerPacksURL; ++ (void)prefetchRecentStickersWithLimit:(NSUInteger)limit; + @end diff --git a/Telegram/Watch/Bridge/TGBridgeStickersSignals.m b/Telegram/Watch/Bridge/TGBridgeStickersSignals.m index b5c7d9e2627..a647b5933b9 100644 --- a/Telegram/Watch/Bridge/TGBridgeStickersSignals.m +++ b/Telegram/Watch/Bridge/TGBridgeStickersSignals.m @@ -4,6 +4,8 @@ #import "TGBridgeStickerPack.h" #import "TGBridgeClient.h" +#import "TGBridgeMediaSignals.h" +#import "TGWatchCommon.h" @implementation TGBridgeStickersSignals @@ -16,18 +18,17 @@ + (SSignal *)cachedRecentStickers + (SSignal *)recentStickersWithLimit:(NSUInteger)limit { - return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeRecentStickersSubscription alloc] initWithLimit:limit]]; -// return [[self cachedRecentStickers] mapToSignal:^SSignal *(NSArray *stickers) { -// SSignal *remote = [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeRecentStickersSubscription alloc] initWithLimit:limit]]; -// remote = [remote onNext:^(NSArray *stickers) { -// cachedStickers = stickers; -// }]; -// if (stickers != nil) { -// return [[SSignal single:stickers] then:remote]; -// } else { -// return remote; -// } -// }]; + return [[self cachedRecentStickers] mapToSignal:^SSignal *(NSArray *stickers) { + SSignal *remote = [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeRecentStickersSubscription alloc] initWithLimit:limit]]; + remote = [remote onNext:^(NSArray *stickers) { + cachedStickers = stickers; + }]; + if (stickers != nil) { + return [[SSignal single:stickers] then:remote]; + } else { + return remote; + } + }]; } + (SSignal *)stickerPacks @@ -47,4 +48,17 @@ + (NSURL *)stickerPacksURL return stickerPacksURL; } ++ (void)prefetchRecentStickersWithLimit:(NSUInteger)limit +{ + [[[self recentStickersWithLimit:limit] deliverOn:[SQueue mainQueue]] startWithNext:^(NSArray *stickers) + { + for (TGBridgeDocumentMediaAttachment *sticker in stickers) + { + CGSize stickerSize = TGWatchStickerSizeForScreen(TGWatchScreenType()); + NSString *imageUrl = [NSString stringWithFormat:@"sticker_%lld_%dx%d_0", sticker.documentId, (int)stickerSize.width, (int)stickerSize.height]; + [[[TGBridgeMediaSignals stickerWithDocumentId:sticker.documentId packId:sticker.stickerPackId accessHash:sticker.stickerPackAccessHash type:TGMediaStickerImageTypeNormal] deliverOn:[SQueue mainQueue]] startWithNext:^(id next) {}]; + } + }]; +} + @end diff --git a/Telegram/Watch/Extension/TGStickersController.m b/Telegram/Watch/Extension/TGStickersController.m index 90d0f451253..4cfbc45c42d 100644 --- a/Telegram/Watch/Extension/TGStickersController.m +++ b/Telegram/Watch/Extension/TGStickersController.m @@ -117,6 +117,7 @@ - (void)reloadData updateInteface(initial, currentStickerModels, true); } }]]; + [TGBridgeStickersSignals prefetchRecentStickersWithLimit:24]; } else { diff --git a/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerPicker.swift b/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerPicker.swift index 8422b1b65a7..2f5174a15c3 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerPicker.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerPicker.swift @@ -19,6 +19,14 @@ struct PickerSticker: Identifiable, Equatable, Hashable { let emoji: String let width: Int let height: Int + + var renderFileId: Int? { + switch render { + case .raster(let id, _): return id + case .lottie(let id): return id + case .none: return nil + } + } } /// How to produce a static grid-tile image for a sticker. diff --git a/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerPickerStore.swift b/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerPickerStore.swift index eae36e5229e..cdb15198549 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerPickerStore.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerPickerStore.swift @@ -21,6 +21,7 @@ final class StickerPickerStore { private let logger = Logger(subsystem: "org.telegram.TelegramWatch", category: "stickerpicker") private var files: [Int: File] = [:] private var trackedFileIds: Set = [] + private var pinnedFileIds: Set = [] private var setStickers: [Int64: [PickerSticker]] = [:] init(loader: StickerPickerLoader) { @@ -52,6 +53,7 @@ final class StickerPickerStore { } func cancelFileDownload(fileId: Int) { + guard !pinnedFileIds.contains(fileId) else { return } trackedFileIds.remove(fileId) logger.info("cancelFileDownload fileId=\(fileId, privacy: .public)") Task { [logger, loader] in @@ -63,6 +65,33 @@ final class StickerPickerStore { } } + /// Pre-downloads favorite and recent stickers with high priority so they are + /// cached on disk before the user scrolls to them. Pinned file IDs are + /// protected from cancellation by `cancelFileDownload`. + func prefetchStickers() { + let all = favorites + recents + var renderIds = Set() + for sticker in all { + if let id = sticker.renderFileId { + renderIds.insert(id) + } + } + guard !renderIds.isEmpty else { return } + let newIds = renderIds.subtracting(trackedFileIds) + pinnedFileIds.formUnion(newIds) + trackedFileIds.formUnion(newIds) + logger.info("prefetchStickers: pinning \(newIds.count, privacy: .public) sticker render files") + for id in newIds { + Task { [logger, loader] in + do { + _ = try await loader.downloadFile(fileId: id, priority: 4) + } catch { + logger.warning("prefetch fileId=\(id, privacy: .public) failed: \(error.localizedDescription, privacy: .public)") + } + } + } + } + /// Fetches favorites, recents, and installed sets concurrently. Each source /// is independent — one failing source still renders the others. `.failed` /// only when all three fail. Re-runs on every sheet open (keeps recents/ diff --git a/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerPickerView.swift b/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerPickerView.swift index feab1c7df85..8a11a5faae4 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerPickerView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerPickerView.swift @@ -30,6 +30,7 @@ struct StickerPickerView: View { .task { client.setActiveStickerPicker(store) await store.load() + store.prefetchStickers() } .onDisappear { client.setActiveStickerPicker(nil) } }