From b5d50c45da665204fb45ad0e845d6209dc291f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Thu, 5 Mar 2026 13:43:06 +0100 Subject: [PATCH] =?UTF-8?q?wip:=20iOS=20refactor=20=E2=80=94=20iMessage=20?= =?UTF-8?q?extension,=20sticker=20browser,=20app=20icon=20assets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- PLAN.md | 146 +++++++++++++ StickerCloner/Shared/StickerStore.swift | 21 ++ .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 67192 bytes .../AppIcon.appiconset/Contents.json | 14 ++ .../MessagesViewController.swift | 1 - .../StickerBrowserViewController.swift | 206 +++++++++++++++++- .../StickerCloner.xcodeproj/project.pbxproj | 20 +- StickerCloner/StickerCloner/AddPackView.swift | 25 ++- 8 files changed, 410 insertions(+), 23 deletions(-) create mode 100644 PLAN.md create mode 100644 StickerCloner/StickerCloner MessagesExtension/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100644 StickerCloner/StickerCloner MessagesExtension/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..b482f87 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,146 @@ +# Plan: standalone iOS app + invisible iMessage sticker extension + +## Goal + +- **iOS app** on the home screen: paste Telegram URL, download packs, preview stickers, toggle individual stickers on/off +- **iMessage extension** is invisible — no custom UI. It serves stickers via `MSStickerBrowserViewController` so they appear in the system sticker picker (emoji keyboard → stickers tab on iOS 17+) +- Data flows from the iOS app → shared App Group container → extension reads it + +## Why the 404 happens + +The backend always calls `manager.getPack()` (Telegram Bot API) before checking its cache. If the bot token is invalid or Telegram is unreachable, it returns `None` → 404, even when sticker files exist on disk. This is a **backend bug** — fix separately. The iOS URL construction is correct. + +## Architecture + +``` +┌─────────────────────────┐ ┌──────────────────────────┐ +│ iOS App (home screen) │ │ iMessage Extension │ +│ │ │ (no custom UI) │ +│ - Paste URL │ │ │ +│ - Download packs │ ──► │ MSStickerBrowserVC │ +│ - Preview stickers │ │ reads shared container │ +│ - Toggle on/off │ │ → stickers appear in │ +│ - Delete packs │ │ system sticker picker │ +└─────────────────────────┘ └──────────────────────────┘ + │ │ + └──────── App Group ───────────────┘ + UserDefaults(suiteName:) + shared Caches/stickers/ +``` + +## File layout after changes + +``` +StickerCloner/ +├── Shared/ ← NEW synced folder, both targets +│ ├── Models.swift moved + updated +│ └── StickerStore.swift moved + rewritten (no Messages import) +│ +├── StickerCloner/ ← iOS app target (auto-synced) +│ ├── Assets.xcassets/ existing +│ ├── StickerClonerApp.swift NEW — SwiftUI @main entry +│ ├── PackListView.swift NEW — list of packs + URL input +│ ├── PackDetailView.swift NEW — sticker grid with toggles +│ └── StickerClonerAPI.swift moved from extension (only app needs it) +│ +├── StickerCloner MessagesExtension/ ← extension target (auto-synced) +│ ├── Assets.xcassets/ existing +│ ├── Base.lproj/MainInterface.storyboard existing +│ ├── Info.plist existing +│ ├── MessagesViewController.swift rewritten (minimal) +│ └── StickerBrowserViewController.swift rewritten (reads shared data) +│ +└── StickerCloner.xcodeproj/ +``` + +## Project file (pbxproj) changes + +1. **Change main target product type**: `com.apple.product-type.application.messages` → `com.apple.product-type.application` +2. **Add Sources build phase** to main target (empty — synced groups handle inclusion) +3. **Add `Shared/` PBXFileSystemSynchronizedRootGroup** to both targets' `fileSystemSynchronizedGroups` +4. **Add App Group entitlements** via `CODE_SIGN_ENTITLEMENTS` build setting on both targets + +## Data model changes + +### Models.swift (Shared/) + +```swift +// API types (unchanged) +struct StickerSetResponse: Codable { ... } +struct StickerResponse: Codable { ... } + +// Local persistence — now tracks enabled stickers +struct SavedPack: Codable { + let name: String + let title: String + let stickerIds: [String] // all sticker IDs in order + var enabledStickerIds: Set // subset the user wants +} +``` + +### StickerStore.swift (Shared/) + +- Uses `UserDefaults(suiteName: "group.de.felixfoertsch.StickerCloner")` instead of `.standard` +- Uses `FileManager.containerURL(forSecurityApplicationGroupIdentifier:)` instead of caches directory +- `stickerFileURLs(for pack:)` returns `[URL]` (not MSSticker — no Messages dependency) +- Returns only enabled stickers + +### StickerClonerAPI.swift (iOS app only) + +Stays in the app target — the extension never calls the API. Unchanged except move location. + +## iOS App views + +### StickerClonerApp.swift +- `@main` SwiftUI app +- Single `PackListView` as root in NavigationStack + +### PackListView.swift +- Text field + "Add" button to paste Telegram URL or pack name +- List of saved packs with swipe-to-delete +- Navigation link to `PackDetailView` per pack +- Loading/error states during download + +### PackDetailView.swift +- Grid of sticker thumbnails (loaded from local PNG files) +- Tap to toggle enabled/disabled (dimmed + checkmark overlay) +- "Select All" / "Deselect All" toolbar buttons +- Pack title in navigation bar + +## Extension changes + +### MessagesViewController.swift +- Hosts `StickerBrowserViewController` as child, fills entire view +- No expanded mode handling, no SwiftUI views +- Reloads stickers on `willBecomeActive` + +### StickerBrowserViewController.swift +- Loads enabled sticker file URLs from `StickerStore` +- Creates `MSSticker` from each URL +- Standard `MSStickerBrowserViewController` data source + +## Entitlements + +Both targets get identical entitlements: +```xml +com.apple.security.application-groups + + group.de.felixfoertsch.StickerCloner + +``` + +## Commits + +1. `convert to standalone iOS app, add shared folder, app group entitlements` +2. `add iOS app ui: pack list, pack detail with sticker filtering` +3. `simplify iMessage extension to read-only sticker browser` + +## Verification + +1. Build in Xcode → no compile errors for both targets +2. iOS app appears on home screen +3. Paste pack URL → downloads (once backend 404 is fixed) +4. Tap pack → see sticker grid, toggle individual stickers +5. Open Messages → stickers appear in sticker picker +6. Only enabled stickers shown +7. Kill app, reopen → state persists via App Group diff --git a/StickerCloner/Shared/StickerStore.swift b/StickerCloner/Shared/StickerStore.swift index 19ebe99..6db556f 100644 --- a/StickerCloner/Shared/StickerStore.swift +++ b/StickerCloner/Shared/StickerStore.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit final class StickerStore { static let shared = StickerStore() @@ -6,6 +7,7 @@ final class StickerStore { private let packsKey = "savedPacks" private let suiteName = "group.de.felixfoertsch.StickerCloner" private let fileManager = FileManager.default + private let thumbnailCache = NSCache() private lazy var defaults: UserDefaults = { guard let defaults = UserDefaults(suiteName: suiteName) else { @@ -66,6 +68,7 @@ final class StickerStore { var packs = savedPacks packs.removeAll { $0.name == name } savedPacks = packs + thumbnailCache.removeObject(forKey: name as NSString) let packDir = packDirectory(name: name) try? fileManager.removeItem(at: packDir) @@ -89,6 +92,14 @@ final class StickerStore { return urls } + func stickerFileURLs(forPack name: String) -> [URL] { + let packDir = packDirectory(name: name) + guard let files = try? fileManager.contentsOfDirectory(at: packDir, includingPropertiesForKeys: nil) else { return [] } + return files + .filter { $0.pathExtension == "png" } + .sorted { $0.lastPathComponent < $1.lastPathComponent } + } + func firstStickerURL(forPack name: String) -> URL? { let packDir = packDirectory(name: name) guard let files = try? fileManager.contentsOfDirectory(at: packDir, includingPropertiesForKeys: nil) else { return nil } @@ -98,6 +109,16 @@ final class StickerStore { .first } + func thumbnail(forPack name: String) -> UIImage? { + let key = name as NSString + if let cached = thumbnailCache.object(forKey: key) { return cached } + guard let url = firstStickerURL(forPack: name), + let data = try? Data(contentsOf: url), + let image = UIImage(data: data) else { return nil } + thumbnailCache.setObject(image, forKey: key) + return image + } + // MARK: - Migration from old sandbox private func migrateIfNeeded() { diff --git a/StickerCloner/StickerCloner MessagesExtension/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/StickerCloner/StickerCloner MessagesExtension/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..a0d2f6607bee07f71216aa41fcea94b7a7997ece GIT binary patch literal 67192 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 ZSfIR9-}MevH7|gtWXH2by@i$ae*vb6eiQ%z literal 0 HcmV?d00001 diff --git a/StickerCloner/StickerCloner MessagesExtension/Assets.xcassets/AppIcon.appiconset/Contents.json b/StickerCloner/StickerCloner MessagesExtension/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..cefcc87 --- /dev/null +++ b/StickerCloner/StickerCloner MessagesExtension/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "AppIcon.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StickerCloner/StickerCloner MessagesExtension/MessagesViewController.swift b/StickerCloner/StickerCloner MessagesExtension/MessagesViewController.swift index efd0056..bcd5cf1 100644 --- a/StickerCloner/StickerCloner MessagesExtension/MessagesViewController.swift +++ b/StickerCloner/StickerCloner MessagesExtension/MessagesViewController.swift @@ -7,7 +7,6 @@ final class MessagesViewController: MSMessagesAppViewController { override func viewDidLoad() { super.viewDidLoad() addStickerBrowser() - stickerBrowser.reloadStickers() } override func willBecomeActive(with conversation: MSConversation) { diff --git a/StickerCloner/StickerCloner MessagesExtension/StickerBrowserViewController.swift b/StickerCloner/StickerCloner MessagesExtension/StickerBrowserViewController.swift index 9a3612d..115dbe1 100644 --- a/StickerCloner/StickerCloner MessagesExtension/StickerBrowserViewController.swift +++ b/StickerCloner/StickerCloner MessagesExtension/StickerBrowserViewController.swift @@ -2,7 +2,23 @@ import Messages import UIKit final class StickerBrowserViewController: MSStickerBrowserViewController { - private var stickers: [MSSticker] = [] + private struct Pack { + let savedPack: SavedPack + let thumbnail: UIImage? + let stickers: [MSSticker] + } + + private var packs: [Pack] = [] + private var selectedPackIndex: Int? // nil = all packs + private var displayedStickers: [MSSticker] = [] + + private var tabCollectionView: UICollectionView! + + private let tabSize: CGFloat = 36 + private let tabSpacing: CGFloat = 6 + private let tabInset: CGFloat = 8 + + // MARK: - Lifecycle init() { super.init(stickerSize: .small) @@ -13,24 +29,196 @@ final class StickerBrowserViewController: MSStickerBrowserViewController { fatalError("init(coder:) is not supported") } + override func viewDidLoad() { + super.viewDidLoad() + setupTabBar() + } + + // MARK: - Data + func reloadStickers() { - let urls = StickerStore.shared.stickerFileURLs() - stickers = urls.compactMap { url in - try? MSSticker( - contentsOfFileURL: url, - localizedDescription: url.deletingLastPathComponent().lastPathComponent - ) + packs = StickerStore.shared.savedPacks + .sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending } + .compactMap { pack in + let urls = StickerStore.shared.stickerFileURLs(forPack: pack.name) + let stickers = urls.compactMap { url in + try? MSSticker(contentsOfFileURL: url, localizedDescription: pack.title) + } + guard !stickers.isEmpty else { return nil } + let thumbnail = StickerStore.shared.thumbnail(forPack: pack.name) + return Pack(savedPack: pack, thumbnail: thumbnail, stickers: stickers) + } + + if let index = selectedPackIndex, index >= packs.count { + selectedPackIndex = nil + } + + updateDisplayedStickers() + tabCollectionView?.reloadData() + } + + private func updateDisplayedStickers() { + if let index = selectedPackIndex { + displayedStickers = packs[index].stickers + } else { + displayedStickers = packs.flatMap(\.stickers) } stickerBrowserView.reloadData() } + // MARK: - Tab bar + + private func setupTabBar() { + let tabBarHeight = tabSize + tabInset * 2 + + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumInteritemSpacing = tabSpacing + layout.minimumLineSpacing = tabSpacing + layout.sectionInset = UIEdgeInsets(top: 0, left: tabInset, bottom: 0, right: tabInset) + layout.itemSize = CGSize(width: tabSize, height: tabSize) + + tabCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + tabCollectionView.translatesAutoresizingMaskIntoConstraints = false + tabCollectionView.backgroundColor = .secondarySystemBackground + tabCollectionView.showsHorizontalScrollIndicator = false + tabCollectionView.dataSource = self + tabCollectionView.delegate = self + tabCollectionView.register(TabCell.self, forCellWithReuseIdentifier: TabCell.reuseID) + + let separator = UIView() + separator.translatesAutoresizingMaskIntoConstraints = false + separator.backgroundColor = .separator + + view.addSubview(tabCollectionView) + view.addSubview(separator) + + NSLayoutConstraint.activate([ + tabCollectionView.topAnchor.constraint(equalTo: view.topAnchor), + tabCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tabCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tabCollectionView.heightAnchor.constraint(equalToConstant: tabBarHeight), + + separator.topAnchor.constraint(equalTo: tabCollectionView.bottomAnchor), + separator.leadingAnchor.constraint(equalTo: view.leadingAnchor), + separator.trailingAnchor.constraint(equalTo: view.trailingAnchor), + separator.heightAnchor.constraint(equalToConstant: 1.0 / UIScreen.main.scale), + ]) + + additionalSafeAreaInsets = UIEdgeInsets(top: tabBarHeight, left: 0, bottom: 0, right: 0) + } + + private func selectPack(at index: Int?) { + guard selectedPackIndex != index else { return } + selectedPackIndex = index + updateDisplayedStickers() + tabCollectionView.reloadData() + } + // MARK: - MSStickerBrowserViewDataSource override func numberOfStickers(in stickerBrowserView: MSStickerBrowserView) -> Int { - stickers.count + displayedStickers.count } override func stickerBrowserView(_ stickerBrowserView: MSStickerBrowserView, stickerAt index: Int) -> MSSticker { - stickers[index] + displayedStickers[index] + } +} + +// MARK: - UICollectionViewDataSource + +extension StickerBrowserViewController: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + packs.count + 1 // +1 for "All" tab + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TabCell.reuseID, for: indexPath) as! TabCell + if indexPath.item == 0 { + let isSelected = selectedPackIndex == nil + cell.configureAsAll(isSelected: isSelected) + } else { + let pack = packs[indexPath.item - 1] + let isSelected = selectedPackIndex == indexPath.item - 1 + cell.configure(image: pack.thumbnail, isSelected: isSelected) + } + return cell + } +} + +// MARK: - UICollectionViewDelegate + +extension StickerBrowserViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if indexPath.item == 0 { + selectPack(at: nil) + } else { + selectPack(at: indexPath.item - 1) + } + } +} + +// MARK: - TabCell + +private final class TabCell: UICollectionViewCell { + static let reuseID = "TabCell" + + private let imageView = UIImageView() + private let label = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.layer.cornerRadius = 8 + contentView.clipsToBounds = true + + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(imageView) + + label.font = .systemFont(ofSize: 11, weight: .medium) + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + label.isHidden = true + contentView.addSubview(label) + + let inset: CGFloat = 4 + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset), + imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset), + imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset), + imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset), + + label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + ]) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported") + } + + func configure(image: UIImage?, isSelected: Bool) { + imageView.image = image + imageView.isHidden = false + label.isHidden = true + contentView.backgroundColor = isSelected ? .systemGray5 : .clear + imageView.alpha = isSelected ? 1.0 : 0.5 + } + + func configureAsAll(isSelected: Bool) { + imageView.isHidden = true + label.isHidden = false + label.text = "All" + label.textColor = isSelected ? .label : .secondaryLabel + contentView.backgroundColor = isSelected ? .systemGray5 : .clear + } + + override func prepareForReuse() { + super.prepareForReuse() + imageView.image = nil + imageView.isHidden = false + label.isHidden = true } } diff --git a/StickerCloner/StickerCloner.xcodeproj/project.pbxproj b/StickerCloner/StickerCloner.xcodeproj/project.pbxproj index 8ee59a7..6e9b07d 100644 --- a/StickerCloner/StickerCloner.xcodeproj/project.pbxproj +++ b/StickerCloner/StickerCloner.xcodeproj/project.pbxproj @@ -73,13 +73,6 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ - 3FD2D01B2F57098E00B76B50 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 3FD2CFFD2F57098D00B76B50 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -88,6 +81,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3FD2D01B2F57098E00B76B50 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -226,14 +226,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 3FD2D01A2F57098E00B76B50 /* Sources */ = { + 3FD2CFFC2F57098D00B76B50 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 3FD2CFFC2F57098D00B76B50 /* Sources */ = { + 3FD2D01A2F57098E00B76B50 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -255,6 +255,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; + ASSETCATALOG_COMPILER_STICKER_ICON_NAME = "iMessage App Icon"; CODE_SIGN_ENTITLEMENTS = "StickerCloner MessagesExtension/StickerClonerMessagesExtension.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -285,6 +286,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; + ASSETCATALOG_COMPILER_STICKER_ICON_NAME = "iMessage App Icon"; CODE_SIGN_ENTITLEMENTS = "StickerCloner MessagesExtension/StickerClonerMessagesExtension.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; diff --git a/StickerCloner/StickerCloner/AddPackView.swift b/StickerCloner/StickerCloner/AddPackView.swift index 679e022..b221500 100644 --- a/StickerCloner/StickerCloner/AddPackView.swift +++ b/StickerCloner/StickerCloner/AddPackView.swift @@ -41,13 +41,30 @@ struct AddPackView: View { Text("Paste a Telegram sticker link or pack name.") } - if !packs.isEmpty { + if packs.isEmpty { + Section { + Button { + urlText = "https://t.me/addstickers/MrBat" + } label: { + HStack(spacing: 12) { + Image(systemName: "star") + .foregroundStyle(.orange) + VStack(alignment: .leading) { + Text("MrBat") + Text("Tap to try this pack") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } header: { + Text("Suggestions") + } + } else { Section("Saved Packs") { ForEach(packs, id: \.name) { pack in HStack(spacing: 12) { - if let url = StickerStore.shared.firstStickerURL(forPack: pack.name), - let data = try? Data(contentsOf: url), - let uiImage = UIImage(data: data) { + if let uiImage = StickerStore.shared.thumbnail(forPack: pack.name) { Image(uiImage: uiImage) .resizable() .aspectRatio(contentMode: .fit)