Skip to content

Commit 6208b89

Browse files
committed
Add Bookmarks feature: new controller, QuickBookmarks screen, assets, and integrations
1 parent eac655f commit 6208b89

29 files changed

Lines changed: 1843 additions & 59 deletions

File tree

MODULE.bazel.lock

Lines changed: 77 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Telegram/Telegram-iOS/en.lproj/Localizable.strings

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5378,6 +5378,10 @@ Sorry for the inconvenience.";
53785378
"ChatList.Tabs.AllChats" = "All Chats";
53795379
"ChatList.Tabs.All" = "All";
53805380
"Settings.ChatFolders" = "Chat Folders";
5381+
"Settings.Bookmarks" = "Bookmarks";
5382+
"Settings.Bookmarks_ListHeader" = "BOOKMARKS";
5383+
"Settings.Bookmarks_AddNew" = "Create New Bookmark";
5384+
53815385
"ChatList.ChatTypesSection" = "CHAT TYPES";
53825386
"ChatList.PeerTypeNonContact" = "user";
53835387
"ChatList.PeerTypeContact" = "contact";
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
"bundle_id": "org.{! a random string !}.Telegram",
3-
"api_id": "{! get one at https://my.telegram.org/apps !}",
4-
"api_hash": "{! get one at https://my.telegram.org/apps !}",
5-
"team_id": "{! check README.md !}",
2+
"bundle_id": "org.5cadfb01253d109e.Telegram",
3+
"api_id": "27611144",
4+
"api_hash": "780adc61658db42203ac31143c6dcb92",
5+
"team_id": "REMYSXBH9T",
66
"app_center_id": "0",
77
"is_internal_build": "true",
88
"is_appstore_build": "false",
@@ -11,4 +11,4 @@
1111
"premium_iap_product_id": "",
1212
"enable_siri": false,
1313
"enable_icloud": false
14-
}
14+
}

submodules/AccountContext/Sources/AccountContext.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,8 @@ public protocol SharedAccountContext: AnyObject {
12251225
func makeStorySelectionController(context: AccountContext, peerId: EnginePeer.Id, excludeIds: [Int32], completion: @escaping ([EngineStoryItem]) -> Void) -> ViewController
12261226
func makeArchiveSettingsController(context: AccountContext) -> ViewController
12271227
func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController
1228+
func makeBookmarksController(context: AccountContext) -> ViewController
1229+
12281230
func makeBusinessSetupScreen(context: AccountContext) -> ViewController
12291231
func makeChatbotSetupScreen(context: AccountContext, initialData: ChatbotSetupScreenInitialData) -> ViewController
12301232
func makeChatbotSetupScreenInitialData(context: AccountContext) -> Signal<ChatbotSetupScreenInitialData, NoError>
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import Foundation
2+
import UIKit
3+
import Display
4+
import AsyncDisplayKit
5+
import SwiftSignalKit
6+
import TelegramCore
7+
import AccountContext
8+
import MapResourceToAvatarSizes
9+
import TelegramPresentationData
10+
import Postbox
11+
12+
private final class BookmarksLauncherController: ViewController {
13+
private let context: AccountContext
14+
private var didLaunch = false
15+
16+
init(context: AccountContext) {
17+
self.context = context
18+
super.init(navigationBarPresentationData: nil)
19+
self.navigationPresentation = .modal
20+
self.statusBar.statusBarStyle = .Ignore
21+
self.title = nil
22+
}
23+
24+
required init(coder: NSCoder) {
25+
preconditionFailure()
26+
}
27+
28+
override func viewDidLoad() {
29+
super.viewDidLoad()
30+
self.view.backgroundColor = .clear
31+
}
32+
33+
override public func viewDidAppear(_ animated: Bool) {
34+
super.viewDidAppear(animated)
35+
if self.didLaunch { return }
36+
self.didLaunch = true
37+
self.ensureAndOpenBookmarks()
38+
}
39+
40+
private func ensureAndOpenBookmarks() {
41+
let context = self.context
42+
let resolve: Signal<EnginePeer?, NoError> = resolveOrCreateBookmarksPeer(context: context)
43+
44+
let _ : Disposable = (resolve
45+
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
46+
guard let self else { return }
47+
if let peer, let navigationController = self.navigationController as? NavigationController {
48+
// Зафиксируем отображение как Messages
49+
let _ = self.context.engine.peers.updateForumViewAsMessages(peerId: peer.id, value: true).startStandalone()
50+
self.context.sharedContext.navigateToChatController(
51+
NavigateToChatControllerParams(
52+
navigationController: navigationController,
53+
context: self.context,
54+
chatLocation: .peer(peer),
55+
keepStack: .always
56+
)
57+
)
58+
}
59+
// This launcher controller can be removed from the stack
60+
if let navigationController = self.navigationController as? NavigationController {
61+
var controllers = navigationController.viewControllers
62+
controllers = controllers.filter { $0 !== self }
63+
navigationController.setViewControllers(controllers, animated: false)
64+
}
65+
})
66+
}
67+
}
68+
69+
// MARK: - Helpers (strongly typed to avoid inference issues)
70+
71+
private func resolveOrCreateBookmarksPeer(context: AccountContext) -> Signal<EnginePeer?, NoError> {
72+
let presentationData: PresentationData = context.sharedContext.currentPresentationData.with { $0 }
73+
let search: Signal<[EngineRenderedPeer], NoError> = context.engine.contacts.searchLocalPeers(query: "Bookmarks")
74+
75+
let found: Signal<EnginePeer?, NoError> = (search
76+
|> take(1)
77+
|> map { (peers: [EngineRenderedPeer]) -> EnginePeer? in
78+
for rendered in peers {
79+
if let peer = rendered.peer, case let .channel(channel) = peer, channel.isForum {
80+
if peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) == "Bookmarks" {
81+
return peer
82+
}
83+
}
84+
}
85+
return nil
86+
})
87+
88+
return found
89+
|> mapToSignal { maybePeer -> Signal<EnginePeer?, NoError> in
90+
if let peer = maybePeer {
91+
// Ensure the Bookmarks group has the bookmarks icon as its avatar
92+
if let image = PresentationResourcesSettings.bookmarks, let data = image.jpegData(compressionQuality: 0.9) {
93+
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
94+
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
95+
let _ = (context.engine.peers.updatePeerPhoto(
96+
peerId: peer.id,
97+
photo: context.engine.peers.uploadedPeerPhoto(resource: resource),
98+
mapResourceToAvatarSizes: { res, reps in
99+
return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: res, representations: reps)
100+
}
101+
)
102+
|> deliverOnMainQueue).startStandalone()
103+
}
104+
return Signal<EnginePeer?, NoError>.single(peer)
105+
} else {
106+
return createBookmarksPeer(context: context)
107+
}
108+
}
109+
}
110+
111+
private func createBookmarksPeer(context: AccountContext) -> Signal<EnginePeer?, NoError> {
112+
// 1) Create forum supergroup → PeerId?, NoError
113+
let createdId: Signal<PeerId?, NoError> = (context.engine.peers.createSupergroup(
114+
title: "Bookmarks",
115+
description: "",
116+
isForum: true,
117+
ttlPeriod: nil
118+
)
119+
|> map(Optional.init)
120+
|> `catch` { _ -> Signal<PeerId?, NoError> in
121+
return .single(nil)
122+
})
123+
124+
// 2) Resolve EnginePeer from id; set avatar; return EnginePeer?, NoError
125+
return createdId
126+
|> mapToSignal { (peerIdOpt: PeerId?) -> Signal<EnginePeer?, NoError> in
127+
guard let peerId = peerIdOpt else {
128+
return .single(nil)
129+
}
130+
return (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
131+
|> take(1))
132+
|> mapToSignal { (peerOpt: EnginePeer?) -> Signal<EnginePeer?, NoError> in
133+
if let peer = peerOpt, case let .channel(channel) = peer, channel.isForum {
134+
if let image = PresentationResourcesSettings.bookmarks, let data = image.jpegData(compressionQuality: 0.9) {
135+
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
136+
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
137+
let _ = (context.engine.peers.updatePeerPhoto(
138+
peerId: peer.id,
139+
photo: context.engine.peers.uploadedPeerPhoto(resource: resource),
140+
mapResourceToAvatarSizes: { res, reps in
141+
return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: res, representations: reps)
142+
}
143+
)
144+
|> deliverOnMainQueue).startStandalone()
145+
}
146+
// View as Messages (и запретим пользователю переключать меню далее)
147+
let _ = context.engine.peers.setChannelForumMode(id: peer.id, isForum: true, displayForumAsTabs: true).startStandalone()
148+
let _ = context.engine.peers.updateForumViewAsMessages(peerId: peer.id, value: true).startStandalone()
149+
}
150+
return .single(peerOpt)
151+
}
152+
}
153+
}
154+
155+
public func bookmarksController(context: AccountContext) -> ViewController {
156+
return BookmarksLauncherController(context: context)
157+
}

submodules/ChatListUI/Sources/ChatListController.swift

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3694,7 +3694,46 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
36943694
return
36953695
}
36963696

3697-
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
3697+
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
3698+
let strings = presentationData.strings
3699+
3700+
// Restrict menu for Bookmarks feature group: only Search and New Topic
3701+
if let peer = peer, peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) == "Bookmarks" {
3702+
var items: [ContextMenuItem] = []
3703+
if let sourceController = sourceController as? ChatController {
3704+
items.append(.action(ContextMenuActionItem(text: strings.Conversation_Search, icon: { theme in
3705+
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Search"), color: theme.contextMenu.primaryColor)
3706+
}, action: { [weak sourceController] action in
3707+
action.dismissWithResult(.default)
3708+
sourceController?.beginMessageSearch("")
3709+
})))
3710+
}
3711+
if channel.hasPermission(.createTopics) {
3712+
items.append(.action(ContextMenuActionItem(text: strings.Chat_CreateTopic, icon: { theme in
3713+
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
3714+
}, action: { action in
3715+
action.dismissWithResult(.default)
3716+
let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create)
3717+
controller.navigationPresentation = .modal
3718+
controller.completion = { [weak controller] title, fileId, iconColor, _ in
3719+
controller?.isInProgress = true
3720+
controller?.view.endEditing(true)
3721+
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: iconColor, iconFileId: fileId)
3722+
|> deliverOnMainQueue).startStandalone(next: { topicId in
3723+
if let navigationController = (sourceController.navigationController as? NavigationController) {
3724+
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, messageId: nil, navigationController: navigationController, activateInput: .text, scrollToEndIfExists: false, keepStack: .never, animated: true).startStandalone()
3725+
}
3726+
}, error: { _ in
3727+
controller?.isInProgress = false
3728+
})
3729+
}
3730+
sourceController.push(controller)
3731+
})))
3732+
}
3733+
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: sourceController, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
3734+
sourceController.presentInGlobalOverlay(contextController)
3735+
return
3736+
}
36983737

36993738
var items: [ContextMenuItem] = []
37003739

@@ -3845,7 +3884,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
38453884
})))
38463885
}
38473886

3848-
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
38493887
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: sourceController, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
38503888
sourceController.presentInGlobalOverlay(contextController)
38513889
})

submodules/ShareController/Sources/ShareController.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@ public final class ShareController: ViewController {
420420

421421
private var defaultAction: ShareControllerAction?
422422
public private(set) var actionIsMediaSaving = false
423+
public var hideTopicsBackButton: Bool = false
424+
public var onTopicSelected: ((PeerId, Int64, String) -> Void)?
423425

424426
public var actionCompleted: (() -> Void)?
425427
public var dismissed: ((Bool) -> Void)?

0 commit comments

Comments
 (0)